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!

This commit is contained in:
laki 2026-03-20 18:11:12 +00:00
parent 0ef995ae7a
commit 2cb52dcf90
12 changed files with 605 additions and 194 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ make_deb_32.sh
make_deb.sh
make_release.sh
gtk2-media-player-v0.7.1-linux.zip
GTK2-template

View File

@ -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 <<EOF > "$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"

View File

@ -2,6 +2,7 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
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);
}
}

View File

@ -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) */

View File

@ -326,7 +326,7 @@ void dialogs_show_about(GtkWindow *parent)
gtk_label_set_markup(GTK_LABEL(title_label), "<span size='xx-large' weight='bold'>GTK2 Media Player</span>");
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)));

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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)

View File

@ -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);

299
src/ui.c
View File

@ -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;