From 2cb52dcf90772256821fc9116814425b9d8ba2a9 Mon Sep 17 00:00:00 2001 From: laki Date: Fri, 20 Mar 2026 18:11:12 +0000 Subject: [PATCH] VERSION 0.9.0 - Added chapters functionality, volume boost to 150%, organised menus better, adjusted button sizes, fixed fullscreen mode stretching out onto multiple monitors, better plugins/scripts support, etc. This is a major release! --- .gitignore | 1 + install.sh | 8 +- src/controls.c | 217 +++++++++++++++++++++++++++++++--- src/controls.h | 2 + src/dialogs.c | 157 +++++++++++-------------- src/dialogs.h | 2 + src/main.c | 2 +- src/mpv_loader.c | 2 + src/mpv_loader.h | 2 + src/player.c | 97 ++++++++++++++- src/player.h | 10 ++ src/ui.c | 299 ++++++++++++++++++++++++++++++++++------------- 12 files changed, 605 insertions(+), 194 deletions(-) diff --git a/.gitignore b/.gitignore index 6f6492f..0a23147 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ make_deb_32.sh make_deb.sh make_release.sh gtk2-media-player-v0.7.1-linux.zip +GTK2-template \ No newline at end of file diff --git a/install.sh b/install.sh index 346b2f9..8702073 100755 --- a/install.sh +++ b/install.sh @@ -8,7 +8,8 @@ if [ "$EUID" -ne 0 ]; then fi INSTALL_DIR="/usr/local/lib/gtk2-mpv-player" -BIN_LINK="/usr/local/bin/gtk2-mpv-player" +BIN_LINK="/usr/bin/gtk2-mpv-player" +BIN_LINK_LOCAL="/usr/local/bin/gtk2-mpv-player" DESKTOP_FILE="/usr/share/applications/gtk2-mpv-player.desktop" echo "Installing to $INSTALL_DIR..." @@ -24,14 +25,15 @@ install -m 644 assets/icon.png "$INSTALL_DIR/" rm -rf "$INSTALL_DIR/lib" cp -r lib "$INSTALL_DIR/" -# Create wrapper script +# Create wrapper scripts cat < "$BIN_LINK" #!/bin/bash cd "$INSTALL_DIR" ./gtk2-mpv-player "\$@" EOF -chmod +x "$BIN_LINK" +cp "$BIN_LINK" "$BIN_LINK_LOCAL" +chmod +x "$BIN_LINK" "$BIN_LINK_LOCAL" # Install desktop file cp assets/gtk2-mpv-player.desktop "$DESKTOP_FILE" diff --git a/src/controls.c b/src/controls.c index b3be964..afd8cce 100644 --- a/src/controls.c +++ b/src/controls.c @@ -2,6 +2,7 @@ #include #include #include +#include struct Controls { Player *player; @@ -42,18 +43,26 @@ struct Controls { /* Icons (stock icons) */ GtkWidget *play_image; GtkWidget *pause_image; + + /* Chapters for snapping/markers */ + double *chapter_times; + int chapter_count; }; /* Forward declarations */ static void on_play_pause_clicked(GtkButton *button, gpointer data); static void on_stop_clicked(GtkButton *button, gpointer data); static void on_prev_clicked(GtkButton *button, gpointer data); +static double snap_to_chapter(Controls *controls, double val); static void on_next_clicked(GtkButton *button, gpointer data); static void on_mute_clicked(GtkButton *button, gpointer data); static void on_fullscreen_clicked(GtkButton *button, gpointer data); static gboolean on_seek_change_value(GtkRange *range, GtkScrollType scroll, gdouble value, gpointer data); + +#define SNAP_THRESHOLD 5.0 /* seconds */ static gboolean on_seek_button_press(GtkWidget *widget, GdkEventButton *event, gpointer data); static gboolean on_seek_button_release(GtkWidget *widget, GdkEventButton *event, gpointer data); +static gboolean on_seek_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer data); static void on_volume_changed(GtkRange *range, gpointer data); static void format_time(double seconds, char *buffer, size_t size) @@ -86,15 +95,24 @@ Controls* controls_new(Player *player, Preferences *prefs) /* Main horizontal box */ controls->hbox = gtk_hbox_new(FALSE, 5); gtk_container_set_border_width(GTK_CONTAINER(controls->hbox), 5); + /* Set a fixed height to prevent layout shift when markers appear at the bottom */ + gtk_widget_set_size_request(controls->hbox, -1, 50); gtk_container_add(GTK_CONTAINER(controls->root), controls->hbox); + /* Left side buttons container, wrapped in alignment to prevent vertical stretch */ + GtkWidget *left_align = gtk_alignment_new(0.5, 0.5, 0.0, 0.0); + GtkWidget *left_box = gtk_hbox_new(FALSE, 5); + gtk_container_add(GTK_CONTAINER(left_align), left_box); + gtk_box_pack_start(GTK_BOX(controls->hbox), left_align, FALSE, FALSE, 0); + /* Previous button */ controls->btn_prev = gtk_button_new(); gtk_button_set_image(GTK_BUTTON(controls->btn_prev), gtk_image_new_from_stock(GTK_STOCK_MEDIA_PREVIOUS, GTK_ICON_SIZE_BUTTON)); + gtk_widget_set_size_request(controls->btn_prev, 36, 36); gtk_widget_set_tooltip_text(controls->btn_prev, "Previous"); g_signal_connect(controls->btn_prev, "clicked", G_CALLBACK(on_prev_clicked), controls); - gtk_box_pack_start(GTK_BOX(controls->hbox), controls->btn_prev, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(left_box), controls->btn_prev, FALSE, FALSE, 0); /* Play/Pause button */ controls->btn_play_pause = gtk_button_new(); @@ -103,27 +121,30 @@ Controls* controls_new(Player *player, Preferences *prefs) g_object_ref(controls->play_image); g_object_ref(controls->pause_image); gtk_button_set_image(GTK_BUTTON(controls->btn_play_pause), controls->play_image); + gtk_widget_set_size_request(controls->btn_play_pause, 36, 36); gtk_widget_set_tooltip_text(controls->btn_play_pause, "Play/Pause"); g_signal_connect(controls->btn_play_pause, "clicked", G_CALLBACK(on_play_pause_clicked), controls); - gtk_box_pack_start(GTK_BOX(controls->hbox), controls->btn_play_pause, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(left_box), controls->btn_play_pause, FALSE, FALSE, 0); /* Stop button */ controls->btn_stop = gtk_button_new(); gtk_button_set_image(GTK_BUTTON(controls->btn_stop), gtk_image_new_from_stock(GTK_STOCK_MEDIA_STOP, GTK_ICON_SIZE_BUTTON)); + gtk_widget_set_size_request(controls->btn_stop, 36, 36); gtk_widget_set_tooltip_text(controls->btn_stop, "Stop"); g_signal_connect(controls->btn_stop, "clicked", G_CALLBACK(on_stop_clicked), controls); - gtk_box_pack_start(GTK_BOX(controls->hbox), controls->btn_stop, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(left_box), controls->btn_stop, FALSE, FALSE, 0); /* Next button */ controls->btn_next = gtk_button_new(); gtk_button_set_image(GTK_BUTTON(controls->btn_next), gtk_image_new_from_stock(GTK_STOCK_MEDIA_NEXT, GTK_ICON_SIZE_BUTTON)); + gtk_widget_set_size_request(controls->btn_next, 36, 36); gtk_widget_set_tooltip_text(controls->btn_next, "Next"); g_signal_connect(controls->btn_next, "clicked", G_CALLBACK(on_next_clicked), controls); - gtk_box_pack_start(GTK_BOX(controls->hbox), controls->btn_next, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(left_box), controls->btn_next, FALSE, FALSE, 0); - /* Separator */ + /* Separator (inside the stretchable area) */ gtk_box_pack_start(GTK_BOX(controls->hbox), gtk_vseparator_new(), FALSE, FALSE, 5); /* Seek slider */ @@ -134,44 +155,59 @@ Controls* controls_new(Player *player, Preferences *prefs) g_signal_connect(controls->seek_scale, "change-value", G_CALLBACK(on_seek_change_value), controls); g_signal_connect(controls->seek_scale, "button-press-event", G_CALLBACK(on_seek_button_press), controls); g_signal_connect(controls->seek_scale, "button-release-event", G_CALLBACK(on_seek_button_release), controls); + g_signal_connect(controls->seek_scale, "motion-notify-event", G_CALLBACK(on_seek_motion_notify), controls); gtk_box_pack_start(GTK_BOX(controls->hbox), controls->seek_scale, TRUE, TRUE, 0); + /* Right side buttons container, wrapped in alignment to prevent vertical stretch */ + GtkWidget *right_align = gtk_alignment_new(0.5, 0.5, 0.0, 0.0); + GtkWidget *right_box = gtk_hbox_new(FALSE, 5); + gtk_container_add(GTK_CONTAINER(right_align), right_box); + gtk_box_pack_start(GTK_BOX(controls->hbox), right_align, FALSE, FALSE, 0); + /* Time label */ controls->time_label = gtk_label_new("0:00 / 0:00"); gtk_misc_set_alignment(GTK_MISC(controls->time_label), 0.5, 0.5); - gtk_widget_set_size_request(controls->time_label, 85, -1); - gtk_box_pack_start(GTK_BOX(controls->hbox), controls->time_label, FALSE, FALSE, 2); + /* gtk_widget_set_size_request(controls->time_label, 85, -1); */ + gtk_box_pack_start(GTK_BOX(right_box), controls->time_label, FALSE, FALSE, 2); /* Separator */ - gtk_box_pack_start(GTK_BOX(controls->hbox), gtk_vseparator_new(), FALSE, FALSE, 2); + gtk_box_pack_start(GTK_BOX(right_box), gtk_vseparator_new(), FALSE, FALSE, 2); /* Volume button (mute toggle) */ controls->btn_mute = gtk_button_new(); GtkWidget *mute_img = gtk_image_new_from_icon_name("audio-volume-high", GTK_ICON_SIZE_BUTTON); gtk_button_set_image(GTK_BUTTON(controls->btn_mute), mute_img); + gtk_widget_set_size_request(controls->btn_mute, 36, 36); gtk_widget_set_tooltip_text(controls->btn_mute, "Mute/Unmute"); g_signal_connect(controls->btn_mute, "clicked", G_CALLBACK(on_mute_clicked), controls); - gtk_box_pack_start(GTK_BOX(controls->hbox), controls->btn_mute, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(right_box), controls->btn_mute, FALSE, FALSE, 0); /* Volume slider */ - controls->volume_scale = gtk_hscale_new_with_range(0, 100, 1); + double max_vol = (prefs && prefs->enable_volume_boost) ? 150.0 : 100.0; + controls->volume_scale = gtk_hscale_new_with_range(0, max_vol, 1); gtk_scale_set_draw_value(GTK_SCALE(controls->volume_scale), FALSE); +#if GTK_CHECK_VERSION(2,16,0) + if (prefs && prefs->enable_volume_boost) { + gtk_scale_add_mark(GTK_SCALE(controls->volume_scale), 100, GTK_POS_BOTTOM, NULL); + } +#endif gtk_range_set_value(GTK_RANGE(controls->volume_scale), 100); gtk_widget_set_size_request(controls->volume_scale, 80, -1); gtk_widget_set_tooltip_text(controls->volume_scale, "Volume"); g_signal_connect(controls->volume_scale, "value-changed", G_CALLBACK(on_volume_changed), controls); - gtk_box_pack_start(GTK_BOX(controls->hbox), controls->volume_scale, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(right_box), controls->volume_scale, FALSE, FALSE, 0); /* Separator */ - gtk_box_pack_start(GTK_BOX(controls->hbox), gtk_vseparator_new(), FALSE, FALSE, 5); + gtk_box_pack_start(GTK_BOX(right_box), gtk_vseparator_new(), FALSE, FALSE, 5); /* Fullscreen button */ controls->btn_fullscreen = gtk_button_new(); gtk_button_set_image(GTK_BUTTON(controls->btn_fullscreen), gtk_image_new_from_stock(GTK_STOCK_FULLSCREEN, GTK_ICON_SIZE_BUTTON)); + gtk_widget_set_size_request(controls->btn_fullscreen, 36, 36); gtk_widget_set_tooltip_text(controls->btn_fullscreen, "Fullscreen"); g_signal_connect(controls->btn_fullscreen, "clicked", G_CALLBACK(on_fullscreen_clicked), controls); - gtk_box_pack_start(GTK_BOX(controls->hbox), controls->btn_fullscreen, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(right_box), controls->btn_fullscreen, FALSE, FALSE, 0); gtk_widget_show_all(controls->root); @@ -184,7 +220,7 @@ void controls_destroy(Controls *controls) if (controls->play_image) g_object_unref(controls->play_image); if (controls->pause_image) g_object_unref(controls->pause_image); - + if (controls->chapter_times) g_free(controls->chapter_times); g_free(controls); } @@ -219,6 +255,11 @@ void controls_update_duration(Controls *controls, double duration) /* Update slider range (still use 0-100 percent) */ gtk_range_set_range(GTK_RANGE(controls->seek_scale), 0, 100); + + /* Refresh markers if we have them */ + if (controls->chapter_count > 0 && controls->chapter_times) { + controls_set_chapters(controls, controls->chapter_count, controls->chapter_times); + } } void controls_update_pause_state(Controls *controls, gboolean paused) @@ -259,15 +300,63 @@ void controls_update_title(Controls *controls, const char *title) void controls_reset(Controls *controls) { if (!controls) return; - - controls->duration = 0.0; + controls->duration = 0; gtk_range_set_value(GTK_RANGE(controls->seek_scale), 0); gtk_label_set_text(GTK_LABEL(controls->time_label), "0:00 / 0:00"); + if (controls->chapter_times) { + g_free(controls->chapter_times); + controls->chapter_times = NULL; + } + controls->chapter_count = 0; + + /* Clear markers */ +#if GTK_CHECK_VERSION(2,16,0) + gtk_scale_clear_marks(GTK_SCALE(controls->seek_scale)); +#endif + /* Show play icon */ gtk_button_set_image(GTK_BUTTON(controls->btn_play_pause), controls->play_image); } +void controls_set_chapters(Controls *controls, int count, double *times) +{ + if (!controls) return; + + if (controls->chapter_times) { + g_free(controls->chapter_times); + controls->chapter_times = NULL; + } + + /* If the preference is visually off, we disable counting and marks entirely */ + if (controls->prefs && !controls->prefs->enable_chapters) { + controls->chapter_count = 0; +#if GTK_CHECK_VERSION(2,16,0) + gtk_scale_clear_marks(GTK_SCALE(controls->seek_scale)); +#endif + return; + } + + controls->chapter_count = count; + if (count > 0 && times) { + controls->chapter_times = g_new0(double, count); + memcpy(controls->chapter_times, times, sizeof(double) * count); + } else { + controls->chapter_times = NULL; + } + + /* Add markers to scale if supported */ +#if GTK_CHECK_VERSION(2,16,0) + gtk_scale_clear_marks(GTK_SCALE(controls->seek_scale)); + if (controls->duration > 0 && count > 0 && times) { + for (int i = 0; i < count; i++) { + double val = (times[i] / controls->duration) * 100.0; + gtk_scale_add_mark(GTK_SCALE(controls->seek_scale), val, GTK_POS_BOTTOM, NULL); + } + } +#endif +} + void controls_set_seeking(Controls *controls, gboolean seeking) { if (!controls) return; @@ -370,6 +459,7 @@ static gboolean on_seek_change_value(GtkRange *range, GtkScrollType scroll, gdou static gboolean on_seek_button_press(GtkWidget *widget, GdkEventButton *event, gpointer data) { Controls *controls = (Controls *)data; + gtk_widget_grab_focus(widget); controls->seeking = TRUE; if (event->button == 1) { @@ -378,12 +468,19 @@ static gboolean on_seek_button_press(GtkWidget *widget, GdkEventButton *event, g GtkRange *range = GTK_RANGE(widget); GtkAdjustment *adj = gtk_range_get_adjustment(range); - /* Calculate relative position (0.0 to 1.0) based on click x coordinate */ - double relative = event->x / (double)widget->allocation.width; + /* Calculate relative position (0.0 to 1.0) using widget-relative coordinates */ + gint x; + gtk_widget_get_pointer(widget, &x, NULL); + + double relative = x / (double)widget->allocation.width; if (relative < 0) relative = 0; if (relative > 1) relative = 1; double new_val = adj->lower + relative * (adj->upper - adj->lower); + + /* Snap to chapter if close */ + new_val = snap_to_chapter(controls, new_val); + gtk_range_set_value(range, new_val); /* Perform seek immediately */ @@ -391,8 +488,10 @@ static gboolean on_seek_button_press(GtkWidget *widget, GdkEventButton *event, g double position = (new_val / 100.0) * controls->duration; player_seek_absolute(controls->player, position); } + + /* Signal handled to prevent GTK from doing its own thing (like jumping by page) */ + return TRUE; } - /* Return FALSE to allow GTK to process the rest of the event (starting the drag/step) */ } return FALSE; @@ -407,9 +506,89 @@ static gboolean on_seek_button_release(GtkWidget *widget, GdkEventButton *event, return FALSE; } +static double snap_to_chapter(Controls *controls, double val) +{ + /* Get seekbar width to calculate pixel-based threshold */ + int width = controls->seek_scale->allocation.width; + if (width <= 0) return val; + + double snap_pixels = 3.0; + double snap_percent = (snap_pixels / (double)width) * 100.0; + + for (int i = 0; i < controls->chapter_count; i++) { + if (controls->duration <= 0) continue; + double chapter_percent = (controls->chapter_times[i] / controls->duration) * 100.0; + if (fabs(val - chapter_percent) < snap_percent) { + return chapter_percent; + } + } + + return val; +} + +static gboolean on_seek_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer data) +{ + Controls *controls = (Controls *)data; + + if (controls->seeking && (event->state & GDK_BUTTON1_MASK)) { + GtkRange *range = GTK_RANGE(widget); + GtkAdjustment *adj = gtk_range_get_adjustment(range); + + gint x; + gtk_widget_get_pointer(widget, &x, NULL); + + double relative = x / (double)widget->allocation.width; + if (relative < 0) relative = 0; + if (relative > 1) relative = 1; + + double new_val = adj->lower + relative * (adj->upper - adj->lower); + + /* Snap to chapter if close */ + new_val = snap_to_chapter(controls, new_val); + + gtk_range_set_value(range, new_val); + + if (controls->duration > 0) { + double position = (new_val / 100.0) * controls->duration; + player_seek_absolute(controls->player, position); + } + return TRUE; + } + + return FALSE; +} + static void on_volume_changed(GtkRange *range, gpointer data) { Controls *controls = (Controls *)data; double volume = gtk_range_get_value(range); + + /* Magnetic snapping to 100% only if boost is on */ + if (controls->prefs && controls->prefs->enable_volume_boost) { + if (fabs(volume - 100.0) < 4.0 && volume != 100.0) { + gtk_range_set_value(range, 100.0); + return; /* Let recursive call handle backend update */ + } + } + player_set_volume(controls->player, volume); } + +void controls_update_volume_boost(Controls *controls, gboolean enable) +{ + if (!controls || !controls->volume_scale) return; + + double current = gtk_range_get_value(GTK_RANGE(controls->volume_scale)); + gtk_range_set_range(GTK_RANGE(controls->volume_scale), 0, enable ? 150.0 : 100.0); + +#if GTK_CHECK_VERSION(2,16,0) + gtk_scale_clear_marks(GTK_SCALE(controls->volume_scale)); + if (enable) { + gtk_scale_add_mark(GTK_SCALE(controls->volume_scale), 100, GTK_POS_BOTTOM, NULL); + } +#endif + + if (!enable && current > 100.0) { + gtk_range_set_value(GTK_RANGE(controls->volume_scale), 100.0); + } +} diff --git a/src/controls.h b/src/controls.h index 970070d..dee0e69 100644 --- a/src/controls.h +++ b/src/controls.h @@ -22,8 +22,10 @@ void controls_update_position(Controls *controls, double position); void controls_update_duration(Controls *controls, double duration); void controls_update_pause_state(Controls *controls, gboolean paused); void controls_update_volume(Controls *controls, double volume); +void controls_update_volume_boost(Controls *controls, gboolean enable); void controls_update_mute_state(Controls *controls, gboolean muted); void controls_update_title(Controls *controls, const char *title); +void controls_set_chapters(Controls *controls, int count, double *times); void controls_reset(Controls *controls); /* Seek slider interaction (to prevent feedback loop) */ diff --git a/src/dialogs.c b/src/dialogs.c index e55b81d..57f3972 100644 --- a/src/dialogs.c +++ b/src/dialogs.c @@ -326,7 +326,7 @@ void dialogs_show_about(GtkWindow *parent) gtk_label_set_markup(GTK_LABEL(title_label), "GTK2 Media Player"); gtk_box_pack_start(GTK_BOX(about_vbox), title_label, FALSE, FALSE, 0); - GtkWidget *version_label = gtk_label_new("Version 0.7.1"); + GtkWidget *version_label = gtk_label_new("Version 9.0"); gtk_box_pack_start(GTK_BOX(about_vbox), version_label, FALSE, FALSE, 0); GtkWidget *comment_label = gtk_label_new("A lightweight GTK2 media player frontend using libmpv."); @@ -672,6 +672,8 @@ Preferences* preferences_new(void) prefs->seekbar_direct_jump = TRUE; /* Default to TRUE as requested earlier */ prefs->hide_focus_rect = FALSE; prefs->enable_osd = TRUE; + prefs->enable_chapters = TRUE; + prefs->enable_volume_boost = FALSE; prefs->last_dir = NULL; prefs->screenshot_directory = g_strdup(g_get_home_dir()); prefs->screenshot_format = g_strdup("png"); @@ -691,6 +693,16 @@ void preferences_free(Preferences *prefs) g_free(prefs); } +static void load_bool_pref(GKeyFile *keyfile, const char *key, gboolean *target) { + GError *error = NULL; + gboolean val = g_key_file_get_boolean(keyfile, "General", key, &error); + if (!error) { + *target = val; + } else { + g_clear_error(&error); + } +} + void preferences_load(Preferences *prefs) { if (!prefs) return; @@ -705,14 +717,19 @@ void preferences_load(Preferences *prefs) if (!error) prefs->default_volume = volume; else { g_clear_error(&error); } - gboolean remember = g_key_file_get_boolean(keyfile, "General", "remember_position", &error); - if (!error) prefs->remember_position = remember; - else { g_clear_error(&error); } - - gboolean playlist = g_key_file_get_boolean(keyfile, "General", "show_playlist_v3", &error); - if (!error) prefs->show_playlist = playlist; - else { g_clear_error(&error); } - + load_bool_pref(keyfile, "remember_position", &prefs->remember_position); + load_bool_pref(keyfile, "show_playlist_v3", &prefs->show_playlist); + load_bool_pref(keyfile, "auto_resize", &prefs->auto_resize); + load_bool_pref(keyfile, "hide_cursor_windowed", &prefs->hide_cursor_windowed); + load_bool_pref(keyfile, "remember_last_dir", &prefs->remember_last_dir); + load_bool_pref(keyfile, "use_mpris", &prefs->use_mpris); + load_bool_pref(keyfile, "open_in_new_window", &prefs->open_in_new_window); + load_bool_pref(keyfile, "seekbar_direct_jump", &prefs->seekbar_direct_jump); + load_bool_pref(keyfile, "hide_focus_rect", &prefs->hide_focus_rect); + load_bool_pref(keyfile, "enable_osd", &prefs->enable_osd); + load_bool_pref(keyfile, "enable_chapters", &prefs->enable_chapters); + load_bool_pref(keyfile, "enable_volume_boost", &prefs->enable_volume_boost); + char *screenshot_dir = g_key_file_get_string(keyfile, "General", "screenshot_directory", &error); if (!error && screenshot_dir) { g_free(prefs->screenshot_directory); @@ -721,26 +738,6 @@ void preferences_load(Preferences *prefs) g_clear_error(&error); g_free(screenshot_dir); } - - gboolean auto_resize = g_key_file_get_boolean(keyfile, "General", "auto_resize", &error); - if (!error) prefs->auto_resize = auto_resize; - else { g_clear_error(&error); } - - gboolean hide_cursor = g_key_file_get_boolean(keyfile, "General", "hide_cursor_windowed", &error); - if (!error) prefs->hide_cursor_windowed = hide_cursor; - else { g_clear_error(&error); } - - gboolean remember_dir = g_key_file_get_boolean(keyfile, "General", "remember_last_dir", &error); - if (!error) prefs->remember_last_dir = remember_dir; - else { g_clear_error(&error); } - - gboolean use_mpris = g_key_file_get_boolean(keyfile, "General", "use_mpris", &error); - if (!error) prefs->use_mpris = use_mpris; - else { g_clear_error(&error); } - - gboolean open_new = g_key_file_get_boolean(keyfile, "General", "open_in_new_window", &error); - if (!error) prefs->open_in_new_window = open_new; - else { g_clear_error(&error); } char *last_dir = g_key_file_get_string(keyfile, "General", "last_dir", &error); if (!error && last_dir) { @@ -751,18 +748,6 @@ void preferences_load(Preferences *prefs) g_free(last_dir); } - gboolean direct_jump = g_key_file_get_boolean(keyfile, "General", "seekbar_direct_jump", &error); - if (!error) prefs->seekbar_direct_jump = direct_jump; - else { g_clear_error(&error); } - - gboolean hide_focus = g_key_file_get_boolean(keyfile, "General", "hide_focus_rect", &error); - if (!error) prefs->hide_focus_rect = hide_focus; - else { g_clear_error(&error); } - - gboolean osd = g_key_file_get_boolean(keyfile, "General", "enable_osd", &error); - if (!error) prefs->enable_osd = osd; - else { g_clear_error(&error); } - char *ss_format = g_key_file_get_string(keyfile, "General", "screenshot_format", &error); if (!error && ss_format) { g_free(prefs->screenshot_format); @@ -799,13 +784,27 @@ void preferences_save(Preferences *prefs) GKeyFile *keyfile = g_key_file_new(); g_key_file_set_double(keyfile, "General", "volume", prefs->default_volume); - g_key_file_set_boolean(keyfile, "General", "remember_position", prefs->remember_position); - g_key_file_set_boolean(keyfile, "General", "show_playlist_v3", prefs->show_playlist); - g_key_file_set_boolean(keyfile, "General", "auto_resize", prefs->auto_resize); - g_key_file_set_boolean(keyfile, "General", "hide_cursor_windowed", prefs->hide_cursor_windowed); - g_key_file_set_boolean(keyfile, "General", "remember_last_dir", prefs->remember_last_dir); - g_key_file_set_boolean(keyfile, "General", "use_mpris", prefs->use_mpris); - g_key_file_set_boolean(keyfile, "General", "open_in_new_window", prefs->open_in_new_window); + + struct { const char *key; gboolean val; } bool_prefs[] = { + {"remember_position", prefs->remember_position}, + {"show_playlist_v3", prefs->show_playlist}, + {"auto_resize", prefs->auto_resize}, + {"hide_cursor_windowed", prefs->hide_cursor_windowed}, + {"remember_last_dir", prefs->remember_last_dir}, + {"use_mpris", prefs->use_mpris}, + {"open_in_new_window", prefs->open_in_new_window}, + {"seekbar_direct_jump", prefs->seekbar_direct_jump}, + {"hide_focus_rect", prefs->hide_focus_rect}, + {"enable_osd", prefs->enable_osd}, + {"enable_chapters", prefs->enable_chapters}, + {"enable_volume_boost", prefs->enable_volume_boost}, + {NULL, FALSE} + }; + + for (int i = 0; bool_prefs[i].key != NULL; i++) { + g_key_file_set_boolean(keyfile, "General", bool_prefs[i].key, bool_prefs[i].val); + } + if (prefs->last_dir) { g_key_file_set_string(keyfile, "General", "last_dir", prefs->last_dir); } @@ -819,9 +818,6 @@ void preferences_save(Preferences *prefs) if (prefs->mpv_config_path) { g_key_file_set_string(keyfile, "General", "mpv_config_path", prefs->mpv_config_path); } - g_key_file_set_boolean(keyfile, "General", "seekbar_direct_jump", prefs->seekbar_direct_jump); - g_key_file_set_boolean(keyfile, "General", "hide_focus_rect", prefs->hide_focus_rect); - g_key_file_set_boolean(keyfile, "General", "enable_osd", prefs->enable_osd); gsize length; char *data = g_key_file_to_data(keyfile, &length, NULL); @@ -869,6 +865,13 @@ static void on_speed_preset_clicked(GtkButton *button, gpointer data) gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), speed_int / 100.0); } +static GtkWidget* create_pref_check(GtkWidget *box, const char *label, gboolean is_active) { + GtkWidget *check = gtk_check_button_new_with_label(label); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), is_active); + gtk_box_pack_start(GTK_BOX(box), check, FALSE, FALSE, 0); + return check; +} + gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginManager *pm) { if (!prefs) return FALSE; @@ -893,33 +896,14 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginM GtkWidget *interface_vbox = gtk_vbox_new(FALSE, 10); gtk_container_set_border_width(GTK_CONTAINER(interface_vbox), 10); - GtkWidget *playlist_check = gtk_check_button_new_with_label("Show playlist by default"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_check), prefs->show_playlist); - gtk_box_pack_start(GTK_BOX(interface_vbox), playlist_check, FALSE, FALSE, 0); - - GtkWidget *resize_check = gtk_check_button_new_with_label("Automatically resize window to fit video"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(resize_check), prefs->auto_resize); - gtk_box_pack_start(GTK_BOX(interface_vbox), resize_check, FALSE, FALSE, 0); - - GtkWidget *cursor_check = gtk_check_button_new_with_label("Automatically hide mouse cursor in windowed mode"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cursor_check), prefs->hide_cursor_windowed); - gtk_box_pack_start(GTK_BOX(interface_vbox), cursor_check, FALSE, FALSE, 0); - - GtkWidget *open_new_check = gtk_check_button_new_with_label("Always open in a new window (Single-Instance off)"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(open_new_check), prefs->open_in_new_window); - gtk_box_pack_start(GTK_BOX(interface_vbox), open_new_check, FALSE, FALSE, 0); - - GtkWidget *jump_check = gtk_check_button_new_with_label("Seek bar: Jump to clicked position immediately"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(jump_check), prefs->seekbar_direct_jump); - gtk_box_pack_start(GTK_BOX(interface_vbox), jump_check, FALSE, FALSE, 0); - - GtkWidget *focus_check = gtk_check_button_new_with_label("Hide focus rectangles (dotted lines) around UI elements"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(focus_check), prefs->hide_focus_rect); - gtk_box_pack_start(GTK_BOX(interface_vbox), focus_check, FALSE, FALSE, 0); - - GtkWidget *osd_check = gtk_check_button_new_with_label("Enable On-Screen Display (OSD)"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(osd_check), prefs->enable_osd); - gtk_box_pack_start(GTK_BOX(interface_vbox), osd_check, FALSE, FALSE, 0); + GtkWidget *playlist_check = create_pref_check(interface_vbox, "Show playlist by default", prefs->show_playlist); + GtkWidget *resize_check = create_pref_check(interface_vbox, "Automatically resize window to fit video", prefs->auto_resize); + GtkWidget *cursor_check = create_pref_check(interface_vbox, "Automatically hide mouse cursor in windowed mode", prefs->hide_cursor_windowed); + GtkWidget *open_new_check = create_pref_check(interface_vbox, "Always open in a new window (Single-Instance off)", prefs->open_in_new_window); + GtkWidget *jump_check = create_pref_check(interface_vbox, "Seek bar: Jump to clicked position immediately", prefs->seekbar_direct_jump); + GtkWidget *focus_check = create_pref_check(interface_vbox, "Hide focus rectangles (dotted lines) around UI elements", prefs->hide_focus_rect); + GtkWidget *osd_check = create_pref_check(interface_vbox, "Enable On-Screen Display (OSD)", prefs->enable_osd); + GtkWidget *chapters_check = create_pref_check(interface_vbox, "Enable Chapters (markers, snapping, and menu)", prefs->enable_chapters); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), interface_vbox, gtk_label_new("Interface")); @@ -962,17 +946,10 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginM gtk_box_pack_start(GTK_BOX(vol_hbox), vol_spin, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(misc_vbox), vol_hbox, FALSE, FALSE, 0); - GtkWidget *position_check = gtk_check_button_new_with_label("Remember playback position"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(position_check), prefs->remember_position); - gtk_box_pack_start(GTK_BOX(misc_vbox), position_check, FALSE, FALSE, 0); - - GtkWidget *dir_check = gtk_check_button_new_with_label("Remember last folder in file chooser"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dir_check), prefs->remember_last_dir); - gtk_box_pack_start(GTK_BOX(misc_vbox), dir_check, FALSE, FALSE, 0); - - GtkWidget *mpris_check = gtk_check_button_new_with_label("Enable MPRIS support (D-Bus)"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(mpris_check), prefs->use_mpris); - gtk_box_pack_start(GTK_BOX(misc_vbox), mpris_check, FALSE, FALSE, 0); + GtkWidget *vol_boost_check = create_pref_check(misc_vbox, "Enable volume boost (up to 150%)", prefs->enable_volume_boost); + GtkWidget *position_check = create_pref_check(misc_vbox, "Remember playback position", prefs->remember_position); + GtkWidget *dir_check = create_pref_check(misc_vbox, "Remember last folder in file chooser", prefs->remember_last_dir); + GtkWidget *mpris_check = create_pref_check(misc_vbox, "Enable MPRIS support (D-Bus)", prefs->use_mpris); /* Screenshot directory */ GtkWidget *ss_hbox = gtk_hbox_new(FALSE, 10); @@ -1018,6 +995,8 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginM prefs->seekbar_direct_jump = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(jump_check)); prefs->hide_focus_rect = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(focus_check)); prefs->enable_osd = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(osd_check)); + prefs->enable_chapters = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(chapters_check)); + prefs->enable_volume_boost = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol_boost_check)); g_free(prefs->screenshot_directory); prefs->screenshot_directory = g_strdup(gtk_entry_get_text(GTK_ENTRY(ss_entry))); diff --git a/src/dialogs.h b/src/dialogs.h index d741299..7b03092 100644 --- a/src/dialogs.h +++ b/src/dialogs.h @@ -21,6 +21,8 @@ typedef struct { gboolean seekbar_direct_jump; gboolean hide_focus_rect; gboolean enable_osd; + gboolean enable_chapters; + gboolean enable_volume_boost; char *last_dir; char *screenshot_directory; char *screenshot_format; diff --git a/src/main.c b/src/main.c index d461424..3997b8f 100644 --- a/src/main.c +++ b/src/main.c @@ -99,7 +99,7 @@ int main(int argc, char *argv[]) return 0; } if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) { - printf("Media Player v0.7.1\n"); + printf("Media Player v9.0\n"); return 0; } } diff --git a/src/mpv_loader.c b/src/mpv_loader.c index 256ea60..a36b6bc 100644 --- a/src/mpv_loader.c +++ b/src/mpv_loader.c @@ -13,6 +13,7 @@ p_mpv_set_wakeup_callback mpv_set_wakeup_callback = NULL; p_mpv_command_async mpv_command_async = NULL; p_mpv_command mpv_command = NULL; p_mpv_set_property_async mpv_set_property_async = NULL; +p_mpv_set_property mpv_set_property = NULL; p_mpv_get_property mpv_get_property = NULL; p_mpv_free mpv_free = NULL; p_mpv_set_property_string mpv_set_property_string = NULL; @@ -65,6 +66,7 @@ int load_mpv_library(void) LOAD_SYM(command_async); LOAD_SYM(command); LOAD_SYM(set_property_async); + LOAD_SYM(set_property); LOAD_SYM(get_property); LOAD_SYM(free); LOAD_SYM(set_property_string); diff --git a/src/mpv_loader.h b/src/mpv_loader.h index 454bca1..5f86eb2 100644 --- a/src/mpv_loader.h +++ b/src/mpv_loader.h @@ -75,6 +75,7 @@ typedef int (*p_mpv_set_option_string)(mpv_handle *ctx, const char *name, const typedef void (*p_mpv_set_wakeup_callback)(mpv_handle *ctx, void (*cb)(void *d), void *d); typedef int (*p_mpv_command_async)(mpv_handle *ctx, uint64_t reply_userdata, const char **args); typedef int (*p_mpv_set_property_async)(mpv_handle *ctx, uint64_t reply_userdata, const char *name, mpv_format format, void *data); +typedef int (*p_mpv_set_property)(mpv_handle *ctx, const char *name, mpv_format format, void *data); typedef int (*p_mpv_get_property)(mpv_handle *ctx, const char *name, mpv_format format, void *data); typedef void (*p_mpv_free)(void *data); typedef int (*p_mpv_set_property_string)(mpv_handle *ctx, const char *name, const char *data); @@ -94,6 +95,7 @@ extern p_mpv_set_wakeup_callback mpv_set_wakeup_callback; extern p_mpv_command_async mpv_command_async; extern p_mpv_command mpv_command; extern p_mpv_set_property_async mpv_set_property_async; +extern p_mpv_set_property mpv_set_property; extern p_mpv_get_property mpv_get_property; extern p_mpv_free mpv_free; extern p_mpv_set_property_string mpv_set_property_string; diff --git a/src/player.c b/src/player.c index 48056a5..94a101d 100644 --- a/src/player.c +++ b/src/player.c @@ -62,6 +62,12 @@ void player_set_window(Player *player, unsigned long wid) player->wid = wid; } +void player_set_volume_boost(Player *player, gboolean enable) +{ + if (!player || !player->mpv) return; + mpv_set_option_string(player->mpv, "volume-max", enable ? "150" : "100"); +} + void player_set_config_path(Player *player, const char *path) { if (!player) return; @@ -227,7 +233,7 @@ void player_seek(Player *player, double seconds) char sec_str[32]; snprintf(sec_str, sizeof(sec_str), "%f", seconds); - const char *cmd[] = {"seek", sec_str, "relative", NULL}; + const char *cmd[] = {"seek", sec_str, "relative+exact", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); /* Show progress OSD */ @@ -725,6 +731,95 @@ void player_free_track_list(char **list, int count) g_free(list); } +/* Chapters */ + +int player_get_chapter_count(Player *player) +{ + if (!player || !player->mpv) return 0; + + int64_t count = 0; + mpv_get_property(player->mpv, "chapters", MPV_FORMAT_INT64, &count); + return (int)count; +} + +int player_get_current_chapter(Player *player) +{ + if (!player || !player->mpv) return -1; + + int64_t chapter = -1; + mpv_get_property(player->mpv, "chapter", MPV_FORMAT_INT64, &chapter); + return (int)chapter; +} + +void player_set_chapter(Player *player, int index) +{ + if (!player || !player->mpv) return; + + int64_t idx = index; + check_error(mpv_set_property(player->mpv, "chapter", MPV_FORMAT_INT64, &idx)); +} + +char** player_get_chapter_list(Player *player, int *count) +{ + if (!player || !player->mpv || !count) return NULL; + + int total = player_get_chapter_count(player); + *count = total; + if (total <= 0) return NULL; + + char **list = g_new0(char*, total); + for (int i = 0; i < total; i++) { + char prop[64]; + snprintf(prop, sizeof(prop), "chapter-list/%d/title", i); + char *title = NULL; + mpv_get_property(player->mpv, prop, MPV_FORMAT_STRING, &title); + + if (title && strlen(title) > 0) { + list[i] = g_strdup_printf("%d: %s", i + 1, title); + } else { + list[i] = g_strdup_printf("Chapter %d", i + 1); + } + + if (title) mpv_free(title); + } + + return list; +} + +double* player_get_chapter_times(Player *player, int *count) +{ + if (!player || !player->mpv || !count) return NULL; + + int total = player_get_chapter_count(player); + *count = total; + if (total <= 0) return NULL; + + double *times = g_new0(double, total); + for (int i = 0; i < total; i++) { + char prop[64]; + snprintf(prop, sizeof(prop), "chapter-list/%d/time", i); + double time = 0; + mpv_get_property(player->mpv, prop, MPV_FORMAT_DOUBLE, &time); + times[i] = time; + } + + return times; +} + +void player_next_chapter(Player *player) +{ + if (!player || !player->mpv) return; + const char *cmd[] = {"add", "chapter", "1", NULL}; + check_error(mpv_command_async(player->mpv, 0, cmd)); +} + +void player_prev_chapter(Player *player) +{ + if (!player || !player->mpv) return; + const char *cmd[] = {"add", "chapter", "-1", NULL}; + check_error(mpv_command_async(player->mpv, 0, cmd)); +} + /* Playlist */ int player_get_playlist_count(Player *player) diff --git a/src/player.h b/src/player.h index 95b592b..e21329e 100644 --- a/src/player.h +++ b/src/player.h @@ -47,6 +47,7 @@ gboolean player_get_shuffle(Player *player); gboolean player_get_loop(Player *player); /* Property setters */ +void player_set_volume_boost(Player *player, gboolean enable); void player_set_volume(Player *player, double volume); void player_set_muted(Player *player, gboolean muted); void player_set_speed(Player *player, double speed); @@ -66,6 +67,15 @@ void player_get_video_resolution(Player *player, int *w, int *h); char** player_get_subtitle_track_list(Player *player, int *count); void player_free_track_list(char **list, int count); +/* Chapters */ +int player_get_chapter_count(Player *player); +int player_get_current_chapter(Player *player); +void player_set_chapter(Player *player, int index); +char** player_get_chapter_list(Player *player, int *count); +double* player_get_chapter_times(Player *player, int *count); +void player_next_chapter(Player *player); +void player_prev_chapter(Player *player); + /* Playlist (internal mpv playlist) */ int player_get_playlist_count(Player *player); int player_get_playlist_pos(Player *player); diff --git a/src/ui.c b/src/ui.c index c3a2324..824bd69 100644 --- a/src/ui.c +++ b/src/ui.c @@ -23,6 +23,8 @@ static void on_audio_track_selected(GtkMenuItem *item, gpointer data); static void on_subtitle_track_selected(GtkMenuItem *item, gpointer data); static void on_subtitle_load(GtkMenuItem *item, gpointer data); static void on_subtitle_disable(GtkMenuItem *item, gpointer data); +static void update_chapter_menu(AppUI *ui); +static void on_chapter_selected(GtkMenuItem *item, gpointer data); static void on_view_fullscreen(GtkMenuItem *item, gpointer data); static void on_view_playlist(GtkMenuItem *item, gpointer data); static void on_view_screenshot(GtkMenuItem *item, gpointer data); @@ -42,6 +44,7 @@ enum { PROP_IDLE_ACTIVE, PROP_SHUFFLE, PROP_LOOP, + PROP_CHAPTER, }; struct AppUI { @@ -86,7 +89,9 @@ struct AppUI { GtkWidget *subtitle_menu; GtkWidget *speed_menu; GtkWidget *aspect_menu; + GtkWidget *chapter_menu; GtkWidget *recent_menu; + GtkWidget *mute_item; GtkRecentManager *recent_manager; @@ -144,6 +149,7 @@ static void on_view_always_on_top(GtkMenuItem *item, gpointer data); static void on_view_screenshot(GtkMenuItem *item, gpointer data); static void on_view_preferences(GtkMenuItem *item, gpointer data); static void on_aspect_ratio_selected(GtkMenuItem *item, gpointer data); +static void on_audio_mute(GtkMenuItem *item, gpointer data); static void on_help_about(GtkMenuItem *item, gpointer data); /* Plugin API wrapper forward declarations */ @@ -295,6 +301,9 @@ AppUI* ui_new(void) gtk_rc_parse_string("style \"no-focus-rect\" { GtkWidget::focus-line-width = 0 GtkWidget::focus-padding = 0 } widget \"*\" style \"no-focus-rect\""); } + /* Observe chapter changes */ + player_observe_property(ui->player, "chapter", MPV_FORMAT_INT64, PROP_CHAPTER); + ui->is_fullscreen = FALSE; ui->always_on_top = FALSE; ui->auto_hide_timer_id = 0; @@ -355,6 +364,7 @@ void ui_set_preferences(AppUI *ui, Preferences *prefs) /* Apply some immediate settings */ if (ui->player) { + player_set_volume_boost(ui->player, ui->prefs->enable_volume_boost); player_set_volume(ui->player, ui->prefs->default_volume); if (ui->prefs->screenshot_directory) { @@ -362,6 +372,10 @@ void ui_set_preferences(AppUI *ui, Preferences *prefs) } } + if (ui->controls) { + controls_update_volume_boost(ui->controls, ui->prefs->enable_volume_boost); + } + /* Update playlist visibility if UI is already realized */ if (ui->playlist) { GtkWidget *playlist_widget = playlist_get_widget(ui->playlist); @@ -578,13 +592,14 @@ void ui_toggle_fullscreen(AppUI *ui) gtk_widget_size_request(controls_widget, &req); int ctrl_h = req.height; - /* For fullscreen, we should use screen size */ + /* For fullscreen, we should use the specific monitor size */ GdkScreen *screen = gtk_window_get_screen(GTK_WINDOW(ui->window)); - int sw = gdk_screen_get_width(screen); - int sh = gdk_screen_get_height(screen); + gint monitor_num = gdk_screen_get_monitor_at_window(screen, ui->window->window); + GdkRectangle geom; + gdk_screen_get_monitor_geometry(screen, monitor_num, &geom); - gtk_window_set_default_size(GTK_WINDOW(ui->fs_window), sw, ctrl_h); - gtk_window_move(GTK_WINDOW(ui->fs_window), 0, sh - ctrl_h); + gtk_window_set_default_size(GTK_WINDOW(ui->fs_window), geom.width, ctrl_h); + gtk_window_move(GTK_WINDOW(ui->fs_window), geom.x, geom.y + geom.height - ctrl_h); gtk_widget_show_all(ui->fs_window); gtk_window_fullscreen(GTK_WINDOW(ui->window)); @@ -753,6 +768,11 @@ static void on_player_event(Player *player, mpv_event *event, gpointer user_data if (prop->format == MPV_FORMAT_FLAG) { int muted = *(int *)prop->data; controls_update_mute_state(ui->controls, muted); + if (ui->mute_item) { + g_signal_handlers_block_by_func(ui->mute_item, G_CALLBACK(on_audio_mute), ui); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(ui->mute_item), muted); + g_signal_handlers_unblock_by_func(ui->mute_item, G_CALLBACK(on_audio_mute), ui); + } } break; @@ -789,6 +809,11 @@ static void on_player_event(Player *player, mpv_event *event, gpointer user_data case PROP_LOOP: mpris_update_loop_shuffle(ui); break; + + case PROP_CHAPTER: + /* Just refresh the menu to update radio selection */ + update_chapter_menu(ui); + break; } break; } @@ -808,6 +833,7 @@ static void on_player_event(Player *player, mpv_event *event, gpointer user_data /* Update audio/subtitle menus */ void update_track_menus(AppUI *ui); update_track_menus(ui); + update_chapter_menu(ui); mpris_update_metadata(ui); mpris_update_playback_status(ui); @@ -933,6 +959,14 @@ static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer dat on_playback_play_pause(NULL, ui); return TRUE; + case GDK_Page_Up: + player_prev_chapter(ui->player); + return TRUE; + + case GDK_Page_Down: + player_next_chapter(ui->player); + return TRUE; + case GDK_Left: player_seek(ui->player, -5); return TRUE; @@ -1149,7 +1183,7 @@ void update_track_menus(AppUI *ui) char **audio_tracks = player_get_audio_track_list(ui->player, &audio_count); if (audio_count == 0) { - GtkWidget *item = gtk_menu_item_new_with_label("(No audio tracks)"); + GtkWidget *item = gtk_menu_item_new_with_label("None available"); gtk_widget_set_sensitive(item, FALSE); gtk_menu_shell_append(GTK_MENU_SHELL(ui->audio_menu), item); } else { @@ -1164,25 +1198,21 @@ void update_track_menus(AppUI *ui) gtk_widget_show_all(ui->audio_menu); /* Update Subtitle Menu */ - /* We keep the first few items: Disable, Separator, Load External */ - /* This clear_menu is a bit too aggressive for subtitle menu if we want to keep Load External */ - /* Let's manually clear only the track items */ - GList *children = gtk_container_get_children(GTK_CONTAINER(ui->subtitle_menu)); - GList *iter; - int count = 0; - for (iter = children; iter != NULL; iter = iter->next) { - if (count > 2) { /* Skip Disable, Separator, Load External */ - gtk_widget_destroy(GTK_WIDGET(iter->data)); - } - count++; - } - g_list_free(children); + clear_menu(ui->subtitle_menu); int sub_count = 0; char **sub_tracks = player_get_subtitle_track_list(ui->player, &sub_count); - if (sub_count > 0) { + if (sub_count == 0) { + GtkWidget *item = gtk_menu_item_new_with_label("None available"); + gtk_widget_set_sensitive(item, FALSE); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), item); + } else { + GtkWidget *sub_disable_item = gtk_menu_item_new_with_mnemonic("_Disable"); + g_signal_connect(sub_disable_item, "activate", G_CALLBACK(on_subtitle_disable), ui); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), sub_disable_item); gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), gtk_separator_menu_item_new()); + for (int i = 0; i < sub_count; i++) { GtkWidget *item = gtk_menu_item_new_with_label(sub_tracks[i]); g_object_set_data(G_OBJECT(item), "track", GINT_TO_POINTER(i + 1)); @@ -1215,6 +1245,13 @@ static void create_menubar(AppUI *ui) g_signal_connect(open_dir_item, "activate", G_CALLBACK(on_file_open_directory), ui); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_dir_item); + GtkWidget *open_url_item = gtk_menu_item_new_with_mnemonic("Open _URL..."); + g_signal_connect(open_url_item, "activate", G_CALLBACK(on_file_open_url), ui); + gtk_widget_add_accelerator(open_url_item, "activate", accel_group, GDK_l, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_url_item); + + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new()); + ui->recent_menu = gtk_recent_chooser_menu_new_for_manager(ui->recent_manager); GtkRecentFilter *filter = gtk_recent_filter_new(); gtk_recent_filter_add_mime_type(filter, "video/*"); @@ -1224,18 +1261,13 @@ static void create_menubar(AppUI *ui) gtk_recent_chooser_set_sort_type(GTK_RECENT_CHOOSER(ui->recent_menu), GTK_RECENT_SORT_MRU); g_signal_connect(ui->recent_menu, "item-activated", G_CALLBACK(on_recent_item_activated), ui); - GtkWidget *recent_item = gtk_menu_item_new_with_mnemonic("Recent _Files"); + GtkWidget *recent_item = gtk_menu_item_new_with_mnemonic("_Recent Files"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(recent_item), ui->recent_menu); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), recent_item); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new()); - GtkWidget *open_url_item = gtk_menu_item_new_with_mnemonic("Open _URL..."); - g_signal_connect(open_url_item, "activate", G_CALLBACK(on_file_open_url), ui); - gtk_widget_add_accelerator(open_url_item, "activate", accel_group, GDK_l, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); - gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_url_item); - - GtkWidget *save_playlist_item = gtk_menu_item_new_with_mnemonic("Save _Playlist..."); + GtkWidget *save_playlist_item = gtk_menu_item_new_with_mnemonic("_Save Playlist..."); g_signal_connect(save_playlist_item, "activate", G_CALLBACK(on_file_save_playlist), ui); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), save_playlist_item); @@ -1244,7 +1276,6 @@ static void create_menubar(AppUI *ui) GtkWidget *quit_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, NULL); g_signal_connect(quit_item, "activate", G_CALLBACK(on_file_quit), ui); gtk_widget_add_accelerator(quit_item, "activate", accel_group, GDK_q, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); - /* Also Q key */ gtk_widget_add_accelerator(quit_item, "activate", accel_group, GDK_q, 0, 0); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit_item); @@ -1270,35 +1301,42 @@ static void create_menubar(AppUI *ui) GtkWidget *prev_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_PREVIOUS, NULL); g_signal_connect(prev_item, "activate", G_CALLBACK(on_playback_prev), ui); - gtk_widget_add_accelerator(prev_item, "activate", accel_group, GDK_Page_Up, 0, GTK_ACCEL_VISIBLE); + gtk_widget_add_accelerator(prev_item, "activate", accel_group, GDK_Page_Up, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), prev_item); - + GtkWidget *next_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_NEXT, NULL); g_signal_connect(next_item, "activate", G_CALLBACK(on_playback_next), ui); - gtk_widget_add_accelerator(next_item, "activate", accel_group, GDK_Page_Down, 0, GTK_ACCEL_VISIBLE); + gtk_widget_add_accelerator(next_item, "activate", accel_group, GDK_Page_Down, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), next_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new()); - GtkWidget *seek_fwd_item = gtk_menu_item_new_with_mnemonic("Seek _Forward 10s"); + /* Seek Submenu */ + GtkWidget *seek_menu = gtk_menu_new(); + GtkWidget *seek_item = gtk_menu_item_new_with_mnemonic("_Seek"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(seek_item), seek_menu); + + GtkWidget *seek_fwd_item = gtk_menu_item_new_with_mnemonic("Forward 10s"); g_signal_connect(seek_fwd_item, "activate", G_CALLBACK(on_playback_seek_forward), ui); gtk_widget_add_accelerator(seek_fwd_item, "activate", accel_group, GDK_Right, 0, GTK_ACCEL_VISIBLE); - gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_fwd_item); + gtk_menu_shell_append(GTK_MENU_SHELL(seek_menu), seek_fwd_item); - GtkWidget *seek_back_item = gtk_menu_item_new_with_mnemonic("Seek _Backward 10s"); + GtkWidget *seek_back_item = gtk_menu_item_new_with_mnemonic("Backward 10s"); g_signal_connect(seek_back_item, "activate", G_CALLBACK(on_playback_seek_backward), ui); gtk_widget_add_accelerator(seek_back_item, "activate", accel_group, GDK_Left, 0, GTK_ACCEL_VISIBLE); - gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_back_item); + gtk_menu_shell_append(GTK_MENU_SHELL(seek_menu), seek_back_item); GtkWidget *frame_step_item = gtk_menu_item_new_with_mnemonic("_Frame Step"); g_signal_connect(frame_step_item, "activate", G_CALLBACK(on_playback_frame_step), ui); gtk_widget_add_accelerator(frame_step_item, "activate", accel_group, GDK_period, 0, GTK_ACCEL_VISIBLE); - gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), frame_step_item); + gtk_menu_shell_append(GTK_MENU_SHELL(seek_menu), frame_step_item); GtkWidget *goto_time_item = gtk_menu_item_new_with_mnemonic("_Go to Time..."); g_signal_connect(goto_time_item, "activate", G_CALLBACK(on_playback_goto_time), ui); gtk_widget_add_accelerator(goto_time_item, "activate", accel_group, GDK_g, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); - gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), goto_time_item); + gtk_menu_shell_append(GTK_MENU_SHELL(seek_menu), goto_time_item); + + gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new()); @@ -1329,32 +1367,91 @@ static void create_menubar(AppUI *ui) gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), playback_item); /* Audio Menu */ - ui->audio_menu = gtk_menu_new(); + GtkWidget *audio_root_menu = gtk_menu_new(); GtkWidget *audio_item = gtk_menu_item_new_with_mnemonic("_Audio"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(audio_item), ui->audio_menu); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(audio_item), audio_root_menu); - GtkWidget *audio_placeholder = gtk_menu_item_new_with_label("(No tracks available)"); + GtkWidget *audio_tracks_item = gtk_menu_item_new_with_mnemonic("_Select Track"); + ui->audio_menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(audio_tracks_item), ui->audio_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(audio_root_menu), audio_tracks_item); + + GtkWidget *audio_placeholder = gtk_menu_item_new_with_label("None available"); gtk_widget_set_sensitive(audio_placeholder, FALSE); gtk_menu_shell_append(GTK_MENU_SHELL(ui->audio_menu), audio_placeholder); + gtk_menu_shell_append(GTK_MENU_SHELL(audio_root_menu), gtk_separator_menu_item_new()); + + ui->mute_item = gtk_check_menu_item_new_with_mnemonic("_Muted"); + g_signal_connect(ui->mute_item, "toggled", G_CALLBACK(on_audio_mute), ui); + gtk_menu_shell_append(GTK_MENU_SHELL(audio_root_menu), ui->mute_item); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), audio_item); - /* Subtitles Menu */ - ui->subtitle_menu = gtk_menu_new(); + /* Video Menu */ + GtkWidget *video_menu = gtk_menu_new(); + GtkWidget *video_item = gtk_menu_item_new_with_mnemonic("_Video"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(video_item), video_menu); + + /* Subtitles Submenu */ + GtkWidget *subtitle_root_menu = gtk_menu_new(); GtkWidget *subtitle_item = gtk_menu_item_new_with_mnemonic("_Subtitles"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(subtitle_item), ui->subtitle_menu); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(subtitle_item), subtitle_root_menu); - GtkWidget *sub_disable_item = gtk_menu_item_new_with_mnemonic("_Disable"); - g_signal_connect(sub_disable_item, "activate", G_CALLBACK(on_subtitle_disable), ui); - gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), sub_disable_item); + GtkWidget *sub_tracks_item = gtk_menu_item_new_with_mnemonic("_Select Track"); + ui->subtitle_menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(sub_tracks_item), ui->subtitle_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(subtitle_root_menu), sub_tracks_item); - gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), gtk_separator_menu_item_new()); + GtkWidget *sub_placeholder = gtk_menu_item_new_with_label("None available"); + gtk_widget_set_sensitive(sub_placeholder, FALSE); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), sub_placeholder); + + gtk_menu_shell_append(GTK_MENU_SHELL(subtitle_root_menu), gtk_separator_menu_item_new()); GtkWidget *sub_load_item = gtk_menu_item_new_with_mnemonic("_Load External..."); g_signal_connect(sub_load_item, "activate", G_CALLBACK(on_subtitle_load), ui); - gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), sub_load_item); + gtk_menu_shell_append(GTK_MENU_SHELL(subtitle_root_menu), sub_load_item); - gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), subtitle_item); + gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), subtitle_item); + + /* Chapters Submenu */ + ui->chapter_menu = gtk_menu_new(); + GtkWidget *chapter_item = gtk_menu_item_new_with_mnemonic("_Chapters"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(chapter_item), ui->chapter_menu); + + GtkWidget *chapter_placeholder = gtk_menu_item_new_with_label("(No chapters)"); + gtk_widget_set_sensitive(chapter_placeholder, FALSE); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->chapter_menu), chapter_placeholder); + + gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), chapter_item); + + gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), gtk_separator_menu_item_new()); + + /* Aspect ratio submenu */ + ui->aspect_menu = gtk_menu_new(); + GtkWidget *aspect_item = gtk_menu_item_new_with_mnemonic("_Aspect Ratio"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(aspect_item), ui->aspect_menu); + + const char *aspects[] = {"Auto", "4:3", "16:9", "16:10", "1.85:1", "2.35:1", NULL}; + const char *aspect_vals[] = {"-1", "4:3", "16:9", "16:10", "1.85:1", "2.35:1", NULL}; + + for (int i = 0; aspects[i] != NULL; i++) { + GtkWidget *ar_item = gtk_menu_item_new_with_label(aspects[i]); + g_object_set_data(G_OBJECT(ar_item), "aspect", (gpointer)aspect_vals[i]); + g_signal_connect(ar_item, "activate", G_CALLBACK(on_aspect_ratio_selected), ui); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->aspect_menu), ar_item); + } + gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), aspect_item); + + gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), gtk_separator_menu_item_new()); + + GtkWidget *screenshot_item = gtk_menu_item_new_with_mnemonic("_Screenshot"); + g_signal_connect(screenshot_item, "activate", G_CALLBACK(on_view_screenshot), ui); + gtk_widget_add_accelerator(screenshot_item, "activate", accel_group, GDK_s, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE); + gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), screenshot_item); + + gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), video_item); /* View Menu */ GtkWidget *view_menu = gtk_menu_new(); @@ -1374,53 +1471,35 @@ static void create_menubar(AppUI *ui) gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), playlist_toggle_item); GtkWidget *on_top_item = gtk_check_menu_item_new_with_mnemonic("Always on _Top"); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(on_top_item), ui->always_on_top); g_signal_connect(on_top_item, "toggled", G_CALLBACK(on_view_always_on_top), ui); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), on_top_item); - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), gtk_separator_menu_item_new()); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), view_item); - /* Aspect ratio submenu */ - ui->aspect_menu = gtk_menu_new(); - GtkWidget *aspect_item = gtk_menu_item_new_with_mnemonic("_Aspect Ratio"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(aspect_item), ui->aspect_menu); + /* Tools Menu */ + GtkWidget *tools_menu = gtk_menu_new(); + GtkWidget *tools_item = gtk_menu_item_new_with_mnemonic("_Tools"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(tools_item), tools_menu); - const char *aspects[] = {"Auto", "4:3", "16:9", "16:10", "1.85:1", "2.35:1", NULL}; - const char *aspect_vals[] = {"-1", "4:3", "16:9", "16:10", "1.85:1", "2.35:1", NULL}; + GtkWidget *plugins_menu = plugin_manager_get_menu(ui->plugin_manager); + GtkWidget *plugins_item = gtk_menu_item_new_with_mnemonic("_Plugins"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(plugins_item), plugins_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(tools_menu), plugins_item); - for (int i = 0; aspects[i] != NULL; i++) { - GtkWidget *ar_item = gtk_menu_item_new_with_label(aspects[i]); - g_object_set_data(G_OBJECT(ar_item), "aspect", (gpointer)aspect_vals[i]); - g_signal_connect(ar_item, "activate", G_CALLBACK(on_aspect_ratio_selected), ui); - gtk_menu_shell_append(GTK_MENU_SHELL(ui->aspect_menu), ar_item); - } - - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), aspect_item); - - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), gtk_separator_menu_item_new()); - - GtkWidget *screenshot_item = gtk_menu_item_new_with_mnemonic("_Screenshot"); - g_signal_connect(screenshot_item, "activate", G_CALLBACK(on_view_screenshot), ui); - gtk_widget_add_accelerator(screenshot_item, "activate", accel_group, GDK_s, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE); - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), screenshot_item); - - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), gtk_separator_menu_item_new()); + gtk_menu_shell_append(GTK_MENU_SHELL(tools_menu), gtk_separator_menu_item_new()); GtkWidget *prefs_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_PREFERENCES, NULL); g_signal_connect(prefs_item, "activate", G_CALLBACK(on_view_preferences), ui); - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), prefs_item); + gtk_menu_shell_append(GTK_MENU_SHELL(tools_menu), prefs_item); - gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), view_item); - - /* Plugins Menu */ - GtkWidget *plugins_menu = plugin_manager_get_menu(ui->plugin_manager); - GtkWidget *plugins_item = gtk_menu_item_new_with_mnemonic("P_lugins"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(plugins_item), plugins_menu); - gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), plugins_item); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), tools_item); /* Help Menu */ GtkWidget *help_menu = gtk_menu_new(); GtkWidget *help_item = gtk_menu_item_new_with_mnemonic("_Help"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(help_item), help_menu); + gtk_menu_item_set_right_justified(GTK_MENU_ITEM(help_item), FALSE); GtkWidget *about_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_ABOUT, NULL); g_signal_connect(about_item, "activate", G_CALLBACK(on_help_about), ui); @@ -1431,6 +1510,12 @@ static void create_menubar(AppUI *ui) /* Menu callbacks */ +static void on_audio_mute(GtkMenuItem *item, gpointer data) +{ + AppUI *ui = (AppUI *)data; + player_set_muted(ui->player, gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))); +} + static void on_file_open(GtkMenuItem *item, gpointer data) { (void)item; @@ -1530,6 +1615,58 @@ static void on_playback_next(GtkMenuItem *item, gpointer data) playlist_play_next(ui->playlist); } +static void on_chapter_selected(GtkMenuItem *item, gpointer data) +{ + AppUI *ui = (AppUI *)data; + int index = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(item), "chapter-index")); + player_set_chapter(ui->player, index); +} + +static void update_chapter_menu(AppUI *ui) +{ + if (!ui || !ui->chapter_menu) return; + if (ui->prefs && !ui->prefs->enable_chapters) return; + + /* Clear old menu */ + GList *children = gtk_container_get_children(GTK_CONTAINER(ui->chapter_menu)); + for (GList *iter = children; iter != NULL; iter = iter->next) { + gtk_widget_destroy(GTK_WIDGET(iter->data)); + } + g_list_free(children); + + int count = 0; + char **chapters = player_get_chapter_list(ui->player, &count); + double *times = player_get_chapter_times(ui->player, &count); + + /* Sync markers to controls */ + controls_set_chapters(ui->controls, count, times); + if (times) g_free(times); + + if (count > 0 && chapters) { + int current = player_get_current_chapter(ui->player); + for (int i = 0; i < count; i++) { + GtkWidget *item = gtk_image_menu_item_new_with_label(chapters[i]); + + if (i == current) { + GtkWidget *img = gtk_image_new_from_stock(GTK_STOCK_GO_FORWARD, GTK_ICON_SIZE_MENU); + gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), img); + gtk_image_menu_item_set_always_show_image(GTK_IMAGE_MENU_ITEM(item), TRUE); + } + + g_object_set_data(G_OBJECT(item), "chapter-index", GINT_TO_POINTER(i)); + g_signal_connect(item, "activate", G_CALLBACK(on_chapter_selected), ui); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->chapter_menu), item); + } + player_free_track_list(chapters, count); + } else { + GtkWidget *placeholder = gtk_menu_item_new_with_label("(No chapters)"); + gtk_widget_set_sensitive(placeholder, FALSE); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->chapter_menu), placeholder); + } + + gtk_widget_show_all(ui->chapter_menu); +} + static void on_playback_seek_forward(GtkMenuItem *item, gpointer data) { (void)item;