PIKApp/app/display/pikadisplayshell-selection.c

515 lines
16 KiB
C

/* 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 <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <gegl.h>
#include <gtk/gtk.h>
#include "display-types.h"
#include "config/pikadisplayconfig.h"
#include "core/pika.h"
#include "core/pika-cairo.h"
#include "core/pikaboundary.h"
#include "core/pikachannel.h"
#include "core/pikaimage.h"
#include "pikadisplay.h"
#include "pikadisplayshell.h"
#include "pikadisplayshell-appearance.h"
#include "pikadisplayshell-draw.h"
#include "pikadisplayshell-expose.h"
#include "pikadisplayshell-selection.h"
#include "pikadisplayshell-transform.h"
struct _Selection
{
PikaDisplayShell *shell; /* shell that owns the selection */
PikaSegment *segs_in; /* segments of area boundary */
gint n_segs_in; /* number of segments in segs_in */
PikaSegment *segs_out; /* 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 PikaBoundSeg *src_segs,
PikaSegment *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
pika_display_shell_selection_init (PikaDisplayShell *shell)
{
Selection *selection;
g_return_if_fail (PIKA_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 = pika_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
pika_display_shell_selection_free (PikaDisplayShell *shell)
{
Selection *selection;
g_return_if_fail (PIKA_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
pika_display_shell_selection_undraw (PikaDisplayShell *shell)
{
g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
g_return_if_fail (shell->selection != NULL);
if (pika_display_get_image (shell->display))
{
selection_undraw (shell->selection);
}
else
{
selection_stop (shell->selection);
selection_free_segs (shell->selection);
}
}
void
pika_display_shell_selection_restart (PikaDisplayShell *shell)
{
g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
g_return_if_fail (shell->selection != NULL);
if (pika_display_get_image (shell->display))
{
selection_start (shell->selection);
}
}
void
pika_display_shell_selection_pause (PikaDisplayShell *shell)
{
g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
g_return_if_fail (shell->selection != NULL);
if (pika_display_get_image (shell->display))
{
if (shell->selection->paused == 0)
selection_stop (shell->selection);
shell->selection->paused++;
}
}
void
pika_display_shell_selection_resume (PikaDisplayShell *shell)
{
g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
g_return_if_fail (shell->selection != NULL);
if (pika_display_get_image (shell->display))
{
shell->selection->paused--;
if (shell->selection->paused == 0)
selection_start (shell->selection);
}
}
void
pika_display_shell_selection_set_show (PikaDisplayShell *shell,
gboolean show)
{
g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
g_return_if_fail (shell->selection != NULL);
if (pika_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 &&
pika_display_get_image (selection->shell->display) &&
selection->show_selection)
{
/* Draw the ants once */
selection_timeout (selection);
if (selection->segs_in && selection->shell_visible)
{
PikaDisplayConfig *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
pika_display_shell_selection_draw (PikaDisplayShell *shell,
cairo_t *cr)
{
if (pika_display_get_image (shell->display) &&
shell->selection && shell->selection->show_selection)
{
PikaDisplayConfig *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)
{
pika_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);
pika_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 (pika_display_shell_mask_bounds (selection->shell, &x, &y, &w, &h))
{
/* expose will restart the selection */
pika_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);
pika_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 PikaBoundSeg *src_segs,
PikaSegment *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;
pika_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)
{
PikaImage *image = pika_display_get_image (selection->shell->display);
const PikaBoundSeg *segs_in;
const PikaBoundSeg *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 PikaSegments
*/
pika_channel_boundary (pika_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 (PikaSegment, 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 (PikaSegment, 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)
{
PikaDisplayConfig *config = selection->shell->display->config;
gint64 time = g_get_monotonic_time ();
if ((time - selection->shell->selection_update) / 1000 > config->marching_ants_speed)
{
GdkWindow *window;
cairo_rectangle_int_t rect;
cairo_region_t *region;
window = gtk_widget_get_window (GTK_WIDGET (selection->shell));
rect.x = 0;
rect.y = 0;
rect.width = gdk_window_get_width (window);
rect.height = gdk_window_get_height (window);
region = cairo_region_create_rectangle (&rect);
gtk_widget_queue_draw_region (GTK_WIDGET (selection->shell), region);
cairo_region_destroy (region);
}
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;
}