diff --git a/Kino b/Kino new file mode 100755 index 0000000..91258d2 Binary files /dev/null and b/Kino differ diff --git a/Makefile b/Makefile index 55ac4d3..ff400d4 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,8 @@ SRCS = $(SRC_DIR)/main.c \ $(SRC_DIR)/dialogs.c \ $(SRC_DIR)/mpv_loader.c \ $(SRC_DIR)/mpris.c \ - $(SRC_DIR)/plugin_manager.c + $(SRC_DIR)/plugin_manager.c \ + $(SRC_DIR)/keybinds.c OBJS = $(SRCS:.c=.o) TARGET = Kino diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..0e7e57b --- /dev/null +++ b/install.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Simple installation script for Kino + +if [ "$EUID" -ne 0 ]; then + echo "Please run as root (use sudo)" + exit 1 +fi + +INSTALL_DIR="/usr/local/lib/Kino" +BIN_LINK="/usr/bin/kino" +BIN_LINK_LOCAL="/usr/local/bin/kino" +DESKTOP_FILE="/usr/share/applications/kino.desktop" + +echo "Installing to $INSTALL_DIR..." + +# Create directory +mkdir -p "$INSTALL_DIR" + +# Copy binary and metadata using 'install' to handle running processes correctly +install -m 755 Kino "$INSTALL_DIR/" +install -m 644 assets/icon.png "$INSTALL_DIR/" + +# For directory copy, we use cp but remove destination first to ensure clean update +rm -rf "$INSTALL_DIR/lib" +cp -r lib "$INSTALL_DIR/" + +# Create wrapper scripts +cat < "$BIN_LINK" +#!/bin/bash +cd "$INSTALL_DIR" +./Kino "\$@" +EOF + +cp "$BIN_LINK" "$BIN_LINK_LOCAL" +chmod +x "$BIN_LINK" "$BIN_LINK_LOCAL" + +# Install desktop file +cp assets/kino.desktop "$DESKTOP_FILE" +# Update path in desktop file if needed + +# Update MIME database +echo "Updating MIME database..." +update-desktop-database + +echo "Installation complete!" +echo "Installed version timestamp: $(date -r "$INSTALL_DIR/Kino")" +echo "You can now run 'kino' or find it in your application menu." diff --git a/lib/libmpv.so.2 b/lib/libmpv.so.2 new file mode 100644 index 0000000..6dc8ecb Binary files /dev/null and b/lib/libmpv.so.2 differ diff --git a/src/dialogs.c b/src/dialogs.c index 6463303..fce9301 100644 --- a/src/dialogs.c +++ b/src/dialogs.c @@ -1,4 +1,5 @@ #include "dialogs.h" +#include #include #include #include @@ -294,27 +295,238 @@ char* dialogs_save_screenshot(GtkWindow *parent) return result; } -void dialogs_show_keybinds(GtkWindow *parent) +/* ---- Keybind helpers ---- */ + +enum { + KB_COL_ACTION_NAME, + KB_COL_SHORTCUT, + KB_COL_ACTION_ID, + KB_NUM_COLS +}; + +typedef struct { + GtkWidget *dialog; + guint captured_keyval; + GdkModifierType captured_mods; + gboolean got_key; +} KeyGrabData; + +static gboolean on_keygrab_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data) +{ + (void)widget; + KeyGrabData *kgd = (KeyGrabData *)data; + + switch (event->keyval) { + case GDK_Shift_L: case GDK_Shift_R: + case GDK_Control_L: case GDK_Control_R: + case GDK_Alt_L: case GDK_Alt_R: + case GDK_Super_L: case GDK_Super_R: + return TRUE; + } + + GdkModifierType relevant = GDK_CONTROL_MASK | GDK_SHIFT_MASK | GDK_MOD1_MASK; + kgd->captured_keyval = event->keyval; + kgd->captured_mods = event->state & relevant; + kgd->got_key = TRUE; + + gtk_dialog_response(GTK_DIALOG(kgd->dialog), GTK_RESPONSE_OK); + return TRUE; +} + +static void on_keybind_row_activated(GtkTreeView *tree, GtkTreePath *path, + GtkTreeViewColumn *col, gpointer data) +{ + (void)col; + KeybindManager *km = (KeybindManager *)data; + GtkTreeModel *model = gtk_tree_view_get_model(tree); + GtkTreeIter iter; + + if (!gtk_tree_model_get_iter(model, &iter, path)) return; + + int action_id; + char *action_name; + gtk_tree_model_get(model, &iter, KB_COL_ACTION_NAME, &action_name, KB_COL_ACTION_ID, &action_id, -1); + + GtkWidget *parent_w = gtk_widget_get_toplevel(GTK_WIDGET(tree)); + GtkWidget *grab_dialog = gtk_dialog_new_with_buttons( + "Press a key...", + GTK_WINDOW(parent_w), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + NULL); + gtk_window_set_default_size(GTK_WINDOW(grab_dialog), 300, -1); + + GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(grab_dialog)); + char *prompt = g_strdup_printf("Press a new key combination for:\n\n%s", action_name); + GtkWidget *label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(label), prompt); + gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER); + gtk_container_set_border_width(GTK_CONTAINER(label), 20); + gtk_box_pack_start(GTK_BOX(content), label, TRUE, TRUE, 0); + g_free(prompt); + + KeyGrabData kgd = { grab_dialog, 0, 0, FALSE }; + g_signal_connect(grab_dialog, "key-press-event", G_CALLBACK(on_keygrab_key_press), &kgd); + + gtk_widget_show_all(grab_dialog); + int response = gtk_dialog_run(GTK_DIALOG(grab_dialog)); + gtk_widget_destroy(grab_dialog); + + if (response == GTK_RESPONSE_OK && kgd.got_key) { + keybind_manager_set_keybind(km, (KeybindAction)action_id, kgd.captured_keyval, kgd.captured_mods); + + char *display = keybind_get_display_string(kgd.captured_keyval, kgd.captured_mods); + gtk_list_store_set(GTK_LIST_STORE(model), &iter, KB_COL_SHORTCUT, display, -1); + g_free(display); + } + + g_free(action_name); +} + +static void on_keybinds_reset_clicked(GtkButton *button, gpointer data); + +static GtkWidget* create_keybinds_editor_widget(KeybindManager *km) +{ + GtkWidget *vbox = gtk_vbox_new(FALSE, 8); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 12); + + GtkWidget *info_label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(info_label), "Double-click a shortcut to change it."); + gtk_misc_set_alignment(GTK_MISC(info_label), 0.0, 0.5); + gtk_box_pack_start(GTK_BOX(vbox), info_label, FALSE, FALSE, 4); + + GtkListStore *store = gtk_list_store_new(KB_NUM_COLS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT); + + for (int i = 0; i < KEYBIND_COUNT; i++) { + Keybind *kb = keybind_manager_get(km, (KeybindAction)i); + if (!kb) continue; + + char *display = keybind_get_display_string(kb->keyval, kb->mods); + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + KB_COL_ACTION_NAME, kb->label, + KB_COL_SHORTCUT, display, + KB_COL_ACTION_ID, i, -1); + g_free(display); + } + + GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); + g_object_unref(store); + + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes("Action", renderer, "text", KB_COL_ACTION_NAME, NULL); + gtk_tree_view_column_set_expand(column, TRUE); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes("Shortcut", renderer, "text", KB_COL_SHORTCUT, NULL); + gtk_tree_view_column_set_min_width(column, 150); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + + g_signal_connect(tree, "row-activated", G_CALLBACK(on_keybind_row_activated), km); + + GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_container_add(GTK_CONTAINER(scroll), tree); + gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0); + + /* Reset button */ + GtkWidget *btn_hbox = gtk_hbox_new(FALSE, 0); + GtkWidget *reset_btn = gtk_button_new_with_label("Reset to Defaults"); + g_object_set_data(G_OBJECT(reset_btn), "km", km); + g_object_set_data(G_OBJECT(reset_btn), "store", store); + g_signal_connect(reset_btn, "clicked", G_CALLBACK(on_keybinds_reset_clicked), NULL); + gtk_box_pack_end(GTK_BOX(btn_hbox), reset_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), btn_hbox, FALSE, FALSE, 4); + + return vbox; +} + +static void on_keybinds_reset_clicked(GtkButton *button, gpointer data) +{ + (void)data; + KeybindManager *km = g_object_get_data(G_OBJECT(button), "km"); + GtkListStore *store = g_object_get_data(G_OBJECT(button), "store"); + + keybind_manager_reset_defaults(km); + + gtk_list_store_clear(store); + for (int i = 0; i < KEYBIND_COUNT; i++) { + Keybind *kb = keybind_manager_get(km, (KeybindAction)i); + if (!kb) continue; + char *display = keybind_get_display_string(kb->keyval, kb->mods); + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + KB_COL_ACTION_NAME, kb->label, + KB_COL_SHORTCUT, display, + KB_COL_ACTION_ID, i, -1); + g_free(display); + } +} + +/* Help > Keyboard Shortcuts: read-only quick reference */ +void dialogs_show_keybinds(GtkWindow *parent, KeybindManager *km) { GtkWidget *dialog = gtk_dialog_new_with_buttons("Keyboard Shortcuts", parent, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL); - gtk_window_set_default_size(GTK_WINDOW(dialog), 350, -1); + gtk_window_set_default_size(GTK_WINDOW(dialog), 400, 420); GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + gtk_container_set_border_width(GTK_CONTAINER(content_area), 12); - GtkWidget *label = gtk_label_new( - "Space\t\t\tPlay/Pause\n" - "F / F11\t\t\tFullscreen\n" - "Left / Right\tSeek Backward/Forward (5s)\n" - "Up / Down\t\tVolume Up/Down\n" - "M\t\t\t\tMute\n" - "Page Up/Down\tPrevious/Next Chapter\n" - "Esc\t\t\t\tExit Fullscreen" - ); - gtk_label_set_use_markup(GTK_LABEL(label), TRUE); - gtk_container_set_border_width(GTK_CONTAINER(label), 20); - gtk_box_pack_start(GTK_BOX(content_area), label, TRUE, TRUE, 0); + /* Title */ + GtkWidget *title = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(title), "Keyboard Shortcuts"); + gtk_misc_set_alignment(GTK_MISC(title), 0.0, 0.5); + gtk_box_pack_start(GTK_BOX(content_area), title, FALSE, FALSE, 4); + + GtkWidget *sep = gtk_hseparator_new(); + gtk_box_pack_start(GTK_BOX(content_area), sep, FALSE, FALSE, 4); + + /* Read-only list */ + GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); + + for (int i = 0; i < KEYBIND_COUNT; i++) { + Keybind *kb = keybind_manager_get(km, (KeybindAction)i); + if (!kb) continue; + char *display = keybind_get_display_string(kb->keyval, kb->mods); + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, kb->label, 1, display, -1); + g_free(display); + } + + GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); + g_object_unref(store); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree), TRUE); + + GtkCellRenderer *r = gtk_cell_renderer_text_new(); + GtkTreeViewColumn *c; + c = gtk_tree_view_column_new_with_attributes("Action", r, "text", 0, NULL); + gtk_tree_view_column_set_expand(c, TRUE); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), c); + + r = gtk_cell_renderer_text_new(); + c = gtk_tree_view_column_new_with_attributes("Shortcut", r, "text", 1, NULL); + gtk_tree_view_column_set_min_width(c, 140); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), c); + + GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_container_add(GTK_CONTAINER(scroll), tree); + gtk_box_pack_start(GTK_BOX(content_area), scroll, TRUE, TRUE, 0); + + /* Footer hint */ + GtkWidget *hint = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(hint), "To customise shortcuts, go to Tools \342\206\222 Preferences \342\206\222 Keybinds tab."); + gtk_misc_set_alignment(GTK_MISC(hint), 0.0, 0.5); + gtk_box_pack_start(GTK_BOX(content_area), hint, FALSE, FALSE, 8); gtk_widget_show_all(dialog); gtk_dialog_run(GTK_DIALOG(dialog)); @@ -1069,7 +1281,7 @@ static void on_sub_font_browse_clicked(GtkButton *btn, gpointer data) gtk_widget_destroy(font_dialog); } -gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginManager *pm, int initial_tab, PreferencesApplyCallback apply_cb, gpointer user_data) +gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginManager *pm, KeybindManager *km, int initial_tab, PreferencesApplyCallback apply_cb, gpointer user_data) { if (!prefs) return FALSE; @@ -1308,6 +1520,12 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginM gtk_notebook_append_page(GTK_NOTEBOOK(notebook), config_vbox, gtk_label_new("Config File")); + /* 7. Keybinds Tab */ + if (km) { + GtkWidget *keybinds_widget = create_keybinds_editor_widget(km); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), keybinds_widget, gtk_label_new("Keybinds")); + } + if (initial_tab >= 0 && initial_tab < gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook))) { gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), initial_tab); } diff --git a/src/dialogs.h b/src/dialogs.h index dadb114..c66a432 100644 --- a/src/dialogs.h +++ b/src/dialogs.h @@ -4,6 +4,7 @@ #include #include "player.h" #include "playlist.h" +#include "keybinds.h" /* Forward declaration */ struct PluginManager; @@ -53,7 +54,7 @@ void preferences_free(Preferences *prefs); void preferences_load(Preferences *prefs); void preferences_save(Preferences *prefs); typedef void (*PreferencesApplyCallback)(gpointer data); -gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, struct PluginManager *pm, int initial_tab, PreferencesApplyCallback apply_cb, gpointer user_data); +gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, struct PluginManager *pm, KeybindManager *km, int initial_tab, PreferencesApplyCallback apply_cb, gpointer user_data); /* File dialogs */ GSList* dialogs_open_files(GtkWindow *parent, Preferences *prefs); @@ -65,7 +66,7 @@ char* dialogs_save_screenshot(GtkWindow *parent); /* About and Keybinds dialogs */ void dialogs_show_about(GtkWindow *parent); -void dialogs_show_keybinds(GtkWindow *parent); +void dialogs_show_keybinds(GtkWindow *parent, KeybindManager *km); /* Go to time dialog */ double dialogs_goto_time(GtkWindow *parent, double current_time, double duration); diff --git a/src/keybinds.c b/src/keybinds.c new file mode 100644 index 0000000..52cc596 --- /dev/null +++ b/src/keybinds.c @@ -0,0 +1,153 @@ +#include "keybinds.h" +#include +#include +#include +#include + +/* Default keybind table */ +static const Keybind default_keybinds[KEYBIND_COUNT] = { + { KEYBIND_PLAY_PAUSE, "Play/Pause", GDK_space, 0 }, + { KEYBIND_STOP, "Stop", GDK_s, 0 }, + { KEYBIND_FULLSCREEN, "Fullscreen", GDK_f, 0 }, + { KEYBIND_EXIT_FULLSCREEN, "Exit Fullscreen", GDK_Escape, 0 }, + { KEYBIND_SEEK_FORWARD, "Seek Forward", GDK_Right, 0 }, + { KEYBIND_SEEK_BACKWARD, "Seek Backward", GDK_Left, 0 }, + { KEYBIND_VOLUME_UP, "Volume Up", GDK_Up, 0 }, + { KEYBIND_VOLUME_DOWN, "Volume Down", GDK_Down, 0 }, + { KEYBIND_MUTE, "Mute", GDK_m, 0 }, + { KEYBIND_PREV_CHAPTER, "Previous Chapter", GDK_Page_Up, 0 }, + { KEYBIND_NEXT_CHAPTER, "Next Chapter", GDK_Page_Down, 0 }, + { KEYBIND_FRAME_STEP, "Frame Step", GDK_period, 0 }, + { KEYBIND_OPEN_FILE, "Open File", GDK_o, GDK_CONTROL_MASK }, + { KEYBIND_OPEN_URL, "Open URL", GDK_l, GDK_CONTROL_MASK }, + { KEYBIND_GOTO_TIME, "Go to Time", GDK_g, GDK_CONTROL_MASK }, + { KEYBIND_SCREENSHOT, "Screenshot", GDK_s, GDK_CONTROL_MASK | GDK_SHIFT_MASK }, + { KEYBIND_TOGGLE_PLAYLIST, "Toggle Playlist", GDK_p, GDK_CONTROL_MASK }, + { KEYBIND_QUIT, "Quit", GDK_q, GDK_CONTROL_MASK }, +}; + +KeybindManager* keybind_manager_new(void) +{ + KeybindManager *km = g_new0(KeybindManager, 1); + keybind_manager_reset_defaults(km); + return km; +} + +void keybind_manager_free(KeybindManager *km) +{ + if (km) g_free(km); +} + +void keybind_manager_reset_defaults(KeybindManager *km) +{ + if (!km) return; + memcpy(km->binds, default_keybinds, sizeof(default_keybinds)); +} + +void keybind_manager_load(KeybindManager *km, GKeyFile *keyfile) +{ + if (!km || !keyfile) return; + + /* Start from defaults, then override with saved values */ + keybind_manager_reset_defaults(km); + + if (!g_key_file_has_group(keyfile, "Keybinds")) return; + + for (int i = 0; i < KEYBIND_COUNT; i++) { + const char *action_name = km->binds[i].label; + GError *error = NULL; + + /* Build key names like "Play/Pause_key" and "Play/Pause_mods" */ + char key_name[128]; + char mod_name[128]; + snprintf(key_name, sizeof(key_name), "%s_key", action_name); + snprintf(mod_name, sizeof(mod_name), "%s_mods", action_name); + + char *keyval_str = g_key_file_get_string(keyfile, "Keybinds", key_name, &error); + if (keyval_str && !error) { + guint kv = gdk_keyval_from_name(keyval_str); + if (kv != 0xffffff) { /* GDK_KEY_VoidSymbol */ + km->binds[i].keyval = kv; + } + g_free(keyval_str); + } + if (error) { g_error_free(error); error = NULL; } + + int mods_val = g_key_file_get_integer(keyfile, "Keybinds", mod_name, &error); + if (!error) { + km->binds[i].mods = (GdkModifierType)mods_val; + } + if (error) { g_error_free(error); error = NULL; } + } +} + +void keybind_manager_save(KeybindManager *km, GKeyFile *keyfile) +{ + if (!km || !keyfile) return; + + for (int i = 0; i < KEYBIND_COUNT; i++) { + const char *action_name = km->binds[i].label; + char key_name[128]; + char mod_name[128]; + snprintf(key_name, sizeof(key_name), "%s_key", action_name); + snprintf(mod_name, sizeof(mod_name), "%s_mods", action_name); + + const char *keyval_str = gdk_keyval_name(km->binds[i].keyval); + if (keyval_str) { + g_key_file_set_string(keyfile, "Keybinds", key_name, keyval_str); + } + g_key_file_set_integer(keyfile, "Keybinds", mod_name, (int)km->binds[i].mods); + } +} + +void keybind_manager_set_keybind(KeybindManager *km, KeybindAction action, guint keyval, GdkModifierType mods) +{ + if (!km || action < 0 || action >= KEYBIND_COUNT) return; + km->binds[action].keyval = keyval; + km->binds[action].mods = mods; +} + +Keybind* keybind_manager_get(KeybindManager *km, KeybindAction action) +{ + if (!km || action < 0 || action >= KEYBIND_COUNT) return NULL; + return &km->binds[action]; +} + +KeybindAction keybind_manager_find_action(KeybindManager *km, guint keyval, GdkModifierType mods) +{ + if (!km) return KEYBIND_COUNT; + + /* Mask out irrelevant modifier bits (lock keys, etc.) */ + GdkModifierType relevant = GDK_CONTROL_MASK | GDK_SHIFT_MASK | GDK_MOD1_MASK; + GdkModifierType clean_mods = mods & relevant; + + for (int i = 0; i < KEYBIND_COUNT; i++) { + if (km->binds[i].keyval == keyval && km->binds[i].mods == clean_mods) { + return (KeybindAction)i; + } + } + return KEYBIND_COUNT; /* not found */ +} + +char* keybind_get_display_string(guint keyval, GdkModifierType mods) +{ + GString *str = g_string_new(NULL); + + if (mods & GDK_CONTROL_MASK) g_string_append(str, "Ctrl+"); + if (mods & GDK_SHIFT_MASK) g_string_append(str, "Shift+"); + if (mods & GDK_MOD1_MASK) g_string_append(str, "Alt+"); + + const char *name = gdk_keyval_name(keyval); + if (name) { + /* Capitalize first letter for display */ + if (strlen(name) == 1 && name[0] >= 'a' && name[0] <= 'z') { + g_string_append_c(str, name[0] - 32); + } else { + g_string_append(str, name); + } + } else { + g_string_append(str, "???"); + } + + return g_string_free(str, FALSE); +} diff --git a/src/keybinds.h b/src/keybinds.h new file mode 100644 index 0000000..a3c8997 --- /dev/null +++ b/src/keybinds.h @@ -0,0 +1,59 @@ +#ifndef KEYBINDS_H +#define KEYBINDS_H + +#include + +/* All bindable actions */ +typedef enum { + KEYBIND_PLAY_PAUSE, + KEYBIND_STOP, + KEYBIND_FULLSCREEN, + KEYBIND_EXIT_FULLSCREEN, + KEYBIND_SEEK_FORWARD, + KEYBIND_SEEK_BACKWARD, + KEYBIND_VOLUME_UP, + KEYBIND_VOLUME_DOWN, + KEYBIND_MUTE, + KEYBIND_PREV_CHAPTER, + KEYBIND_NEXT_CHAPTER, + KEYBIND_FRAME_STEP, + KEYBIND_OPEN_FILE, + KEYBIND_OPEN_URL, + KEYBIND_GOTO_TIME, + KEYBIND_SCREENSHOT, + KEYBIND_TOGGLE_PLAYLIST, + KEYBIND_QUIT, + KEYBIND_COUNT /* must be last */ +} KeybindAction; + +typedef struct { + KeybindAction action; + const char *label; /* Human-readable name */ + guint keyval; /* GDK key value */ + GdkModifierType mods; /* Modifier mask (Ctrl, Shift, etc.) */ +} Keybind; + +typedef struct { + Keybind binds[KEYBIND_COUNT]; +} KeybindManager; + +/* Lifecycle */ +KeybindManager* keybind_manager_new(void); +void keybind_manager_free(KeybindManager *km); + +/* Persistence */ +void keybind_manager_load(KeybindManager *km, GKeyFile *keyfile); +void keybind_manager_save(KeybindManager *km, GKeyFile *keyfile); + +/* Modification */ +void keybind_manager_reset_defaults(KeybindManager *km); +void keybind_manager_set_keybind(KeybindManager *km, KeybindAction action, guint keyval, GdkModifierType mods); + +/* Lookup */ +Keybind* keybind_manager_get(KeybindManager *km, KeybindAction action); +KeybindAction keybind_manager_find_action(KeybindManager *km, guint keyval, GdkModifierType mods); + +/* Utility */ +char* keybind_get_display_string(guint keyval, GdkModifierType mods); + +#endif /* KEYBINDS_H */ diff --git a/src/keybinds.o b/src/keybinds.o new file mode 100644 index 0000000..2929589 Binary files /dev/null and b/src/keybinds.o differ diff --git a/src/ui.c b/src/ui.c index 93e3623..7d1056b 100644 --- a/src/ui.c +++ b/src/ui.c @@ -106,6 +106,9 @@ struct AppUI { /* Subtitle dragging */ gboolean sub_dragging; + + /* Keybind manager */ + KeybindManager *keybinds; }; /* Forward declarations */ @@ -192,6 +195,20 @@ AppUI* ui_new(void) ui->prefs = preferences_new(); preferences_load(ui->prefs); + /* Load keybinds from config file */ + ui->keybinds = keybind_manager_new(); + { + char *config_dir = g_build_filename(g_get_user_config_dir(), "gtk2-media-player", NULL); + char *config_path = g_build_filename(config_dir, "settings.conf", NULL); + GKeyFile *kf = g_key_file_new(); + if (g_key_file_load_from_file(kf, config_path, G_KEY_FILE_NONE, NULL)) { + keybind_manager_load(ui->keybinds, kf); + } + g_key_file_free(kf); + g_free(config_path); + g_free(config_dir); + } + /* Main window */ ui->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(ui->window), "Kino"); @@ -346,6 +363,24 @@ void ui_destroy(AppUI *ui) { if (!ui) return; + /* Save keybinds */ + if (ui->keybinds) { + char *config_dir = g_build_filename(g_get_user_config_dir(), "gtk2-media-player", NULL); + char *config_path = g_build_filename(config_dir, "settings.conf", NULL); + GKeyFile *kf = g_key_file_new(); + g_key_file_load_from_file(kf, config_path, G_KEY_FILE_NONE, NULL); + keybind_manager_save(ui->keybinds, kf); + gchar *data = g_key_file_to_data(kf, NULL, NULL); + if (data) { + g_file_set_contents(config_path, data, -1, NULL); + g_free(data); + } + g_key_file_free(kf); + g_free(config_path); + g_free(config_dir); + keybind_manager_free(ui->keybinds); + } + if (ui->auto_hide_timer_id > 0) { g_source_remove(ui->auto_hide_timer_id); } @@ -1011,50 +1046,72 @@ static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer dat (void)widget; AppUI *ui = (AppUI *)data; - switch (event->keyval) { - case GDK_Escape: - if (ui->is_fullscreen) { - ui_toggle_fullscreen(ui); - } - return TRUE; - - case GDK_F11: - case GDK_f: - ui_toggle_fullscreen(ui); - return TRUE; - - case GDK_space: + GdkModifierType relevant = GDK_CONTROL_MASK | GDK_SHIFT_MASK | GDK_MOD1_MASK; + GdkModifierType mods = event->state & relevant; + KeybindAction action = keybind_manager_find_action(ui->keybinds, event->keyval, mods); + + switch (action) { + case KEYBIND_PLAY_PAUSE: on_playback_play_pause(NULL, ui); return TRUE; - - case GDK_Page_Up: - player_prev_chapter(ui->player); + case KEYBIND_STOP: + player_stop(ui->player); return TRUE; - - case GDK_Page_Down: - player_next_chapter(ui->player); + case KEYBIND_FULLSCREEN: + ui_toggle_fullscreen(ui); return TRUE; - - case GDK_Left: - player_seek(ui->player, -5); + case KEYBIND_EXIT_FULLSCREEN: + if (ui->is_fullscreen) ui_toggle_fullscreen(ui); return TRUE; - - case GDK_Right: + case KEYBIND_SEEK_FORWARD: player_seek(ui->player, 5); return TRUE; - - case GDK_Up: + case KEYBIND_SEEK_BACKWARD: + player_seek(ui->player, -5); + return TRUE; + case KEYBIND_VOLUME_UP: player_set_volume(ui->player, player_get_volume(ui->player) + 5); return TRUE; - - case GDK_Down: + case KEYBIND_VOLUME_DOWN: player_set_volume(ui->player, player_get_volume(ui->player) - 5); return TRUE; - - case GDK_m: + case KEYBIND_MUTE: player_set_muted(ui->player, !player_get_muted(ui->player)); return TRUE; - + case KEYBIND_PREV_CHAPTER: + player_prev_chapter(ui->player); + return TRUE; + case KEYBIND_NEXT_CHAPTER: + player_next_chapter(ui->player); + return TRUE; + case KEYBIND_FRAME_STEP: + on_playback_frame_step(NULL, ui); + return TRUE; + case KEYBIND_OPEN_FILE: + ui_open_file(ui); + return TRUE; + case KEYBIND_OPEN_URL: { + char *url = dialogs_open_url(GTK_WINDOW(ui->window), ui->prefs); + if (url) { + playlist_clear(ui->playlist); + playlist_add_file(ui->playlist, url, FALSE); + playlist_play_index(ui->playlist, 0); + g_free(url); + } + return TRUE; + } + case KEYBIND_GOTO_TIME: + on_playback_goto_time(NULL, ui); + return TRUE; + case KEYBIND_SCREENSHOT: + on_view_screenshot(NULL, ui); + return TRUE; + case KEYBIND_TOGGLE_PLAYLIST: + on_view_playlist(NULL, ui); + return TRUE; + case KEYBIND_QUIT: + gtk_main_quit(); + return TRUE; default: break; } @@ -1946,7 +2003,7 @@ static void on_view_preferences(GtkMenuItem *item, gpointer data) { (void)item; AppUI *ui = (AppUI *)data; - dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs, ui->plugin_manager, -1, ui_apply_preferences_internal, ui); + dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs, ui->plugin_manager, ui->keybinds, -1, ui_apply_preferences_internal, ui); } static void on_subtitle_settings_clicked(GtkMenuItem *item, gpointer data) @@ -1954,14 +2011,14 @@ static void on_subtitle_settings_clicked(GtkMenuItem *item, gpointer data) (void)item; AppUI *ui = (AppUI *)data; /* Open preferences directly on the Subtitles tab (index 1) */ - dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs, ui->plugin_manager, 1, ui_apply_preferences_internal, ui); + dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs, ui->plugin_manager, ui->keybinds, 1, ui_apply_preferences_internal, ui); } static void on_help_keybinds(GtkMenuItem *item, gpointer data) { (void)item; AppUI *ui = (AppUI *)data; - dialogs_show_keybinds(GTK_WINDOW(ui->window)); + dialogs_show_keybinds(GTK_WINDOW(ui->window), ui->keybinds); } static void on_help_about(GtkMenuItem *item, gpointer data)