/* 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 "libpikawidgets/pikawidgets.h" #include "tools-types.h" #include "core/pikachannel.h" #include "core/pikaerror.h" #include "core/pikaimage.h" #include "core/pikaimage-pick-item.h" #include "core/pikaimage-undo.h" #include "core/pikapickable.h" #include "core/pikaundostack.h" #include "display/pikadisplay.h" #include "display/pikadisplayshell-appearance.h" #include "widgets/pikawidgets-utils.h" #include "pikaeditselectiontool.h" #include "pikaselectiontool.h" #include "pikaselectionoptions.h" #include "pikatoolcontrol.h" #include "pikatools-utils.h" #include "pika-intl.h" static void pika_selection_tool_control (PikaTool *tool, PikaToolAction action, PikaDisplay *display); static void pika_selection_tool_modifier_key (PikaTool *tool, GdkModifierType key, gboolean press, GdkModifierType state, PikaDisplay *display); static void pika_selection_tool_oper_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, gboolean proximity, PikaDisplay *display); static void pika_selection_tool_cursor_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, PikaDisplay *display); static gboolean pika_selection_tool_real_have_selection (PikaSelectionTool *sel_tool, PikaDisplay *display); static void pika_selection_tool_commit (PikaSelectionTool *sel_tool); static void pika_selection_tool_halt (PikaSelectionTool *sel_tool, PikaDisplay *display); static gboolean pika_selection_tool_check (PikaSelectionTool *sel_tool, PikaDisplay *display, GError **error); static gboolean pika_selection_tool_have_selection (PikaSelectionTool *sel_tool, PikaDisplay *display); G_DEFINE_TYPE (PikaSelectionTool, pika_selection_tool, PIKA_TYPE_DRAW_TOOL) #define parent_class pika_selection_tool_parent_class static void pika_selection_tool_class_init (PikaSelectionToolClass *klass) { PikaToolClass *tool_class = PIKA_TOOL_CLASS (klass); tool_class->control = pika_selection_tool_control; tool_class->modifier_key = pika_selection_tool_modifier_key; tool_class->key_press = pika_edit_selection_tool_key_press; tool_class->oper_update = pika_selection_tool_oper_update; tool_class->cursor_update = pika_selection_tool_cursor_update; klass->have_selection = pika_selection_tool_real_have_selection; } static void pika_selection_tool_init (PikaSelectionTool *selection_tool) { selection_tool->function = SELECTION_SELECT; selection_tool->saved_operation = PIKA_CHANNEL_OP_REPLACE; selection_tool->saved_show_selection = FALSE; selection_tool->undo = NULL; selection_tool->redo = NULL; selection_tool->idle_id = 0; selection_tool->allow_move = TRUE; } static void pika_selection_tool_control (PikaTool *tool, PikaToolAction action, PikaDisplay *display) { PikaSelectionTool *selection_tool = PIKA_SELECTION_TOOL (tool); switch (action) { case PIKA_TOOL_ACTION_PAUSE: case PIKA_TOOL_ACTION_RESUME: break; case PIKA_TOOL_ACTION_HALT: pika_selection_tool_halt (selection_tool, display); break; case PIKA_TOOL_ACTION_COMMIT: pika_selection_tool_commit (selection_tool); break; } PIKA_TOOL_CLASS (parent_class)->control (tool, action, display); } static void pika_selection_tool_modifier_key (PikaTool *tool, GdkModifierType key, gboolean press, GdkModifierType state, PikaDisplay *display) { PikaSelectionTool *selection_tool = PIKA_SELECTION_TOOL (tool); PikaSelectionOptions *options = PIKA_SELECTION_TOOL_GET_OPTIONS (tool); GdkModifierType extend_mask; GdkModifierType modify_mask; extend_mask = pika_get_extend_selection_mask (); modify_mask = pika_get_modify_selection_mask (); if (key == extend_mask || key == modify_mask || key == GDK_MOD1_MASK) { PikaChannelOps button_op = options->operation; state &= extend_mask | modify_mask | GDK_MOD1_MASK; if (press) { if (key == state || /* PikaPolygonSelectTool may mask-out part of the state, which * can cause the wrong mode to be restored on release if we don't * init saved_operation here. * * see issue #4992. */ ! state) { /* first modifier pressed */ selection_tool->saved_operation = options->operation; } } else { if (! state) { /* last modifier released */ button_op = selection_tool->saved_operation; } } if (state & GDK_MOD1_MASK) { /* if alt is down, pretend that neither * shift nor control are down */ button_op = selection_tool->saved_operation; } else if (state & (extend_mask | modify_mask)) { /* else get the operation from the modifier state, but only * if there is actually a modifier pressed, so we don't * override the "last modifier released" assignment above */ button_op = pika_modifiers_to_channel_op (state); } if (button_op != options->operation) { g_object_set (options, "operation", button_op, NULL); } } } static void pika_selection_tool_oper_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, gboolean proximity, PikaDisplay *display) { PikaSelectionTool *selection_tool = PIKA_SELECTION_TOOL (tool); PikaSelectionOptions *options = PIKA_SELECTION_TOOL_GET_OPTIONS (tool); PikaImage *image; GList *drawables; PikaLayer *layer; PikaLayer *floating_sel; GdkModifierType extend_mask; GdkModifierType modify_mask; gboolean have_selection; gboolean move_layer = FALSE; gboolean move_floating_sel = FALSE; image = pika_display_get_image (display); drawables = pika_image_get_selected_drawables (image); layer = pika_image_pick_layer (image, coords->x, coords->y, NULL); floating_sel = pika_image_get_floating_selection (image); extend_mask = pika_get_extend_selection_mask (); modify_mask = pika_get_modify_selection_mask (); have_selection = pika_selection_tool_have_selection (selection_tool, display); if (drawables) { if (floating_sel) { if (layer == floating_sel) move_floating_sel = TRUE; } else if (have_selection) { GList *iter; for (iter = drawables; iter; iter = iter->next) { if (pika_item_mask_intersect (PIKA_ITEM (iter->data), NULL, NULL, NULL, NULL)) { move_layer = TRUE; break; } } } g_list_free (drawables); } selection_tool->function = SELECTION_SELECT; if (selection_tool->allow_move && (state & GDK_MOD1_MASK) && (state & modify_mask) && move_layer) { /* move the selection */ selection_tool->function = SELECTION_MOVE; } else if (selection_tool->allow_move && (state & GDK_MOD1_MASK) && (state & extend_mask) && move_layer) { /* move a copy of the selection */ selection_tool->function = SELECTION_MOVE_COPY; } else if (selection_tool->allow_move && (state & GDK_MOD1_MASK) && have_selection) { /* move the selection mask */ selection_tool->function = SELECTION_MOVE_MASK; } else if (selection_tool->allow_move && ! (state & (extend_mask | modify_mask)) && move_floating_sel) { /* move the selection */ selection_tool->function = SELECTION_MOVE; } else if ((state & modify_mask) || (state & extend_mask)) { /* select */ selection_tool->function = SELECTION_SELECT; } else if (floating_sel) { /* anchor the selection */ selection_tool->function = SELECTION_ANCHOR; } pika_tool_pop_status (tool, display); if (proximity) { const gchar *status = NULL; gboolean free_status = FALSE; GdkModifierType modifiers = (extend_mask | modify_mask); if (have_selection) modifiers |= GDK_MOD1_MASK; switch (selection_tool->function) { case SELECTION_SELECT: switch (options->operation) { case PIKA_CHANNEL_OP_REPLACE: if (have_selection) { status = pika_suggest_modifiers (_("Click-Drag to replace the " "current selection"), modifiers & ~state, NULL, NULL, NULL); free_status = TRUE; } else { status = _("Click-Drag to create a new selection"); } break; case PIKA_CHANNEL_OP_ADD: status = pika_suggest_modifiers (_("Click-Drag to add to the " "current selection"), modifiers & ~(state | extend_mask), NULL, NULL, NULL); free_status = TRUE; break; case PIKA_CHANNEL_OP_SUBTRACT: status = pika_suggest_modifiers (_("Click-Drag to subtract from the " "current selection"), modifiers & ~(state | modify_mask), NULL, NULL, NULL); free_status = TRUE; break; case PIKA_CHANNEL_OP_INTERSECT: status = pika_suggest_modifiers (_("Click-Drag to intersect with " "the current selection"), modifiers & ~state, NULL, NULL, NULL); free_status = TRUE; break; } break; case SELECTION_MOVE_MASK: status = pika_suggest_modifiers (_("Click-Drag to move the " "selection mask"), modifiers & ~state, NULL, NULL, NULL); free_status = TRUE; break; case SELECTION_MOVE: status = _("Click-Drag to move the selected pixels"); break; case SELECTION_MOVE_COPY: status = _("Click-Drag to move a copy of the selected pixels"); break; case SELECTION_ANCHOR: status = _("Click to anchor the floating selection"); break; default: g_return_if_reached (); } if (status) pika_tool_push_status (tool, display, "%s", status); if (free_status) g_free ((gchar *) status); } } static void pika_selection_tool_cursor_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, PikaDisplay *display) { PikaSelectionTool *selection_tool = PIKA_SELECTION_TOOL (tool); PikaSelectionOptions *options; PikaToolCursorType tool_cursor; PikaCursorModifier modifier; options = PIKA_SELECTION_TOOL_GET_OPTIONS (tool); tool_cursor = pika_tool_control_get_tool_cursor (tool->control); modifier = PIKA_CURSOR_MODIFIER_NONE; switch (selection_tool->function) { case SELECTION_SELECT: switch (options->operation) { case PIKA_CHANNEL_OP_REPLACE: break; case PIKA_CHANNEL_OP_ADD: modifier = PIKA_CURSOR_MODIFIER_PLUS; break; case PIKA_CHANNEL_OP_SUBTRACT: modifier = PIKA_CURSOR_MODIFIER_MINUS; break; case PIKA_CHANNEL_OP_INTERSECT: modifier = PIKA_CURSOR_MODIFIER_INTERSECT; break; } break; case SELECTION_MOVE_MASK: modifier = PIKA_CURSOR_MODIFIER_MOVE; break; case SELECTION_MOVE: case SELECTION_MOVE_COPY: tool_cursor = PIKA_TOOL_CURSOR_MOVE; break; case SELECTION_ANCHOR: modifier = PIKA_CURSOR_MODIFIER_ANCHOR; break; } /* our subclass might have set a BAD modifier, in which case we leave it * there, since it's more important than what we have to say. */ if (pika_tool_control_get_cursor_modifier (tool->control) == PIKA_CURSOR_MODIFIER_BAD || ! pika_selection_tool_check (selection_tool, display, NULL)) { modifier = PIKA_CURSOR_MODIFIER_BAD; } pika_tool_set_cursor (tool, display, pika_tool_control_get_cursor (tool->control), tool_cursor, modifier); } static gboolean pika_selection_tool_real_have_selection (PikaSelectionTool *sel_tool, PikaDisplay *display) { PikaImage *image = pika_display_get_image (display); PikaChannel *selection = pika_image_get_mask (image); return ! pika_channel_is_empty (selection); } static void pika_selection_tool_commit (PikaSelectionTool *sel_tool) { /* make sure pika_selection_tool_halt() doesn't undo the change, if any */ g_clear_weak_pointer (&sel_tool->undo); } static void pika_selection_tool_halt (PikaSelectionTool *sel_tool, PikaDisplay *display) { g_warn_if_fail (sel_tool->change_count == 0); if (display) { PikaTool *tool = PIKA_TOOL (sel_tool); PikaImage *image = pika_display_get_image (display); PikaUndoStack *undo_stack = pika_image_get_undo_stack (image); PikaUndo *undo = pika_undo_stack_peek (undo_stack); /* if we have an existing selection in the current display, then * we have already "executed", and need to undo at this point, * unless the user has done something in the meantime */ if (undo && sel_tool->undo == undo) { /* prevent this change from halting the tool */ pika_tool_control_push_preserve (tool->control, TRUE); pika_image_undo (image); pika_image_flush (image); pika_tool_control_pop_preserve (tool->control); } /* reset the automatic undo/redo mechanism */ g_clear_weak_pointer (&sel_tool->undo); g_clear_weak_pointer (&sel_tool->redo); } } static gboolean pika_selection_tool_check (PikaSelectionTool *sel_tool, PikaDisplay *display, GError **error) { PikaSelectionOptions *options = PIKA_SELECTION_TOOL_GET_OPTIONS (sel_tool); PikaImage *image = pika_display_get_image (display); switch (sel_tool->function) { case SELECTION_SELECT: switch (options->operation) { case PIKA_CHANNEL_OP_ADD: case PIKA_CHANNEL_OP_REPLACE: break; case PIKA_CHANNEL_OP_SUBTRACT: if (! pika_item_bounds (PIKA_ITEM (pika_image_get_mask (image)), NULL, NULL, NULL, NULL)) { g_set_error (error, PIKA_ERROR, PIKA_FAILED, _("Cannot subtract from an empty selection.")); return FALSE; } break; case PIKA_CHANNEL_OP_INTERSECT: if (! pika_item_bounds (PIKA_ITEM (pika_image_get_mask (image)), NULL, NULL, NULL, NULL)) { g_set_error (error, PIKA_ERROR, PIKA_FAILED, _("Cannot intersect with an empty selection.")); return FALSE; } break; } break; case SELECTION_MOVE: case SELECTION_MOVE_COPY: { GList *drawables = pika_image_get_selected_drawables (image); PikaItem *locked_item = NULL; GList *iter; for (iter = drawables; iter; iter = iter->next) { if (pika_viewable_get_children (iter->data)) { g_set_error (error, PIKA_ERROR, PIKA_FAILED, _("Cannot modify the pixels of layer groups.")); g_list_free (drawables); return FALSE; } else if (pika_item_is_content_locked (iter->data, &locked_item)) { g_set_error (error, PIKA_ERROR, PIKA_FAILED, _("A selected item's pixels are locked.")); if (error) pika_tools_blink_lock_box (display->pika, locked_item); g_list_free (drawables); return FALSE; } } g_list_free (drawables); } break; default: break; } return TRUE; } static gboolean pika_selection_tool_have_selection (PikaSelectionTool *sel_tool, PikaDisplay *display) { return PIKA_SELECTION_TOOL_GET_CLASS (sel_tool)->have_selection (sel_tool, display); } /* public functions */ gboolean pika_selection_tool_start_edit (PikaSelectionTool *sel_tool, PikaDisplay *display, const PikaCoords *coords) { PikaTool *tool; PikaSelectionOptions *options; GError *error = NULL; g_return_val_if_fail (PIKA_IS_SELECTION_TOOL (sel_tool), FALSE); g_return_val_if_fail (PIKA_IS_DISPLAY (display), FALSE); g_return_val_if_fail (coords != NULL, FALSE); tool = PIKA_TOOL (sel_tool); options = PIKA_SELECTION_TOOL_GET_OPTIONS (sel_tool); g_return_val_if_fail (pika_tool_control_is_active (tool->control) == FALSE, FALSE); if (! pika_selection_tool_check (sel_tool, display, &error)) { pika_tool_message_literal (tool, display, error->message); pika_tools_show_tool_options (display->pika); pika_widget_blink (options->mode_box); g_clear_error (&error); return TRUE; } switch (sel_tool->function) { case SELECTION_MOVE_MASK: pika_edit_selection_tool_start (tool, display, coords, PIKA_TRANSLATE_MODE_MASK, FALSE); return TRUE; case SELECTION_MOVE: case SELECTION_MOVE_COPY: { PikaTranslateMode edit_mode; pika_tool_control (tool, PIKA_TOOL_ACTION_COMMIT, display); if (sel_tool->function == SELECTION_MOVE) edit_mode = PIKA_TRANSLATE_MODE_MASK_TO_LAYER; else edit_mode = PIKA_TRANSLATE_MODE_MASK_COPY_TO_LAYER; pika_edit_selection_tool_start (tool, display, coords, edit_mode, FALSE); return TRUE; } default: break; } return FALSE; } static gboolean pika_selection_tool_idle (PikaSelectionTool *sel_tool) { PikaTool *tool = PIKA_TOOL (sel_tool); PikaDisplayShell *shell = pika_display_get_shell (tool->display); pika_display_shell_set_show_selection (shell, FALSE); sel_tool->idle_id = 0; return G_SOURCE_REMOVE; } void pika_selection_tool_start_change (PikaSelectionTool *sel_tool, gboolean create, PikaChannelOps operation) { PikaTool *tool; PikaDisplayShell *shell; PikaImage *image; PikaUndoStack *undo_stack; g_return_if_fail (PIKA_IS_SELECTION_TOOL (sel_tool)); tool = PIKA_TOOL (sel_tool); g_return_if_fail (tool->display != NULL); if (sel_tool->change_count++ > 0) return; shell = pika_display_get_shell (tool->display); image = pika_display_get_image (tool->display); undo_stack = pika_image_get_undo_stack (image); sel_tool->saved_show_selection = pika_display_shell_get_show_selection (shell); if (create) { g_clear_weak_pointer (&sel_tool->undo); } else { PikaUndoStack *redo_stack = pika_image_get_redo_stack (image); PikaUndo *undo; undo = pika_undo_stack_peek (undo_stack); if (undo && undo == sel_tool->undo) { /* prevent this change from halting the tool */ pika_tool_control_push_preserve (tool->control, TRUE); pika_image_undo (image); pika_tool_control_pop_preserve (tool->control); g_clear_weak_pointer (&sel_tool->undo); /* we will need to redo if the user cancels or executes */ g_set_weak_pointer (&sel_tool->redo, pika_undo_stack_peek (redo_stack)); } /* if the operation is "Replace", turn off the marching ants, * because they are confusing ... */ if (operation == PIKA_CHANNEL_OP_REPLACE) { /* ... however, do this in an idle function, to avoid unnecessarily * restarting the selection if we don't visit the main loop between * the start_change() and end_change() calls. */ sel_tool->idle_id = g_idle_add_full ( G_PRIORITY_HIGH_IDLE, (GSourceFunc) pika_selection_tool_idle, sel_tool, NULL); } } g_set_weak_pointer (&sel_tool->undo, pika_undo_stack_peek (undo_stack)); } void pika_selection_tool_end_change (PikaSelectionTool *sel_tool, gboolean cancel) { PikaTool *tool; PikaDisplayShell *shell; PikaImage *image; PikaUndoStack *undo_stack; g_return_if_fail (PIKA_IS_SELECTION_TOOL (sel_tool)); g_return_if_fail (sel_tool->change_count > 0); tool = PIKA_TOOL (sel_tool); g_return_if_fail (tool->display != NULL); if (--sel_tool->change_count > 0) return; shell = pika_display_get_shell (tool->display); image = pika_display_get_image (tool->display); undo_stack = pika_image_get_undo_stack (image); if (cancel) { PikaUndoStack *redo_stack = pika_image_get_redo_stack (image); PikaUndo *redo = pika_undo_stack_peek (redo_stack); if (redo && redo == sel_tool->redo) { /* prevent this from halting the tool */ pika_tool_control_push_preserve (tool->control, TRUE); pika_image_redo (image); pika_tool_control_pop_preserve (tool->control); g_set_weak_pointer (&sel_tool->undo, pika_undo_stack_peek (undo_stack)); } else { g_clear_weak_pointer (&sel_tool->undo); } } else { PikaUndo *undo = pika_undo_stack_peek (undo_stack); /* save the undo that we got when executing, but only if * we actually selected something */ if (undo && undo != sel_tool->undo) g_set_weak_pointer (&sel_tool->undo, undo); else g_clear_weak_pointer (&sel_tool->undo); } g_clear_weak_pointer (&sel_tool->redo); if (sel_tool->idle_id) { g_source_remove (sel_tool->idle_id); sel_tool->idle_id = 0; } else { pika_display_shell_set_show_selection (shell, sel_tool->saved_show_selection); } pika_image_flush (image); }