diff options
Diffstat (limited to 'fiv-view.c')
| -rw-r--r-- | fiv-view.c | 220 |
1 files changed, 201 insertions, 19 deletions
@@ -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: |
