From 25dcc3b136d96cd1d47754b9bae2a006f71586e2 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch <p@janouch.name>
Date: Sun, 9 Jan 2022 07:44:47 +0100
Subject: Handle view bindings through an action signal

This makes them adjustable.
---
 README.adoc |   6 ++
 fiv-view.c  | 182 ++++++++++++++++++++++++++++--------------------------------
 fiv-view.h  |  65 +++++++++++++---------
 3 files changed, 130 insertions(+), 123 deletions(-)

diff --git a/README.adoc b/README.adoc
index 7837f4b..456a590 100644
--- a/README.adoc
+++ b/README.adoc
@@ -61,6 +61,12 @@ _~/.config/gtk-3.0/gtk.css_:
 
  fiv-browser { -FivBrowser-spacing: 0; padding: 0; border: 0; margin: 0; }
 
+Similarly, you can adjust some of the key bindings, as per the command table
+in link:fiv-view.h[]:
+
+ @binding-set ViewBindings { bind 'p' { 'command' (print) }; }
+ fiv-view { -gtk-key-bindings: ViewBindings; }
+
 Should you want to experiment, you will find the GTK+ inspector very helpful.
 
 Contributing and Support
diff --git a/fiv-view.c b/fiv-view.c
index 5531fc4..b8278ad 100644
--- a/fiv-view.c
+++ b/fiv-view.c
@@ -31,6 +31,23 @@
 #include <gdk/gdkquartz.h>
 #endif  // GDK_WINDOWING_QUARTZ
 
+GType
+fiv_view_command_get_type(void)
+{
+	static gsize guard;
+	if (g_once_init_enter(&guard)) {
+#define XX(constant, name) {constant, #constant, name},
+		static const GEnumValue values[] = {FIV_VIEW_COMMANDS(XX) {}};
+#undef XX
+		GType type = g_enum_register_static(
+			g_intern_static_string("FivViewCommand"), values);
+		g_once_init_leave(&guard, type);
+	}
+	return guard;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
 struct _FivView {
 	GtkWidget parent_instance;
 	gchar *uri;                         ///< Path to the current image (if any)
@@ -112,6 +129,14 @@ enum {
 
 static GParamSpec *view_properties[N_PROPERTIES];
 
+enum {
+	COMMAND,
+	LAST_SIGNAL
+};
+
+// Globals are, sadly, the canonical way of storing signal numbers.
+static guint view_signals[LAST_SIGNAL];
+
 static void
 fiv_view_finalize(GObject *gobject)
 {
@@ -478,26 +503,24 @@ set_scale(FivView *self, double scale)
 	return set_scale_to_fit(self, false);
 }
 
-static gboolean
+static void
 set_scale_to_fit_width(FivView *self)
 {
 	double w = get_surface_dimensions(self).width;
 	int allocated = gtk_widget_get_allocated_width(
 		gtk_widget_get_parent(GTK_WIDGET(self)));
 	if (ceil(w * self->scale) > allocated)
-		return set_scale(self, allocated / w);
-	return TRUE;
+		set_scale(self, allocated / w);
 }
 
-static gboolean
+static void
 set_scale_to_fit_height(FivView *self)
 {
 	double h = get_surface_dimensions(self).height;
 	int allocated = gtk_widget_get_allocated_height(
 		gtk_widget_get_parent(GTK_WIDGET(self)));
 	if (ceil(h * self->scale) > allocated)
-		return set_scale(self, allocated / h);
-	return TRUE;
+		set_scale(self, allocated / h);
 }
 
 static gboolean
@@ -951,102 +974,27 @@ info(FivView *self)
 
 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
-static inline gboolean
-command(FivView *self, FivViewCommand command)
-{
-	fiv_view_command(self, command);
-	return TRUE;
-}
-
 static gboolean
 fiv_view_key_press_event(GtkWidget *widget, GdkEventKey *event)
 {
 	FivView *self = FIV_VIEW(widget);
-	if (!self->image)
-		return FALSE;
-
-	// It should not matter that GDK_KEY_plus involves holding Shift.
-	guint state = event->state & gtk_accelerator_get_default_mod_mask() &
-		~GDK_SHIFT_MASK;
-
-	// The standard, intuitive bindings.
-	if (state == GDK_CONTROL_MASK) {
-		switch (event->keyval) {
-		case GDK_KEY_0:
-			return command(self, FIV_VIEW_COMMAND_ZOOM_1);
-		case GDK_KEY_plus:
-			return command(self, FIV_VIEW_COMMAND_ZOOM_IN);
-		case GDK_KEY_minus:
-			return command(self, FIV_VIEW_COMMAND_ZOOM_OUT);
-		case GDK_KEY_p:
-			return command(self, FIV_VIEW_COMMAND_PRINT);
-		case GDK_KEY_s:
-			return command(self, FIV_VIEW_COMMAND_SAVE_PAGE);
-		case GDK_KEY_S:
-			return save_as(self, self->frame);
-		}
-	}
-	if (state == GDK_MOD1_MASK) {
-		switch (event->keyval) {
-		case GDK_KEY_Return:
-			return command(self, FIV_VIEW_COMMAND_INFO);
-		}
-	}
-	if (state != 0)
-		return FALSE;
 
-	switch (event->keyval) {
-	case GDK_KEY_1:
-	case GDK_KEY_2:
-	case GDK_KEY_3:
-	case GDK_KEY_4:
-	case GDK_KEY_5:
-	case GDK_KEY_6:
-	case GDK_KEY_7:
-	case GDK_KEY_8:
-	case GDK_KEY_9:
+	// So far, our commands cannot accept arguments, so these few are hardcoded.
+	if (self->image &&
+		!(event->state & gtk_accelerator_get_default_mod_mask()) &&
+		event->keyval >= GDK_KEY_1 && event->keyval <= GDK_KEY_9)
 		return set_scale(self, event->keyval - GDK_KEY_0);
-	case GDK_KEY_plus:
-		return command(self, FIV_VIEW_COMMAND_ZOOM_IN);
-	case GDK_KEY_minus:
-		return command(self, FIV_VIEW_COMMAND_ZOOM_OUT);
-
-	case GDK_KEY_w:
-		return set_scale_to_fit_width(self);
-	case GDK_KEY_h:
-		return set_scale_to_fit_height(self);
-
-	case GDK_KEY_c:
-		return command(self, FIV_VIEW_COMMAND_TOGGLE_CMS);
-	case GDK_KEY_x:  // Inspired by gThumb, which has more such modes.
-		return command(self, FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT);
-	case GDK_KEY_i:
-		return command(self, FIV_VIEW_COMMAND_TOGGLE_FILTER);
-	case GDK_KEY_t:
-		return command(self, FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD);
-	case GDK_KEY_e:
-		return command(self, FIV_VIEW_COMMAND_TOGGLE_ENHANCE);
-
-	case GDK_KEY_less:
-		return command(self, FIV_VIEW_COMMAND_ROTATE_LEFT);
-	case GDK_KEY_equal:
-		return command(self, FIV_VIEW_COMMAND_MIRROR);
-	case GDK_KEY_greater:
-		return command(self, FIV_VIEW_COMMAND_ROTATE_RIGHT);
-
-	case GDK_KEY_bracketleft:
-		return command(self, FIV_VIEW_COMMAND_PAGE_PREVIOUS);
-	case GDK_KEY_bracketright:
-		return command(self, FIV_VIEW_COMMAND_PAGE_NEXT);
-
-	case GDK_KEY_braceleft:
-		return command(self, FIV_VIEW_COMMAND_FRAME_PREVIOUS);
-	case GDK_KEY_braceright:
-		return command(self, FIV_VIEW_COMMAND_FRAME_NEXT);
-	case GDK_KEY_space:
-		return command(self, FIV_VIEW_COMMAND_TOGGLE_PLAYBACK);
-	}
-	return FALSE;
+
+	return GTK_WIDGET_CLASS(fiv_view_parent_class)
+		->key_press_event(widget, event);
+}
+
+static void
+bind(GtkBindingSet *bs, guint keyval, GdkModifierType modifiers,
+	FivViewCommand command)
+{
+	gtk_binding_entry_add_signal(
+		bs, keyval, modifiers, "command", 1, FIV_TYPE_VIEW_COMMAND, command);
 }
 
 static void
@@ -1093,6 +1041,11 @@ fiv_view_class_init(FivViewClass *klass)
 	g_object_class_install_properties(
 		object_class, N_PROPERTIES, view_properties);
 
+	view_signals[COMMAND] =
+		g_signal_new_class_handler("command", G_TYPE_FROM_CLASS(klass),
+			G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK(fiv_view_command),
+			NULL, NULL, NULL, G_TYPE_NONE, 1, FIV_TYPE_VIEW_COMMAND);
+
 	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
 	widget_class->get_preferred_height = fiv_view_get_preferred_height;
 	widget_class->get_preferred_width = fiv_view_get_preferred_width;
@@ -1105,6 +1058,37 @@ fiv_view_class_init(FivViewClass *klass)
 	widget_class->scroll_event = fiv_view_scroll_event;
 	widget_class->key_press_event = fiv_view_key_press_event;
 
+	GtkBindingSet *bs = gtk_binding_set_by_class(klass);
+	// First, the standard, intuitive bindings.
+	bind(bs, GDK_KEY_0,      GDK_CONTROL_MASK, FIV_VIEW_COMMAND_ZOOM_1);
+	bind(bs, GDK_KEY_plus,   GDK_CONTROL_MASK, FIV_VIEW_COMMAND_ZOOM_IN);
+	bind(bs, GDK_KEY_minus,  GDK_CONTROL_MASK, FIV_VIEW_COMMAND_ZOOM_OUT);
+	bind(bs, GDK_KEY_p,      GDK_CONTROL_MASK, FIV_VIEW_COMMAND_PRINT);
+	bind(bs, GDK_KEY_s,      GDK_CONTROL_MASK, FIV_VIEW_COMMAND_SAVE_PAGE);
+	bind(bs, GDK_KEY_s,      GDK_MOD1_MASK,    FIV_VIEW_COMMAND_SAVE_FRAME);
+	bind(bs, GDK_KEY_Return, GDK_MOD1_MASK,    FIV_VIEW_COMMAND_INFO);
+
+	// The scale-to-fit binding is from gThumb, which has more such modes.
+	bind(bs, GDK_KEY_plus,         0, FIV_VIEW_COMMAND_ZOOM_IN);
+	bind(bs, GDK_KEY_minus,        0, FIV_VIEW_COMMAND_ZOOM_OUT);
+	bind(bs, GDK_KEY_w,            0, FIV_VIEW_COMMAND_FIT_WIDTH);
+	bind(bs, GDK_KEY_h,            0, FIV_VIEW_COMMAND_FIT_HEIGHT);
+	bind(bs, GDK_KEY_x,            0, FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT);
+	bind(bs, GDK_KEY_c,            0, FIV_VIEW_COMMAND_TOGGLE_CMS);
+	bind(bs, GDK_KEY_i,            0, FIV_VIEW_COMMAND_TOGGLE_FILTER);
+	bind(bs, GDK_KEY_t,            0, FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD);
+	bind(bs, GDK_KEY_e,            0, FIV_VIEW_COMMAND_TOGGLE_ENHANCE);
+
+	bind(bs, GDK_KEY_less,         0, FIV_VIEW_COMMAND_ROTATE_LEFT);
+	bind(bs, GDK_KEY_equal,        0, FIV_VIEW_COMMAND_MIRROR);
+	bind(bs, GDK_KEY_greater,      0, FIV_VIEW_COMMAND_ROTATE_RIGHT);
+
+	bind(bs, GDK_KEY_bracketleft,  0, FIV_VIEW_COMMAND_PAGE_PREVIOUS);
+	bind(bs, GDK_KEY_bracketright, 0, FIV_VIEW_COMMAND_PAGE_NEXT);
+	bind(bs, GDK_KEY_braceleft,    0, FIV_VIEW_COMMAND_FRAME_PREVIOUS);
+	bind(bs, GDK_KEY_braceright,   0, FIV_VIEW_COMMAND_FRAME_NEXT);
+	bind(bs, GDK_KEY_space,        0, FIV_VIEW_COMMAND_TOGGLE_PLAYBACK);
+
 	// TODO(p): Later override "screen_changed", recreate Pango layouts there,
 	// if we get to have any, or otherwise reflect DPI changes.
 	gtk_widget_class_set_css_name(widget_class, "fiv-view");
@@ -1277,6 +1261,8 @@ fiv_view_command(FivView *self, FivViewCommand command)
 		print(self);
 	break; case FIV_VIEW_COMMAND_SAVE_PAGE:
 		save_as(self, NULL);
+	break; case FIV_VIEW_COMMAND_SAVE_FRAME:
+		save_as(self, self->frame);
 	break; case FIV_VIEW_COMMAND_INFO:
 		info(self);
 
@@ -1286,6 +1272,10 @@ fiv_view_command(FivView *self, FivViewCommand command)
 		set_scale(self, self->scale / SCALE_STEP);
 	break; case FIV_VIEW_COMMAND_ZOOM_1:
 		set_scale(self, 1.0);
+	break; case FIV_VIEW_COMMAND_FIT_WIDTH:
+		set_scale_to_fit_width(self);
+	break; case FIV_VIEW_COMMAND_FIT_HEIGHT:
+		set_scale_to_fit_height(self);
 	break; case FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT:
 		set_scale_to_fit(self, !self->scale_to_fit);
 	}
diff --git a/fiv-view.h b/fiv-view.h
index 7a37169..cec7093 100644
--- a/fiv-view.h
+++ b/fiv-view.h
@@ -25,35 +25,46 @@ G_DECLARE_FINAL_TYPE(FivView, fiv_view, FIV, VIEW, GtkWidget)
 /// Try to open the given file, synchronously, to be displayed by the widget.
 gboolean fiv_view_open(FivView *self, const gchar *uri, GError **error);
 
+// And this is how you avoid glib-mkenums.
 typedef enum _FivViewCommand {
-	FIV_VIEW_COMMAND_ROTATE_LEFT = 1,
-	FIV_VIEW_COMMAND_MIRROR,
-	FIV_VIEW_COMMAND_ROTATE_RIGHT,
-
-	FIV_VIEW_COMMAND_PAGE_FIRST,
-	FIV_VIEW_COMMAND_PAGE_PREVIOUS,
-	FIV_VIEW_COMMAND_PAGE_NEXT,
-	FIV_VIEW_COMMAND_PAGE_LAST,
-
-	FIV_VIEW_COMMAND_FRAME_FIRST,
-	FIV_VIEW_COMMAND_FRAME_PREVIOUS,
-	FIV_VIEW_COMMAND_FRAME_NEXT,
-	// Going to the end frame makes no sense, wrap around if needed.
-	FIV_VIEW_COMMAND_TOGGLE_PLAYBACK,
-
-	FIV_VIEW_COMMAND_TOGGLE_CMS,
-	FIV_VIEW_COMMAND_TOGGLE_FILTER,
-	FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD,
-	FIV_VIEW_COMMAND_TOGGLE_ENHANCE,
-	FIV_VIEW_COMMAND_PRINT,
-	FIV_VIEW_COMMAND_SAVE_PAGE,
-	FIV_VIEW_COMMAND_INFO,
-
-	FIV_VIEW_COMMAND_ZOOM_IN,
-	FIV_VIEW_COMMAND_ZOOM_OUT,
-	FIV_VIEW_COMMAND_ZOOM_1,
-	FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT,
+#define FIV_VIEW_COMMANDS(XX)                                                  \
+	XX(FIV_VIEW_COMMAND_ROTATE_LEFT,         "rotate-left")                    \
+	XX(FIV_VIEW_COMMAND_MIRROR,              "mirror")                         \
+	XX(FIV_VIEW_COMMAND_ROTATE_RIGHT,        "rotate-right")                   \
+	\
+	XX(FIV_VIEW_COMMAND_PAGE_FIRST,          "page-first")                     \
+	XX(FIV_VIEW_COMMAND_PAGE_PREVIOUS,       "page-previous")                  \
+	XX(FIV_VIEW_COMMAND_PAGE_NEXT,           "page-next")                      \
+	XX(FIV_VIEW_COMMAND_PAGE_LAST,           "page-last")                      \
+	\
+	XX(FIV_VIEW_COMMAND_FRAME_FIRST,         "frame-first")                    \
+	XX(FIV_VIEW_COMMAND_FRAME_PREVIOUS,      "frame-previous")                 \
+	XX(FIV_VIEW_COMMAND_FRAME_NEXT,          "frame-next")                     \
+	/* Going to the end frame makes no sense, wrap around if needed. */        \
+	XX(FIV_VIEW_COMMAND_TOGGLE_PLAYBACK,     "toggle-playback")                \
+	\
+	XX(FIV_VIEW_COMMAND_TOGGLE_CMS,          "toggle-cms")                     \
+	XX(FIV_VIEW_COMMAND_TOGGLE_FILTER,       "toggle-filter")                  \
+	XX(FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD, "toggle-checkerboard")            \
+	XX(FIV_VIEW_COMMAND_TOGGLE_ENHANCE,      "toggle-enhance")                 \
+	XX(FIV_VIEW_COMMAND_PRINT,               "print")                          \
+	XX(FIV_VIEW_COMMAND_SAVE_PAGE,           "save-page")                      \
+	XX(FIV_VIEW_COMMAND_SAVE_FRAME,          "save-frame")                     \
+	XX(FIV_VIEW_COMMAND_INFO,                "info")                           \
+	\
+	XX(FIV_VIEW_COMMAND_ZOOM_IN,             "zoom-in")                        \
+	XX(FIV_VIEW_COMMAND_ZOOM_OUT,            "zoom-out")                       \
+	XX(FIV_VIEW_COMMAND_ZOOM_1,              "zoom-1")                         \
+	XX(FIV_VIEW_COMMAND_FIT_WIDTH,           "fit-width")                      \
+	XX(FIV_VIEW_COMMAND_FIT_HEIGHT,          "fit-height")                     \
+	XX(FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT, "toggle-scale-to-fit")
+#define XX(constant, name) constant,
+	FIV_VIEW_COMMANDS(XX)
+#undef XX
 } FivViewCommand;
 
+GType fiv_view_command_get_type(void) G_GNUC_CONST;
+#define FIV_TYPE_VIEW_COMMAND (fiv_view_command_get_type())
+
 /// Execute a user action.
 void fiv_view_command(FivView *self, FivViewCommand command);
-- 
cgit v1.2.3-70-g09d2