aboutsummaryrefslogtreecommitdiff
path: root/fiv-view.c
diff options
context:
space:
mode:
Diffstat (limited to 'fiv-view.c')
-rw-r--r--fiv-view.c220
1 files changed, 201 insertions, 19 deletions
diff --git a/fiv-view.c b/fiv-view.c
index fb01b3a..42c2585 100644
--- a/fiv-view.c
+++ b/fiv-view.c
@@ -1,7 +1,7 @@
//
// fiv-view.c: image viewing widget
//
-// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2025, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -77,6 +77,9 @@ struct _FivView {
bool fixate : 1; ///< Keep zoom and position
double scale; ///< Scaling factor
double drag_start[2]; ///< Adjustment values for drag origin
+ double zoom_gesture_initial_scale; ///< Pinch gesture initial scale
+ double zoom_gesture_center[2]; ///< Pinch gesture widget coordinates
+ double zoom_gesture_surface[2]; ///< Pinch gesture surface coordinates
FivIoImage *enhance_swap; ///< Quick swap in/out
FivIoProfile *screen_cms_profile; ///< Target colour profile for widget
@@ -646,7 +649,8 @@ reload_screen_cms_profile(FivView *self, GdkWindow *window)
gchar *data = NULL;
gsize length = 0;
if (g_file_get_contents(path, &data, &length, NULL))
- self->screen_cms_profile = fiv_io_profile_new(data, length);
+ self->screen_cms_profile = fiv_io_cmm_get_profile(
+ fiv_io_cmm_get_default(), data, length);
g_free(data);
}
g_free(path);
@@ -850,6 +854,10 @@ gl_draw(FivView *self, cairo_t *cr)
cliph = allocation.height;
}
+ int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self));
+ clipw *= scale;
+ cliph *= scale;
+
enum { SRC, DEST };
GLuint textures[2] = {};
glGenTextures(2, textures);
@@ -957,12 +965,14 @@ gl_draw(FivView *self, cairo_t *cr)
// XXX: Native GdkWindows send this to the software fallback path.
// XXX: This only reliably alpha blends when using the software fallback,
// such as with a native window, because 7237f5d in GTK+ 3 is a regression.
+ // (Introduced in 3.24.39, reverted in 3.24.42.)
+ //
// We had to resort to rendering the checkerboard pattern in the shader.
// Unfortunately, it is hard to retrieve the theme colours from CSS.
GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
cairo_translate(cr, dx, dy);
gdk_cairo_draw_from_gl(
- cr, window, textures[DEST], GL_TEXTURE, 1, 0, 0, clipw, cliph);
+ cr, window, textures[DEST], GL_TEXTURE, scale, 0, 0, clipw, cliph);
gdk_gl_context_make_current(self->gl_context);
glDeleteBuffers(1, &vertex_buffer);
@@ -1121,28 +1131,14 @@ widget_to_surface(FivView *self, double *x, double *y)
}
static gboolean
-set_scale(FivView *self, double scale, const GdkEvent *event)
+set_scale_with_focus(FivView *self, double scale,
+ double focus_x, double focus_y, double surface_x, double surface_y)
{
- // FIXME: Zooming to exactly 1:1 breaks rendering with some images
- // when using a native X11 Window. This is a silly workaround.
- GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
- if (window && gdk_window_has_native(window) && scale == 1)
- scale = 0.999999999999999;
-
if (self->scale == scale)
goto out;
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
- double focus_x = 0, focus_y = 0;
- if (!event || !gdk_event_get_coords(event, &focus_x, &focus_y)) {
- focus_x = 0.5 * allocation.width;
- focus_y = 0.5 * allocation.height;
- }
-
- double surface_x = focus_x;
- double surface_y = focus_y;
- widget_to_surface(self, &surface_x, &surface_y);
self->scale = scale;
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_SCALE]);
@@ -1167,6 +1163,31 @@ out:
return set_scale_to_fit(self, false);
}
+static gboolean
+set_scale(FivView *self, double scale, const GdkEvent *event)
+{
+ // FIXME: Zooming to exactly 1:1 breaks rendering with some images
+ // when using a native X11 Window. This is a silly workaround.
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
+ if (window && gdk_window_has_native(window) && scale == 1)
+ scale = 0.999999999999999;
+
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
+ double focus_x = 0, focus_y = 0;
+ if (!event || !gdk_event_get_coords(event, &focus_x, &focus_y)) {
+ focus_x = 0.5 * allocation.width;
+ focus_y = 0.5 * allocation.height;
+ }
+
+ double surface_x = focus_x;
+ double surface_y = focus_y;
+ widget_to_surface(self, &surface_x, &surface_y);
+
+ return set_scale_with_focus(
+ self, scale, focus_x, focus_y, surface_x, surface_y);
+}
+
static void
set_scale_to_fit_width(FivView *self)
{
@@ -1411,6 +1432,33 @@ on_drag_end(GtkGestureDrag *drag, G_GNUC_UNUSED gdouble start_x,
gdk_window_set_cursor(window, NULL);
}
+static void
+on_zoom_begin(GtkGestureZoom *gesture,
+ G_GNUC_UNUSED GdkEventSequence *sequence, gpointer user_data)
+{
+ FivView *self = FIV_VIEW(user_data);
+ self->zoom_gesture_initial_scale = self->scale;
+
+ double x = 0, y = 0;
+ gtk_gesture_get_bounding_box_center(GTK_GESTURE(gesture), &x, &y);
+ self->zoom_gesture_center[0] = x;
+ self->zoom_gesture_center[1] = y;
+
+ widget_to_surface(self, &x, &y);
+ self->zoom_gesture_surface[0] = x;
+ self->zoom_gesture_surface[1] = y;
+}
+
+static void
+on_zoom_scale_changed(
+ G_GNUC_UNUSED GtkGestureZoom *gesture, gdouble scale, gpointer user_data)
+{
+ FivView *self = FIV_VIEW(user_data);
+ set_scale_with_focus(self, self->zoom_gesture_initial_scale * scale,
+ self->zoom_gesture_center[0], self->zoom_gesture_center[1],
+ self->zoom_gesture_surface[0], self->zoom_gesture_surface[1]);
+}
+
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
@@ -1431,6 +1479,127 @@ get_toplevel(GtkWidget *widget)
return NULL;
}
+struct zoom_ask_context {
+ GtkWidget *result_left, *result_right;
+ Dimensions dimensions;
+};
+
+static void
+on_zoom_ask_spin_changed(GtkSpinButton *spin, G_GNUC_UNUSED gpointer user_data)
+{
+ // We don't want to call gtk_spin_button_update(),
+ // that would immediately replace whatever the user has typed in.
+ gdouble scale = -1;
+ const gchar *text = gtk_entry_get_text(GTK_ENTRY(spin));
+ if (*text) {
+ gchar *end = NULL;
+ gdouble value = g_strtod(text, &end);
+ if (!*end)
+ scale = value / 100.;
+ }
+
+ struct zoom_ask_context *data = user_data;
+ GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(spin));
+ if (scale <= 0) {
+ gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING);
+ gtk_label_set_text(GTK_LABEL(data->result_left), "—");
+ gtk_label_set_text(GTK_LABEL(data->result_right), "—");
+ } else {
+ gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING);
+ gchar *left = g_strdup_printf("%.0f", data->dimensions.width * scale);
+ gchar *right = g_strdup_printf("%.0f", data->dimensions.height * scale);
+ gtk_label_set_text(GTK_LABEL(data->result_left), left);
+ gtk_label_set_text(GTK_LABEL(data->result_right), right);
+ g_free(left);
+ g_free(right);
+ }
+}
+
+static void
+zoom_ask(FivView *self)
+{
+ GtkWidget *dialog = gtk_dialog_new_with_buttons("Set zoom level",
+ get_toplevel(GTK_WIDGET(self)),
+ GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL |
+ GTK_DIALOG_USE_HEADER_BAR,
+ "_OK", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL);
+
+ Dimensions dimensions = get_surface_dimensions(self);
+ gchar *original_width = g_strdup_printf("%.0f", dimensions.width);
+ gchar *original_height = g_strdup_printf("%.0f", dimensions.height);
+ GtkWidget *original_left = gtk_label_new(original_width);
+ GtkWidget *original_middle = gtk_label_new("×");
+ GtkWidget *original_right = gtk_label_new(original_height);
+ g_free(original_width);
+ g_free(original_height);
+ gtk_label_set_xalign(GTK_LABEL(original_left), 1.);
+ gtk_label_set_xalign(GTK_LABEL(original_right), 0.);
+
+ GtkWidget *original_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_box_pack_start(
+ GTK_BOX(original_box), original_left, TRUE, TRUE, 0);
+ gtk_box_pack_start(
+ GTK_BOX(original_box), original_middle, FALSE, FALSE, 0);
+ gtk_box_pack_start(
+ GTK_BOX(original_box), original_right, TRUE, TRUE, 0);
+
+ // FIXME: This widget's behaviour is absolutely miserable.
+ // For example, we would like to be flexible with decimal spaces.
+ GtkAdjustment *adjustment = gtk_adjustment_new(
+ self->scale * 100, 0., 100000., 1., 10., 0.);
+ GtkWidget *spin = gtk_spin_button_new(adjustment, 1., 2);
+ gtk_spin_button_set_update_policy(
+ GTK_SPIN_BUTTON(spin), GTK_UPDATE_IF_VALID);
+ gtk_entry_set_activates_default(GTK_ENTRY(spin), TRUE);
+
+ GtkWidget *zoom_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ GtkWidget *zoom_label = gtk_label_new_with_mnemonic("_Zoom:");
+ gtk_label_set_mnemonic_widget(GTK_LABEL(zoom_label), spin);
+ gtk_box_pack_start(GTK_BOX(zoom_box), zoom_label, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(zoom_box), spin, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(zoom_box), gtk_label_new("%"), FALSE, FALSE, 0);
+
+ GtkWidget *result_left = gtk_label_new(NULL);
+ GtkWidget *result_middle = gtk_label_new("×");
+ GtkWidget *result_right = gtk_label_new(NULL);
+ gtk_label_set_xalign(GTK_LABEL(result_left), 1.);
+ gtk_label_set_xalign(GTK_LABEL(result_right), 0.);
+
+ GtkSizeGroup *group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
+ gtk_size_group_add_widget(group, original_left);
+ gtk_size_group_add_widget(group, original_right);
+ gtk_size_group_add_widget(group, result_left);
+ gtk_size_group_add_widget(group, result_right);
+
+ GtkWidget *result_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_box_pack_start(GTK_BOX(result_box), result_left, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(result_box), result_middle, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(result_box), result_right, TRUE, TRUE, 0);
+
+ struct zoom_ask_context data = { result_left, result_right, dimensions };
+ g_signal_connect(spin, "changed",
+ G_CALLBACK(on_zoom_ask_spin_changed), &data);
+ on_zoom_ask_spin_changed(GTK_SPIN_BUTTON(spin), &data);
+
+ GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+ g_object_set(content, "margin", 12, NULL);
+ gtk_box_set_spacing(GTK_BOX(content), 6);
+ gtk_container_add(GTK_CONTAINER(content), original_box);
+ gtk_container_add(GTK_CONTAINER(content), zoom_box);
+ gtk_container_add(GTK_CONTAINER(content), result_box);
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
+ gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
+ gtk_widget_show_all(dialog);
+
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));
+ if (value > 0)
+ set_scale(self, value / 100., NULL);
+ }
+ gtk_widget_destroy(dialog);
+ g_object_unref(group);
+}
+
static void
copy(FivView *self)
{
@@ -1775,6 +1944,17 @@ fiv_view_init(FivView *self)
G_CALLBACK(on_drag_update), self);
g_signal_connect(drag, "drag-end",
G_CALLBACK(on_drag_end), self);
+
+ GtkGesture *zoom = gtk_gesture_zoom_new(GTK_WIDGET(self));
+ gtk_event_controller_set_propagation_phase(
+ GTK_EVENT_CONTROLLER(zoom), GTK_PHASE_BUBBLE);
+ g_object_set_data_full(
+ G_OBJECT(self), "fiv-view-zoom-gesture", zoom, g_object_unref);
+
+ g_signal_connect(zoom, "begin",
+ G_CALLBACK(on_zoom_begin), self);
+ g_signal_connect(zoom, "scale-changed",
+ G_CALLBACK(on_zoom_scale_changed), self);
}
// --- Public interface --------------------------------------------------------
@@ -2021,6 +2201,8 @@ fiv_view_command(FivView *self, FivViewCommand command)
set_scale(self, self->scale / SCALE_STEP, NULL);
break; case FIV_VIEW_COMMAND_ZOOM_1:
set_scale(self, 1.0, NULL);
+ break; case FIV_VIEW_COMMAND_ZOOM_ASK:
+ zoom_ask(self);
break; case FIV_VIEW_COMMAND_FIT_WIDTH:
set_scale_to_fit_width(self);
break; case FIV_VIEW_COMMAND_FIT_HEIGHT: