/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include #include #include #include "libpikabase/pikabase.h" #include "libpikamath/pikamath.h" #include "libpikawidgets/pikawidgets.h" #include "display-types.h" #include "config/pikaguiconfig.h" #include "core/pika.h" #include "core/pikaimage.h" #include "pikadisplay.h" #include "pikadisplayshell.h" #include "pikadisplayshell-expose.h" #include "pikadisplayshell-render.h" #include "pikadisplayshell-rotate.h" #include "pikadisplayshell-scale.h" #include "pikadisplayshell-scroll.h" #include "pikadisplayshell-transform.h" #include "pikaimagewindow.h" #define SCALE_TIMEOUT 2 #define SCALE_EPSILON 0.0001 #define ALMOST_CENTERED_THRESHOLD 2 #define SCALE_EQUALS(a,b) (fabs ((a) - (b)) < SCALE_EPSILON) /* local function prototypes */ static void pika_display_shell_scale_get_screen_resolution (PikaDisplayShell *shell, gdouble *xres, gdouble *yres); static void pika_display_shell_scale_get_image_size_for_scale (PikaDisplayShell *shell, gdouble scale, gint *w, gint *h); static void pika_display_shell_calculate_scale_x_and_y (PikaDisplayShell *shell, gdouble scale, gdouble *scale_x, gdouble *scale_y); static void pika_display_shell_scale_to (PikaDisplayShell *shell, gdouble scale, gdouble viewport_x, gdouble viewport_y); static void pika_display_shell_scale_fit_or_fill (PikaDisplayShell *shell, gboolean fill); static gboolean pika_display_shell_scale_image_starts_to_fit (PikaDisplayShell *shell, gdouble new_scale, gdouble current_scale, gboolean *horizontally, gboolean *vertically); static gboolean pika_display_shell_scale_image_stops_to_fit (PikaDisplayShell *shell, gdouble new_scale, gdouble current_scale, gboolean *horizontally, gboolean *vertically); static gboolean pika_display_shell_scale_viewport_coord_almost_centered (PikaDisplayShell *shell, gint x, gint y, gboolean *horizontally, gboolean *vertically); static void pika_display_shell_scale_get_image_center_viewport (PikaDisplayShell *shell, gint *image_center_x, gint *image_center_y); static void pika_display_shell_scale_get_zoom_focus (PikaDisplayShell *shell, gdouble new_scale, gdouble current_scale, gdouble *x, gdouble *y, PikaZoomFocus zoom_focus); /* public functions */ /** * pika_display_shell_scale_revert: * @shell: the #PikaDisplayShell * * Reverts the display to the previously used scale. If no previous * scale exist, then the call does nothing. * * Returns: %TRUE if the scale was reverted, otherwise %FALSE. **/ gboolean pika_display_shell_scale_revert (PikaDisplayShell *shell) { g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE); /* don't bother if no scale has been set */ if (shell->last_scale < SCALE_EPSILON) return FALSE; shell->last_scale_time = 0; pika_display_shell_scale_by_values (shell, shell->last_scale, shell->last_offset_x, shell->last_offset_y, FALSE); /* don't resize the window */ return TRUE; } /** * pika_display_shell_scale_can_revert: * @shell: the #PikaDisplayShell * * Returns: %TRUE if a previous display scale exists, otherwise %FALSE. **/ gboolean pika_display_shell_scale_can_revert (PikaDisplayShell *shell) { g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE); return (shell->last_scale > SCALE_EPSILON); } /** * pika_display_shell_scale_save_revert_values: * @shell: * * Handle the updating of the Revert Zoom variables. **/ void pika_display_shell_scale_save_revert_values (PikaDisplayShell *shell) { guint now; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); now = time (NULL); if (now - shell->last_scale_time >= SCALE_TIMEOUT) { shell->last_scale = pika_zoom_model_get_factor (shell->zoom); shell->last_offset_x = shell->offset_x; shell->last_offset_y = shell->offset_y; } shell->last_scale_time = now; } /** * pika_display_shell_scale_set_dot_for_dot: * @shell: the #PikaDisplayShell * @dot_for_dot: whether "Dot for Dot" should be enabled * * If @dot_for_dot is set to %TRUE then the "Dot for Dot" mode (where image and * screen pixels are of the same size) is activated. Dually, the mode is * disabled if @dot_for_dot is %FALSE. **/ void pika_display_shell_scale_set_dot_for_dot (PikaDisplayShell *shell, gboolean dot_for_dot) { g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); if (dot_for_dot != shell->dot_for_dot) { PikaDisplayConfig *config = shell->display->config; gboolean resize_window; /* Resize windows only in multi-window mode */ resize_window = (config->resize_windows_on_zoom && ! PIKA_GUI_CONFIG (config)->single_window_mode); /* freeze the active tool */ pika_display_shell_pause (shell); shell->dot_for_dot = dot_for_dot; pika_display_shell_scale_update (shell); pika_display_shell_scale_resize (shell, resize_window, FALSE); /* re-enable the active tool */ pika_display_shell_resume (shell); } } /** * pika_display_shell_scale_get_image_size: * @shell: * @w: * @h: * * Gets the size of the rendered image after it has been scaled. * **/ void pika_display_shell_scale_get_image_size (PikaDisplayShell *shell, gint *w, gint *h) { g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); pika_display_shell_scale_get_image_size_for_scale (shell, pika_zoom_model_get_factor (shell->zoom), w, h); } /** * pika_display_shell_scale_get_image_bounds: * @shell: * @x: * @y: * @w: * @h: * * Gets the screen-space boudning box of the image, after it has * been transformed (i.e., scaled, rotated, and scrolled). **/ void pika_display_shell_scale_get_image_bounds (PikaDisplayShell *shell, gint *x, gint *y, gint *w, gint *h) { PikaImage *image; gdouble x1, y1; gdouble x2, y2; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); image = pika_display_get_image (shell->display); pika_display_shell_transform_bounds (shell, 0, 0, pika_image_get_width (image), pika_image_get_height (image), &x1, &y1, &x2, &y2); x1 = ceil (x1); y1 = ceil (y1); x2 = floor (x2); y2 = floor (y2); if (x) *x = x1 + shell->offset_x; if (y) *y = y1 + shell->offset_y; if (w) *w = x2 - x1; if (h) *h = y2 - y1; } /** * pika_display_shell_scale_get_image_unrotated_bounds: * @shell: * @x: * @y: * @w: * @h: * * Gets the screen-space boudning box of the image, after it has * been scaled and scrolled, but before it has been rotated. **/ void pika_display_shell_scale_get_image_unrotated_bounds (PikaDisplayShell *shell, gint *x, gint *y, gint *w, gint *h) { PikaImage *image; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); image = pika_display_get_image (shell->display); if (x) *x = -shell->offset_x; if (y) *y = -shell->offset_y; if (w) *w = floor (pika_image_get_width (image) * shell->scale_x); if (h) *h = floor (pika_image_get_height (image) * shell->scale_y); } /** * pika_display_shell_scale_get_image_bounding_box: * @shell: * @x: * @y: * @w: * @h: * * Gets the screen-space boudning box of the image content, after it has * been transformed (i.e., scaled, rotated, and scrolled). **/ void pika_display_shell_scale_get_image_bounding_box (PikaDisplayShell *shell, gint *x, gint *y, gint *w, gint *h) { GeglRectangle bounding_box; gdouble x1, y1; gdouble x2, y2; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); bounding_box = pika_display_shell_get_bounding_box (shell); pika_display_shell_transform_bounds (shell, bounding_box.x, bounding_box.y, bounding_box.x + bounding_box.width, bounding_box.y + bounding_box.height, &x1, &y1, &x2, &y2); if (! shell->show_all) { x1 = ceil (x1); y1 = ceil (y1); x2 = floor (x2); y2 = floor (y2); } else { x1 = floor (x1); y1 = floor (y1); x2 = ceil (x2); y2 = ceil (y2); } if (x) *x = x1 + shell->offset_x; if (y) *y = y1 + shell->offset_y; if (w) *w = x2 - x1; if (h) *h = y2 - y1; } /** * pika_display_shell_scale_get_image_unrotated_bounding_box: * @shell: * @x: * @y: * @w: * @h: * * Gets the screen-space boudning box of the image content, after it has * been scaled and scrolled, but before it has been rotated. **/ void pika_display_shell_scale_get_image_unrotated_bounding_box (PikaDisplayShell *shell, gint *x, gint *y, gint *w, gint *h) { GeglRectangle bounding_box; gdouble x1, y1; gdouble x2, y2; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); bounding_box = pika_display_shell_get_bounding_box (shell); x1 = bounding_box.x * shell->scale_x - shell->offset_x; y1 = bounding_box.y * shell->scale_y - shell->offset_y; x2 = (bounding_box.x + bounding_box.width) * shell->scale_x - shell->offset_x; y2 = (bounding_box.y + bounding_box.height) * shell->scale_y - shell->offset_y; if (! shell->show_all) { x1 = ceil (x1); y1 = ceil (y1); x2 = floor (x2); y2 = floor (y2); } else { x1 = floor (x1); y1 = floor (y1); x2 = ceil (x2); y2 = ceil (y2); } if (x) *x = x1; if (y) *y = y1; if (w) *w = x2 - x1; if (h) *h = y2 - y1; } /** * pika_display_shell_scale_image_is_within_viewport: * @shell: * * Returns: %TRUE if the (scaled) image is smaller than and within the * viewport. **/ gboolean pika_display_shell_scale_image_is_within_viewport (PikaDisplayShell *shell, gboolean *horizontally, gboolean *vertically) { gboolean horizontally_dummy, vertically_dummy; g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE); if (! horizontally) horizontally = &horizontally_dummy; if (! vertically) vertically = &vertically_dummy; if (! pika_display_shell_get_infinite_canvas (shell)) { gint sx, sy; gint sw, sh; pika_display_shell_scale_get_image_bounding_box (shell, &sx, &sy, &sw, &sh); sx -= shell->offset_x; sy -= shell->offset_y; *horizontally = sx >= 0 && sx + sw <= shell->disp_width; *vertically = sy >= 0 && sy + sh <= shell->disp_height; } else { *horizontally = FALSE; *vertically = FALSE; } return *vertically && *horizontally; } /* We used to calculate the scale factor in the SCALEFACTOR_X() and * SCALEFACTOR_Y() macros. But since these are rather frequently * called and the values rarely change, we now store them in the * shell and call this function whenever they need to be recalculated. */ void pika_display_shell_scale_update (PikaDisplayShell *shell) { PikaImage *image; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); image = pika_display_get_image (shell->display); if (image) { pika_display_shell_calculate_scale_x_and_y (shell, pika_zoom_model_get_factor (shell->zoom), &shell->scale_x, &shell->scale_y); } else { shell->scale_x = 1.0; shell->scale_y = 1.0; } } /** * pika_display_shell_scale: * @shell: the #PikaDisplayShell * @zoom_type: whether to zoom in, out or to a specific scale * @scale: ignored unless @zoom_type == %PIKA_ZOOM_TO * * This function figures out the context of the zoom and behaves * appropriately thereafter. * **/ void pika_display_shell_scale (PikaDisplayShell *shell, PikaZoomType zoom_type, gdouble new_scale, PikaZoomFocus zoom_focus) { PikaDisplayConfig *config; gdouble current_scale; gdouble delta = 0.0; gboolean resize_window; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); g_return_if_fail (shell->canvas != NULL); current_scale = pika_zoom_model_get_factor (shell->zoom); if (zoom_type == PIKA_ZOOM_SMOOTH) delta = -new_scale; if (zoom_type == PIKA_ZOOM_PINCH) delta = new_scale; if (zoom_type != PIKA_ZOOM_TO) new_scale = pika_zoom_model_zoom_step (zoom_type, current_scale, delta); if (SCALE_EQUALS (new_scale, current_scale)) return; config = shell->display->config; /* Resize windows only in multi-window mode */ resize_window = (config->resize_windows_on_zoom && ! PIKA_GUI_CONFIG (config)->single_window_mode); if (resize_window) { /* If the window is resized on zoom, simply do the zoom and get * things rolling */ pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, new_scale); pika_display_shell_scale_resize (shell, TRUE, FALSE); /* XXX The @zoom_focus policy is clearly not working in this code * path. This should be fixed. */ } else { gdouble x, y; gint image_center_x; gint image_center_y; pika_display_shell_scale_get_zoom_focus (shell, new_scale, current_scale, &x, &y, zoom_focus); pika_display_shell_scale_get_image_center_viewport (shell, &image_center_x, &image_center_y); pika_display_shell_scale_to (shell, new_scale, x, y); /* skip centering magic if pointer focus was requested */ if (zoom_focus != PIKA_ZOOM_FOCUS_POINTER) { gboolean starts_fitting_horiz; gboolean starts_fitting_vert; gboolean zoom_focus_almost_centered_horiz; gboolean zoom_focus_almost_centered_vert; gboolean image_center_almost_centered_horiz; gboolean image_center_almost_centered_vert; /* If an image axis started to fit due to zooming out or if * the focus point is as good as in the center, center on * that axis */ pika_display_shell_scale_image_starts_to_fit (shell, new_scale, current_scale, &starts_fitting_horiz, &starts_fitting_vert); pika_display_shell_scale_viewport_coord_almost_centered (shell, x, y, &zoom_focus_almost_centered_horiz, &zoom_focus_almost_centered_vert); pika_display_shell_scale_viewport_coord_almost_centered (shell, image_center_x, image_center_y, &image_center_almost_centered_horiz, &image_center_almost_centered_vert); pika_display_shell_scroll_center_image (shell, starts_fitting_horiz || (zoom_focus_almost_centered_horiz && image_center_almost_centered_horiz), starts_fitting_vert || (zoom_focus_almost_centered_vert && image_center_almost_centered_vert)); } } } /** * pika_display_shell_scale_to_rectangle: * @shell: the #PikaDisplayShell * @zoom_type: whether to zoom in or out * @x: retangle's x in image coordinates * @y: retangle's y in image coordinates * @width: retangle's width in image coordinates * @height: retangle's height in image coordinates * @resize_window: whether the display window should be resized * * Scales and scrolls to a specific image rectangle **/ void pika_display_shell_scale_to_rectangle (PikaDisplayShell *shell, PikaZoomType zoom_type, gdouble x, gdouble y, gdouble width, gdouble height, gboolean resize_window) { gdouble current_scale; gdouble new_scale; gdouble factor = 1.0; gint offset_x = 0; gint offset_y = 0; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); pika_display_shell_transform_bounds (shell, x, y, x + width, y + height, &x, &y, &width, &height); /* Convert scrolled (x1, y1, x2, y2) to unscrolled (x, y, width, height). */ width -= x; height -= y; x += shell->offset_x; y += shell->offset_y; width = MAX (1.0, width); height = MAX (1.0, height); current_scale = pika_zoom_model_get_factor (shell->zoom); switch (zoom_type) { case PIKA_ZOOM_IN: factor = MIN ((shell->disp_width / width), (shell->disp_height / height)); break; case PIKA_ZOOM_OUT: factor = MAX ((width / shell->disp_width), (height / shell->disp_height)); break; default: g_return_if_reached (); break; } new_scale = current_scale * factor; switch (zoom_type) { case PIKA_ZOOM_IN: /* move the center of the rectangle to the center of the * viewport: * * new_offset = center of rectangle in new scale screen coords * including offset * - * center of viewport in screen coords without * offset */ offset_x = RINT (factor * (x + width / 2.0) - (shell->disp_width / 2)); offset_y = RINT (factor * (y + height / 2.0) - (shell->disp_height / 2)); break; case PIKA_ZOOM_OUT: /* move the center of the viewport to the center of the * rectangle: * * new_offset = center of viewport in new scale screen coords * including offset * - * center of rectangle in screen coords without * offset */ offset_x = RINT (factor * (shell->offset_x + shell->disp_width / 2) - ((x + width / 2.0) - shell->offset_x)); offset_y = RINT (factor * (shell->offset_y + shell->disp_height / 2) - ((y + height / 2.0) - shell->offset_y)); break; default: break; } if (new_scale != current_scale || offset_x != shell->offset_x || offset_y != shell->offset_y) { pika_display_shell_scale_by_values (shell, new_scale, offset_x, offset_y, resize_window); } } /** * pika_display_shell_scale_fit_in: * @shell: the #PikaDisplayShell * * Sets the scale such that the entire image precisely fits in the * display area. **/ void pika_display_shell_scale_fit_in (PikaDisplayShell *shell) { g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); pika_display_shell_scale_fit_or_fill (shell, /* fill = */ FALSE); } /** * pika_display_shell_scale_fill: * @shell: the #PikaDisplayShell * * Sets the scale such that the entire display area is precisely * filled by the image. **/ void pika_display_shell_scale_fill (PikaDisplayShell *shell) { g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); pika_display_shell_scale_fit_or_fill (shell, /* fill = */ TRUE); } /** * pika_display_shell_scale_by_values: * @shell: the #PikaDisplayShell * @scale: the new scale * @offset_x: the new X offset * @offset_y: the new Y offset * @resize_window: whether the display window should be resized * * Directly sets the image scale and image offsets used by the display. If * @resize_window is %TRUE then the display window is resized to better * accommodate the image, see pika_display_shell_shrink_wrap(). **/ void pika_display_shell_scale_by_values (PikaDisplayShell *shell, gdouble scale, gint offset_x, gint offset_y, gboolean resize_window) { g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); /* Abort early if the values are all setup already. We don't * want to inadvertently resize the window (bug #164281). */ if (SCALE_EQUALS (pika_zoom_model_get_factor (shell->zoom), scale) && shell->offset_x == offset_x && shell->offset_y == offset_y) return; pika_display_shell_scale_save_revert_values (shell); /* freeze the active tool */ pika_display_shell_pause (shell); pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, scale); shell->offset_x = offset_x; shell->offset_y = offset_y; pika_display_shell_rotate_update_transform (shell); pika_display_shell_scale_resize (shell, resize_window, FALSE); /* re-enable the active tool */ pika_display_shell_resume (shell); } void pika_display_shell_scale_drag (PikaDisplayShell *shell, gdouble start_x, gdouble start_y, gdouble delta_x, gdouble delta_y) { gdouble scale; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); scale = pika_zoom_model_get_factor (shell->zoom); if (delta_y != 0.0) { PikaDisplayConfig *config = shell->display->config; gdouble speed = config->drag_zoom_speed * 0.01; pika_display_shell_push_zoom_focus_pointer_pos (shell, start_x, start_y); if (config->drag_zoom_mode == PROP_DRAG_ZOOM_MODE_DISTANCE) { pika_display_shell_scale (shell, PIKA_ZOOM_TO, scale * exp (0.005 * speed * delta_y), PIKA_ZOOM_FOCUS_POINTER); } else if (delta_y > 0.0) /* drag_zoom_mode == PROP_DRAG_ZOOM_MODE_DURATION */ { pika_display_shell_scale (shell, PIKA_ZOOM_TO, scale * (1 + 0.1 * speed), PIKA_ZOOM_FOCUS_POINTER); } else /* delta_y < 0.0 */ { pika_display_shell_scale (shell, PIKA_ZOOM_TO, scale * (1 - 0.1 * speed), PIKA_ZOOM_FOCUS_POINTER); } if (shell->zoom_focus_point) { /* In case we hit one of the cases when the focus pointer * position was unused. */ g_slice_free (GdkPoint, shell->zoom_focus_point); shell->zoom_focus_point = NULL; } } } /** * pika_display_shell_scale_shrink_wrap: * @shell: the #PikaDisplayShell * * Convenience function with the same functionality as * pika_display_shell_scale_resize(@shell, TRUE, grow_only). **/ void pika_display_shell_scale_shrink_wrap (PikaDisplayShell *shell, gboolean grow_only) { g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); pika_display_shell_scale_resize (shell, TRUE, grow_only); } /** * pika_display_shell_scale_resize: * @shell: the #PikaDisplayShell * @resize_window: whether the display window should be resized * @grow_only: whether shrinking of the window is allowed or not * * Function commonly called after a change in display scale to make the changes * visible to the user. If @resize_window is %TRUE then the display window is * resized to accommodate the display image as per * pika_display_shell_shrink_wrap(). **/ void pika_display_shell_scale_resize (PikaDisplayShell *shell, gboolean resize_window, gboolean grow_only) { g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); /* freeze the active tool */ pika_display_shell_pause (shell); if (resize_window) { PikaImageWindow *window = pika_display_shell_get_window (shell); if (window && pika_image_window_get_active_shell (window) == shell) { pika_image_window_shrink_wrap (window, grow_only); } } pika_display_shell_scroll_clamp_and_update (shell); pika_display_shell_scaled (shell); pika_display_shell_expose_full (shell); pika_display_shell_render_invalidate_full (shell); /* re-enable the active tool */ pika_display_shell_resume (shell); } void pika_display_shell_set_initial_scale (PikaDisplayShell *shell, gdouble scale, gint *display_width, gint *display_height) { PikaImage *image; GdkRectangle workarea; gint image_width; gint image_height; gint monitor_width; gint monitor_height; gint shell_width; gint shell_height; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); image = pika_display_get_image (shell->display); gdk_monitor_get_workarea (shell->initial_monitor, &workarea); image_width = pika_image_get_width (image); image_height = pika_image_get_height (image); monitor_width = workarea.width * 0.75; monitor_height = workarea.height * 0.75; /* We need to zoom before we use SCALE[XY] */ pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, scale); shell_width = SCALEX (shell, image_width); shell_height = SCALEY (shell, image_height); if (shell->display->config->initial_zoom_to_fit) { /* Limit to the size of the monitor... */ if (shell_width > monitor_width || shell_height > monitor_height) { gdouble new_scale; gdouble current = pika_zoom_model_get_factor (shell->zoom); new_scale = current * MIN (((gdouble) monitor_height) / shell_height, ((gdouble) monitor_width) / shell_width); new_scale = pika_zoom_model_zoom_step (PIKA_ZOOM_OUT, new_scale, 0.0); /* Since zooming out might skip a zoom step we zoom in * again and test if we are small enough. */ pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, pika_zoom_model_zoom_step (PIKA_ZOOM_IN, new_scale, 0.0)); if (SCALEX (shell, image_width) > monitor_width || SCALEY (shell, image_height) > monitor_height) pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, new_scale); shell_width = SCALEX (shell, image_width); shell_height = SCALEY (shell, image_height); } } else { /* Set up size like above, but do not zoom to fit. Useful when * working on large images. */ shell_width = MIN (shell_width, monitor_width); shell_height = MIN (shell_height, monitor_height); } if (display_width) *display_width = shell_width; if (display_height) *display_height = shell_height; } /** * pika_display_shell_get_rotated_scale: * @shell: the #PikaDisplayShell * @scale_x: horizontal scale output * @scale_y: vertical scale output * * Returns the screen space horizontal and vertical scaling * factors, taking rotation into account. **/ void pika_display_shell_get_rotated_scale (PikaDisplayShell *shell, gdouble *scale_x, gdouble *scale_y) { g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); if (shell->rotate_angle == 0.0 || shell->scale_x == shell->scale_y) { if (scale_x) *scale_x = shell->scale_x; if (scale_y) *scale_y = shell->scale_y; } else { gdouble a = G_PI * shell->rotate_angle / 180.0; gdouble cos_a = cos (a); gdouble sin_a = sin (a); if (scale_x) *scale_x = 1.0 / sqrt (SQR (cos_a / shell->scale_x) + SQR (sin_a / shell->scale_y)); if (scale_y) *scale_y = 1.0 / sqrt (SQR (cos_a / shell->scale_y) + SQR (sin_a / shell->scale_x)); } } /** * pika_display_shell_push_zoom_focus_pointer_pos: * @shell: * @x: * @y: * * When the zoom focus mechanism asks for the pointer the next time, * use @x and @y. * * It was primarily created for unit testing (see commit 7e3898da093). * Therefore it should not be used by our code. When it is, we should * make sure that @shell->zoom_focus_point has been properly used and * freed, if we don't want it to leak. * Just calling pika_display_shell_scale() is not enough as there are * currently some code paths where the values is not used. **/ void pika_display_shell_push_zoom_focus_pointer_pos (PikaDisplayShell *shell, gint x, gint y) { GdkPoint *point = g_slice_new (GdkPoint); point->x = x; point->y = y; g_slice_free (GdkPoint, shell->zoom_focus_point); shell->zoom_focus_point = point; } /* private functions */ static void pika_display_shell_scale_get_screen_resolution (PikaDisplayShell *shell, gdouble *xres, gdouble *yres) { gdouble x, y; if (shell->dot_for_dot) { pika_image_get_resolution (pika_display_get_image (shell->display), &x, &y); } else { x = shell->monitor_xres; y = shell->monitor_yres; } if (xres) *xres = x; if (yres) *yres = y; } /** * pika_display_shell_scale_get_image_size_for_scale: * @shell: * @scale: * @w: * @h: * **/ static void pika_display_shell_scale_get_image_size_for_scale (PikaDisplayShell *shell, gdouble scale, gint *w, gint *h) { PikaImage *image = pika_display_get_image (shell->display); gdouble scale_x; gdouble scale_y; pika_display_shell_calculate_scale_x_and_y (shell, scale, &scale_x, &scale_y); if (w) *w = scale_x * pika_image_get_width (image); if (h) *h = scale_y * pika_image_get_height (image); } /** * pika_display_shell_calculate_scale_x_and_y: * @shell: * @scale: * @scale_x: * @scale_y: * **/ static void pika_display_shell_calculate_scale_x_and_y (PikaDisplayShell *shell, gdouble scale, gdouble *scale_x, gdouble *scale_y) { PikaImage *image = pika_display_get_image (shell->display); gdouble xres; gdouble yres; gdouble screen_xres; gdouble screen_yres; pika_image_get_resolution (image, &xres, &yres); pika_display_shell_scale_get_screen_resolution (shell, &screen_xres, &screen_yres); if (scale_x) *scale_x = scale * screen_xres / xres; if (scale_y) *scale_y = scale * screen_yres / yres; } /** * pika_display_shell_scale_to: * @shell: * @scale: * @viewport_x: * @viewport_y: * * Zooms. The display offsets are adjusted so that the point specified * by @x and @y doesn't change it's position on screen. **/ static void pika_display_shell_scale_to (PikaDisplayShell *shell, gdouble scale, gdouble viewport_x, gdouble viewport_y) { gdouble image_x, image_y; gdouble new_viewport_x, new_viewport_y; g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); if (! shell->display) return; /* freeze the active tool */ pika_display_shell_pause (shell); pika_display_shell_untransform_xy_f (shell, viewport_x, viewport_y, &image_x, &image_y); /* Note that we never come here if we need to resize_windows_on_zoom */ pika_display_shell_scale_by_values (shell, scale, shell->offset_x, shell->offset_y, FALSE); pika_display_shell_transform_xy_f (shell, image_x, image_y, &new_viewport_x, &new_viewport_y); pika_display_shell_scroll (shell, new_viewport_x - viewport_x, new_viewport_y - viewport_y); /* re-enable the active tool */ pika_display_shell_resume (shell); } /** * pika_display_shell_scale_fit_or_fill: * @shell: the #PikaDisplayShell * @fill: whether to scale the image to fill the viewport, * or fit inside the viewport * * A common implementation for pika_display_shell_scale_{fit_in,fill}(). **/ static void pika_display_shell_scale_fit_or_fill (PikaDisplayShell *shell, gboolean fill) { GeglRectangle bounding_box; gdouble image_x; gdouble image_y; gdouble image_width; gdouble image_height; gdouble current_scale; gdouble zoom_factor; if (! pika_display_shell_get_infinite_canvas (shell)) { PikaImage *image = pika_display_get_image (shell->display); bounding_box.x = 0; bounding_box.y = 0; bounding_box.width = pika_image_get_width (image); bounding_box.height = pika_image_get_height (image); } else { bounding_box = pika_display_shell_get_bounding_box (shell); } pika_display_shell_transform_bounds (shell, bounding_box.x, bounding_box.y, bounding_box.x + bounding_box.width, bounding_box.y + bounding_box.height, &image_x, &image_y, &image_width, &image_height); image_width -= image_x; image_height -= image_y; current_scale = pika_zoom_model_get_factor (shell->zoom); if (fill) { zoom_factor = MAX (shell->disp_width / image_width, shell->disp_height / image_height); } else { zoom_factor = MIN (shell->disp_width / image_width, shell->disp_height / image_height); } pika_display_shell_scale (shell, PIKA_ZOOM_TO, zoom_factor * current_scale, PIKA_ZOOM_FOCUS_BEST_GUESS); pika_display_shell_scroll_center_content (shell, TRUE, TRUE); } static gboolean pika_display_shell_scale_image_starts_to_fit (PikaDisplayShell *shell, gdouble new_scale, gdouble current_scale, gboolean *horizontally, gboolean *vertically) { gboolean vertically_dummy; gboolean horizontally_dummy; if (! vertically) vertically = &vertically_dummy; if (! horizontally) horizontally = &horizontally_dummy; /* The image can only start to fit if we zoom out */ if (new_scale > current_scale || pika_display_shell_get_infinite_canvas (shell)) { *vertically = FALSE; *horizontally = FALSE; } else { gint current_scale_width; gint current_scale_height; gint new_scale_width; gint new_scale_height; pika_display_shell_scale_get_image_size_for_scale (shell, current_scale, ¤t_scale_width, ¤t_scale_height); pika_display_shell_scale_get_image_size_for_scale (shell, new_scale, &new_scale_width, &new_scale_height); *vertically = (current_scale_width > shell->disp_width && new_scale_width <= shell->disp_width); *horizontally = (current_scale_height > shell->disp_height && new_scale_height <= shell->disp_height); } return *vertically && *horizontally; } static gboolean pika_display_shell_scale_image_stops_to_fit (PikaDisplayShell *shell, gdouble new_scale, gdouble current_scale, gboolean *horizontally, gboolean *vertically) { return pika_display_shell_scale_image_starts_to_fit (shell, current_scale, new_scale, horizontally, vertically); } /** * pika_display_shell_scale_viewport_coord_almost_centered: * @shell: * @x: * @y: * @horizontally: * @vertically: * **/ static gboolean pika_display_shell_scale_viewport_coord_almost_centered (PikaDisplayShell *shell, gint x, gint y, gboolean *horizontally, gboolean *vertically) { gboolean local_horizontally = FALSE; gboolean local_vertically = FALSE; gint center_x = shell->disp_width / 2; gint center_y = shell->disp_height / 2; if (! pika_display_shell_get_infinite_canvas (shell)) { local_horizontally = (x > center_x - ALMOST_CENTERED_THRESHOLD && x < center_x + ALMOST_CENTERED_THRESHOLD); local_vertically = (y > center_y - ALMOST_CENTERED_THRESHOLD && y < center_y + ALMOST_CENTERED_THRESHOLD); } if (horizontally) *horizontally = local_horizontally; if (vertically) *vertically = local_vertically; return local_horizontally && local_vertically; } static void pika_display_shell_scale_get_image_center_viewport (PikaDisplayShell *shell, gint *image_center_x, gint *image_center_y) { gint sw, sh; pika_display_shell_scale_get_image_size (shell, &sw, &sh); if (image_center_x) *image_center_x = -shell->offset_x + sw / 2; if (image_center_y) *image_center_y = -shell->offset_y + sh / 2; } /** * pika_display_shell_scale_get_zoom_focus: * @shell: * @new_scale: * @x: * @y: * * Calculates the viewport coordinate to focus on when zooming * independently for each axis. **/ static void pika_display_shell_scale_get_zoom_focus (PikaDisplayShell *shell, gdouble new_scale, gdouble current_scale, gdouble *x, gdouble *y, PikaZoomFocus zoom_focus) { GtkWidget *window = GTK_WIDGET (pika_display_shell_get_window (shell)); GdkEvent *event; gint image_center_x; gint image_center_y; gint other_x; gint other_y; /* Calculate stops-to-fit focus point */ pika_display_shell_scale_get_image_center_viewport (shell, &image_center_x, &image_center_y); /* Calculate other focus point, default is the canvas center */ other_x = shell->disp_width / 2; other_y = shell->disp_height / 2; /* Center on the mouse position instead of the display center if * one of the following conditions are fulfilled and pointer is * within the canvas: * * (1) there's no current event (the action was triggered by an * input controller) * (2) the event originates from the canvas (a scroll event) * (3) the event originates from the window (a key press event) * * Basically the only situation where we don't want to center on * mouse position is if the action is being called from a menu. */ event = gtk_get_current_event (); if (! event || gtk_get_event_widget (event) == shell->canvas || gtk_get_event_widget (event) == window) { gint canvas_pointer_x; gint canvas_pointer_y; if (shell->zoom_focus_point) { canvas_pointer_x = shell->zoom_focus_point->x; canvas_pointer_y = shell->zoom_focus_point->y; g_slice_free (GdkPoint, shell->zoom_focus_point); shell->zoom_focus_point = NULL; } else { GdkDisplay *display = gtk_widget_get_display (shell->canvas); GdkSeat *seat = gdk_display_get_default_seat (display); gdk_window_get_device_position (gtk_widget_get_window (shell->canvas), gdk_seat_get_pointer (seat), &canvas_pointer_x, &canvas_pointer_y, NULL); } if (canvas_pointer_x >= 0 && canvas_pointer_y >= 0 && canvas_pointer_x < shell->disp_width && canvas_pointer_y < shell->disp_height) { other_x = canvas_pointer_x; other_y = canvas_pointer_y; } } if (zoom_focus == PIKA_ZOOM_FOCUS_RETAIN_CENTERING_ELSE_BEST_GUESS) { if (pika_display_shell_scale_viewport_coord_almost_centered (shell, image_center_x, image_center_y, NULL, NULL)) { zoom_focus = PIKA_ZOOM_FOCUS_IMAGE_CENTER; } else { zoom_focus = PIKA_ZOOM_FOCUS_BEST_GUESS; } } switch (zoom_focus) { case PIKA_ZOOM_FOCUS_POINTER: *x = other_x; *y = other_y; break; case PIKA_ZOOM_FOCUS_IMAGE_CENTER: *x = image_center_x; *y = image_center_y; break; case PIKA_ZOOM_FOCUS_BEST_GUESS: default: { gboolean within_horizontally, within_vertically; gboolean stops_horizontally, stops_vertically; pika_display_shell_scale_image_is_within_viewport (shell, &within_horizontally, &within_vertically); pika_display_shell_scale_image_stops_to_fit (shell, new_scale, current_scale, &stops_horizontally, &stops_vertically); *x = within_horizontally && ! stops_horizontally ? image_center_x : other_x; *y = within_vertically && ! stops_vertically ? image_center_y : other_y; } break; } }