Added keybinds menu. You can change keybinds at will and view then under Help -> Keyboard Shortcuts

This commit is contained in:
laki 2026-03-29 10:57:40 +01:00
parent 06f793f4f4
commit ac19a6af1e
10 changed files with 589 additions and 52 deletions

BIN
Kino Executable file

Binary file not shown.

View File

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

48
install.sh Executable file
View File

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

BIN
lib/libmpv.so.2 Normal file

Binary file not shown.

View File

@ -1,4 +1,5 @@
#include "dialogs.h"
#include <gdk/gdkkeysyms.h>
#include <pango/pango.h>
#include <stdlib.h>
#include <string.h>
@ -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<b>%s</b>", 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), "<i>Double-click a shortcut to change it.</i>");
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(
"<b>Space</b>\t\t\tPlay/Pause\n"
"<b>F / F11</b>\t\t\tFullscreen\n"
"<b>Left / Right</b>\tSeek Backward/Forward (5s)\n"
"<b>Up / Down</b>\t\tVolume Up/Down\n"
"<b>M</b>\t\t\t\tMute\n"
"<b>Page Up/Down</b>\tPrevious/Next Chapter\n"
"<b>Esc</b>\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), "<b>Keyboard Shortcuts</b>");
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), "<i>To customise shortcuts, go to Tools \342\206\222 Preferences \342\206\222 Keybinds tab.</i>");
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);
}

View File

@ -4,6 +4,7 @@
#include <gtk/gtk.h>
#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);

153
src/keybinds.c Normal file
View File

@ -0,0 +1,153 @@
#include "keybinds.h"
#include <gdk/gdkkeysyms.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* 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);
}

59
src/keybinds.h Normal file
View File

@ -0,0 +1,59 @@
#ifndef KEYBINDS_H
#define KEYBINDS_H
#include <gtk/gtk.h>
/* 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 */

BIN
src/keybinds.o Normal file

Binary file not shown.

125
src/ui.c
View File

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