
Not sure why but adding a handler to the "expose-event" signal of GimpDisplayShell (similarly to how we do it in master branch on "draw") just didn't work. But it works on the already existing signal handling on the canvas instead (which actually is not a bad deal, as we also remove the coordinates translation so maybe we should test this on `master` too). Note: why we are backporting all this logics to gimp-2-10 is because changes in macOS BigSur broke the selection's marching ants the same way they broke on Wayland and it was confirmed this fix worked for BigSur as well, at least on the dev builds. It is unnecessary to backport for Wayland (because GIMP 2.10 is based on GTK2 which anyway works only through XWayland, hence doesn't have the issue), we do it only for macOS BigSur (and further). Well at least the fix will hopefully work on the stable branch, because I cannot test myself. See issue #5952.
505 lines
16 KiB
C
505 lines
16 KiB
C
/* GIMP - The GNU Image Manipulation Program
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <gegl.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include "display-types.h"
|
|
|
|
#include "config/gimpdisplayconfig.h"
|
|
|
|
#include "core/gimp.h"
|
|
#include "core/gimp-cairo.h"
|
|
#include "core/gimpboundary.h"
|
|
#include "core/gimpchannel.h"
|
|
#include "core/gimpimage.h"
|
|
|
|
#include "gimpdisplay.h"
|
|
#include "gimpdisplayshell.h"
|
|
#include "gimpdisplayshell-appearance.h"
|
|
#include "gimpdisplayshell-draw.h"
|
|
#include "gimpdisplayshell-expose.h"
|
|
#include "gimpdisplayshell-selection.h"
|
|
#include "gimpdisplayshell-transform.h"
|
|
|
|
|
|
struct _Selection
|
|
{
|
|
GimpDisplayShell *shell; /* shell that owns the selection */
|
|
|
|
GimpSegment *segs_in; /* gdk segments of area boundary */
|
|
gint n_segs_in; /* number of segments in segs_in */
|
|
|
|
GimpSegment *segs_out; /* gdk segments of area boundary */
|
|
gint n_segs_out; /* number of segments in segs_out */
|
|
|
|
guint index; /* index of current stipple pattern */
|
|
gint paused; /* count of pause requests */
|
|
gboolean shell_visible; /* visility of the display shell */
|
|
gboolean show_selection; /* is the selection visible? */
|
|
guint timeout; /* timer for successive draws */
|
|
cairo_pattern_t *segs_in_mask; /* cache for rendered segments */
|
|
};
|
|
|
|
|
|
/* local function prototypes */
|
|
|
|
static void selection_start (Selection *selection);
|
|
static void selection_stop (Selection *selection);
|
|
|
|
static void selection_undraw (Selection *selection);
|
|
|
|
static void selection_render_mask (Selection *selection);
|
|
|
|
static void selection_zoom_segs (Selection *selection,
|
|
const GimpBoundSeg *src_segs,
|
|
GimpSegment *dest_segs,
|
|
gint n_segs,
|
|
gint canvas_offset_x,
|
|
gint canvas_offset_y);
|
|
static void selection_generate_segs (Selection *selection);
|
|
static void selection_free_segs (Selection *selection);
|
|
|
|
static gboolean selection_timeout (Selection *selection);
|
|
|
|
static gboolean selection_window_state_event (GtkWidget *shell,
|
|
GdkEventWindowState *event,
|
|
Selection *selection);
|
|
static gboolean selection_visibility_notify_event (GtkWidget *shell,
|
|
GdkEventVisibility *event,
|
|
Selection *selection);
|
|
|
|
|
|
/* public functions */
|
|
|
|
void
|
|
gimp_display_shell_selection_init (GimpDisplayShell *shell)
|
|
{
|
|
Selection *selection;
|
|
|
|
g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));
|
|
g_return_if_fail (shell->selection == NULL);
|
|
|
|
selection = g_slice_new0 (Selection);
|
|
|
|
selection->shell = shell;
|
|
selection->shell_visible = TRUE;
|
|
selection->show_selection = gimp_display_shell_get_show_selection (shell);
|
|
|
|
shell->selection = selection;
|
|
shell->selection_update = g_get_monotonic_time ();
|
|
|
|
g_signal_connect (shell, "window-state-event",
|
|
G_CALLBACK (selection_window_state_event),
|
|
selection);
|
|
g_signal_connect (shell, "visibility-notify-event",
|
|
G_CALLBACK (selection_visibility_notify_event),
|
|
selection);
|
|
}
|
|
|
|
void
|
|
gimp_display_shell_selection_free (GimpDisplayShell *shell)
|
|
{
|
|
Selection *selection;
|
|
|
|
g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));
|
|
g_return_if_fail (shell->selection != NULL);
|
|
|
|
selection = shell->selection;
|
|
|
|
selection_stop (selection);
|
|
|
|
g_signal_handlers_disconnect_by_func (shell,
|
|
selection_window_state_event,
|
|
selection);
|
|
g_signal_handlers_disconnect_by_func (shell,
|
|
selection_visibility_notify_event,
|
|
selection);
|
|
|
|
selection_free_segs (selection);
|
|
|
|
g_slice_free (Selection, selection);
|
|
|
|
shell->selection = NULL;
|
|
}
|
|
|
|
void
|
|
gimp_display_shell_selection_undraw (GimpDisplayShell *shell)
|
|
{
|
|
g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));
|
|
g_return_if_fail (shell->selection != NULL);
|
|
|
|
if (gimp_display_get_image (shell->display))
|
|
{
|
|
selection_undraw (shell->selection);
|
|
}
|
|
else
|
|
{
|
|
selection_stop (shell->selection);
|
|
selection_free_segs (shell->selection);
|
|
}
|
|
}
|
|
|
|
void
|
|
gimp_display_shell_selection_restart (GimpDisplayShell *shell)
|
|
{
|
|
g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));
|
|
g_return_if_fail (shell->selection != NULL);
|
|
|
|
if (gimp_display_get_image (shell->display))
|
|
{
|
|
selection_start (shell->selection);
|
|
}
|
|
}
|
|
|
|
void
|
|
gimp_display_shell_selection_pause (GimpDisplayShell *shell)
|
|
{
|
|
g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));
|
|
g_return_if_fail (shell->selection != NULL);
|
|
|
|
if (gimp_display_get_image (shell->display))
|
|
{
|
|
if (shell->selection->paused == 0)
|
|
selection_stop (shell->selection);
|
|
|
|
shell->selection->paused++;
|
|
}
|
|
}
|
|
|
|
void
|
|
gimp_display_shell_selection_resume (GimpDisplayShell *shell)
|
|
{
|
|
g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));
|
|
g_return_if_fail (shell->selection != NULL);
|
|
|
|
if (gimp_display_get_image (shell->display))
|
|
{
|
|
shell->selection->paused--;
|
|
|
|
if (shell->selection->paused == 0)
|
|
selection_start (shell->selection);
|
|
}
|
|
}
|
|
|
|
void
|
|
gimp_display_shell_selection_set_show (GimpDisplayShell *shell,
|
|
gboolean show)
|
|
{
|
|
g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));
|
|
g_return_if_fail (shell->selection != NULL);
|
|
|
|
if (gimp_display_get_image (shell->display))
|
|
{
|
|
Selection *selection = shell->selection;
|
|
|
|
if (show != selection->show_selection)
|
|
{
|
|
selection_undraw (selection);
|
|
|
|
selection->show_selection = show;
|
|
|
|
selection_start (selection);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* private functions */
|
|
|
|
static void
|
|
selection_start (Selection *selection)
|
|
{
|
|
selection_stop (selection);
|
|
|
|
/* If this selection is paused, do not start it */
|
|
if (selection->paused == 0 &&
|
|
gimp_display_get_image (selection->shell->display) &&
|
|
selection->show_selection)
|
|
{
|
|
/* Draw the ants once */
|
|
selection_timeout (selection);
|
|
|
|
if (selection->segs_in && selection->shell_visible)
|
|
{
|
|
GimpDisplayConfig *config = selection->shell->display->config;
|
|
|
|
selection->timeout = g_timeout_add_full (G_PRIORITY_DEFAULT_IDLE,
|
|
config->marching_ants_speed,
|
|
(GSourceFunc) selection_timeout,
|
|
selection, NULL);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
selection_stop (Selection *selection)
|
|
{
|
|
if (selection->timeout)
|
|
{
|
|
g_source_remove (selection->timeout);
|
|
selection->timeout = 0;
|
|
}
|
|
}
|
|
|
|
void
|
|
gimp_display_shell_selection_draw (GimpDisplayShell *shell,
|
|
cairo_t *cr)
|
|
{
|
|
if (gimp_display_get_image (shell->display) &&
|
|
shell->selection && shell->selection->show_selection)
|
|
{
|
|
GimpDisplayConfig *config = shell->display->config;
|
|
gint64 time = g_get_monotonic_time ();
|
|
|
|
if ((time - shell->selection_update) / 1000 > config->marching_ants_speed &&
|
|
shell->selection->paused == 0)
|
|
{
|
|
shell->selection_update = time;
|
|
shell->selection->index++;
|
|
}
|
|
|
|
selection_generate_segs (shell->selection);
|
|
|
|
if (shell->selection->segs_in)
|
|
{
|
|
gimp_display_shell_draw_selection_in (shell->selection->shell, cr,
|
|
shell->selection->segs_in_mask,
|
|
shell->selection->index % 8);
|
|
}
|
|
|
|
if (shell->selection->segs_out)
|
|
{
|
|
if (shell->selection->shell->rotate_transform)
|
|
cairo_transform (cr, shell->selection->shell->rotate_transform);
|
|
|
|
gimp_display_shell_draw_selection_out (shell->selection->shell, cr,
|
|
shell->selection->segs_out,
|
|
shell->selection->n_segs_out);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
selection_undraw (Selection *selection)
|
|
{
|
|
gint x, y, w, h;
|
|
|
|
selection_stop (selection);
|
|
|
|
if (gimp_display_shell_mask_bounds (selection->shell, &x, &y, &w, &h))
|
|
{
|
|
/* expose will restart the selection */
|
|
gimp_display_shell_expose_area (selection->shell, x, y, w, h);
|
|
}
|
|
else
|
|
{
|
|
selection_start (selection);
|
|
}
|
|
}
|
|
|
|
static void
|
|
selection_render_mask (Selection *selection)
|
|
{
|
|
GdkWindow *window;
|
|
cairo_surface_t *surface;
|
|
cairo_t *cr;
|
|
|
|
window = gtk_widget_get_window (GTK_WIDGET (selection->shell));
|
|
surface = gdk_window_create_similar_surface (window, CAIRO_CONTENT_ALPHA,
|
|
gdk_window_get_width (window),
|
|
gdk_window_get_height (window));
|
|
cr = cairo_create (surface);
|
|
|
|
cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);
|
|
cairo_set_line_width (cr, 1.0);
|
|
|
|
if (selection->shell->rotate_transform)
|
|
cairo_transform (cr, selection->shell->rotate_transform);
|
|
|
|
gimp_cairo_segments (cr,
|
|
selection->segs_in,
|
|
selection->n_segs_in);
|
|
cairo_stroke (cr);
|
|
|
|
selection->segs_in_mask = cairo_pattern_create_for_surface (surface);
|
|
|
|
cairo_destroy (cr);
|
|
cairo_surface_destroy (surface);
|
|
}
|
|
|
|
static void
|
|
selection_zoom_segs (Selection *selection,
|
|
const GimpBoundSeg *src_segs,
|
|
GimpSegment *dest_segs,
|
|
gint n_segs,
|
|
gint canvas_offset_x,
|
|
gint canvas_offset_y)
|
|
{
|
|
const gint xclamp = selection->shell->disp_width + 1;
|
|
const gint yclamp = selection->shell->disp_height + 1;
|
|
gint i;
|
|
|
|
gimp_display_shell_zoom_segments (selection->shell,
|
|
src_segs, dest_segs, n_segs,
|
|
0.0, 0.0);
|
|
|
|
for (i = 0; i < n_segs; i++)
|
|
{
|
|
if (! selection->shell->rotate_transform)
|
|
{
|
|
dest_segs[i].x1 = CLAMP (dest_segs[i].x1, -1, xclamp) + canvas_offset_x;
|
|
dest_segs[i].y1 = CLAMP (dest_segs[i].y1, -1, yclamp) + canvas_offset_y;
|
|
|
|
dest_segs[i].x2 = CLAMP (dest_segs[i].x2, -1, xclamp) + canvas_offset_x;
|
|
dest_segs[i].y2 = CLAMP (dest_segs[i].y2, -1, yclamp) + canvas_offset_y;
|
|
}
|
|
|
|
/* If this segment is a closing segment && the segments lie inside
|
|
* the region, OR if this is an opening segment and the segments
|
|
* lie outside the region...
|
|
* we need to transform it by one display pixel
|
|
*/
|
|
if (! src_segs[i].open)
|
|
{
|
|
/* If it is vertical */
|
|
if (dest_segs[i].x1 == dest_segs[i].x2)
|
|
{
|
|
dest_segs[i].x1 -= 1;
|
|
dest_segs[i].x2 -= 1;
|
|
}
|
|
else
|
|
{
|
|
dest_segs[i].y1 -= 1;
|
|
dest_segs[i].y2 -= 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
selection_generate_segs (Selection *selection)
|
|
{
|
|
GimpImage *image = gimp_display_get_image (selection->shell->display);
|
|
const GimpBoundSeg *segs_in;
|
|
const GimpBoundSeg *segs_out;
|
|
gint canvas_offset_x = 0;
|
|
gint canvas_offset_y = 0;
|
|
|
|
selection_free_segs (selection);
|
|
|
|
/* Ask the image for the boundary of its selected region...
|
|
* Then transform that information into a new buffer of GimpSegments
|
|
*/
|
|
gimp_channel_boundary (gimp_image_get_mask (image),
|
|
&segs_in, &segs_out,
|
|
&selection->n_segs_in, &selection->n_segs_out,
|
|
0, 0, 0, 0);
|
|
|
|
if (selection->n_segs_in)
|
|
{
|
|
selection->segs_in = g_new (GimpSegment, selection->n_segs_in);
|
|
selection_zoom_segs (selection, segs_in,
|
|
selection->segs_in, selection->n_segs_in,
|
|
canvas_offset_x, canvas_offset_y);
|
|
|
|
selection_render_mask (selection);
|
|
}
|
|
|
|
/* Possible secondary boundary representation */
|
|
if (selection->n_segs_out)
|
|
{
|
|
selection->segs_out = g_new (GimpSegment, selection->n_segs_out);
|
|
selection_zoom_segs (selection, segs_out,
|
|
selection->segs_out, selection->n_segs_out,
|
|
canvas_offset_x, canvas_offset_y);
|
|
}
|
|
}
|
|
|
|
static void
|
|
selection_free_segs (Selection *selection)
|
|
{
|
|
g_clear_pointer (&selection->segs_in, g_free);
|
|
selection->n_segs_in = 0;
|
|
|
|
g_clear_pointer (&selection->segs_out, g_free);
|
|
selection->n_segs_out = 0;
|
|
|
|
g_clear_pointer (&selection->segs_in_mask, cairo_pattern_destroy);
|
|
}
|
|
|
|
static gboolean
|
|
selection_timeout (Selection *selection)
|
|
{
|
|
GimpDisplayConfig *config = selection->shell->display->config;
|
|
gint64 time = g_get_monotonic_time ();
|
|
|
|
if ((time - selection->shell->selection_update) / 1000 > config->marching_ants_speed)
|
|
{
|
|
GdkWindow *window;
|
|
|
|
window = gtk_widget_get_window (GTK_WIDGET (selection->shell));
|
|
|
|
gtk_widget_queue_draw_area (GTK_WIDGET (selection->shell),
|
|
0, 0,
|
|
gdk_window_get_width (window),
|
|
gdk_window_get_height (window));
|
|
}
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
}
|
|
|
|
static void
|
|
selection_set_shell_visible (Selection *selection,
|
|
gboolean shell_visible)
|
|
{
|
|
if (selection->shell_visible != shell_visible)
|
|
{
|
|
selection->shell_visible = shell_visible;
|
|
|
|
if (shell_visible)
|
|
selection_start (selection);
|
|
else
|
|
selection_stop (selection);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
selection_window_state_event (GtkWidget *shell,
|
|
GdkEventWindowState *event,
|
|
Selection *selection)
|
|
{
|
|
selection_set_shell_visible (selection,
|
|
(event->new_window_state & (GDK_WINDOW_STATE_WITHDRAWN |
|
|
GDK_WINDOW_STATE_ICONIFIED)) == 0);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
selection_visibility_notify_event (GtkWidget *shell,
|
|
GdkEventVisibility *event,
|
|
Selection *selection)
|
|
{
|
|
selection_set_shell_visible (selection,
|
|
event->state != GDK_VISIBILITY_FULLY_OBSCURED);
|
|
|
|
return FALSE;
|
|
}
|