/* 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/pika.h" #include "core/pikaimage.h" #include "core/pikaimage-crop.h" #include "core/pikaimage-undo.h" #include "core/pikaitem.h" #include "core/pikatoolinfo.h" #include "widgets/pikahelp-ids.h" #include "display/pikadisplay.h" #include "display/pikadisplayshell.h" #include "display/pikatoolrectangle.h" #include "pikacropoptions.h" #include "pikacroptool.h" #include "pikarectangleoptions.h" #include "pikatoolcontrol.h" #include "pikatools-utils.h" #include "pika-intl.h" static void pika_crop_tool_constructed (GObject *object); static void pika_crop_tool_dispose (GObject *object); static void pika_crop_tool_control (PikaTool *tool, PikaToolAction action, PikaDisplay *display); static void pika_crop_tool_button_press (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonPressType press_type, PikaDisplay *display); static void pika_crop_tool_button_release (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonReleaseType release_type, PikaDisplay *display); static void pika_crop_tool_motion (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaDisplay *display); static void pika_crop_tool_options_notify (PikaTool *tool, PikaToolOptions *options, const GParamSpec *pspec); static void pika_crop_tool_rectangle_changed (PikaToolWidget *rectangle, PikaCropTool *crop_tool); static void pika_crop_tool_rectangle_response (PikaToolWidget *rectangle, gint response_id, PikaCropTool *crop_tool); static void pika_crop_tool_rectangle_change_complete (PikaToolRectangle *rectangle, PikaCropTool *crop_tool); static void pika_crop_tool_start (PikaCropTool *crop_tool, PikaDisplay *display); static void pika_crop_tool_commit (PikaCropTool *crop_tool); static void pika_crop_tool_halt (PikaCropTool *crop_tool); static void pika_crop_tool_update_option_defaults (PikaCropTool *crop_tool, gboolean ignore_pending); static PikaRectangleConstraint pika_crop_tool_get_constraint (PikaCropTool *crop_tool); static void pika_crop_tool_image_changed (PikaCropTool *crop_tool, PikaImage *image, PikaContext *context); static void pika_crop_tool_image_size_changed (PikaCropTool *crop_tool); static void pika_crop_tool_image_selected_layers_changed (PikaCropTool *crop_tool); static void pika_crop_tool_layer_size_changed (PikaCropTool *crop_tool); static void pika_crop_tool_auto_shrink (PikaCropTool *crop_tool); G_DEFINE_TYPE (PikaCropTool, pika_crop_tool, PIKA_TYPE_DRAW_TOOL) #define parent_class pika_crop_tool_parent_class /* public functions */ void pika_crop_tool_register (PikaToolRegisterCallback callback, gpointer data) { (* callback) (PIKA_TYPE_CROP_TOOL, PIKA_TYPE_CROP_OPTIONS, pika_crop_options_gui, PIKA_CONTEXT_PROP_MASK_FOREGROUND | PIKA_CONTEXT_PROP_MASK_BACKGROUND | PIKA_CONTEXT_PROP_MASK_PATTERN, "pika-crop-tool", _("Crop"), _("Crop Tool: Remove edge areas from image or layer"), N_("_Crop"), "C", NULL, PIKA_HELP_TOOL_CROP, PIKA_ICON_TOOL_CROP, data); } static void pika_crop_tool_class_init (PikaCropToolClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); PikaToolClass *tool_class = PIKA_TOOL_CLASS (klass); object_class->constructed = pika_crop_tool_constructed; object_class->dispose = pika_crop_tool_dispose; tool_class->control = pika_crop_tool_control; tool_class->button_press = pika_crop_tool_button_press; tool_class->button_release = pika_crop_tool_button_release; tool_class->motion = pika_crop_tool_motion; tool_class->options_notify = pika_crop_tool_options_notify; } static void pika_crop_tool_init (PikaCropTool *crop_tool) { PikaTool *tool = PIKA_TOOL (crop_tool); pika_tool_control_set_wants_click (tool->control, TRUE); pika_tool_control_set_active_modifiers (tool->control, PIKA_TOOL_ACTIVE_MODIFIERS_SEPARATE); pika_tool_control_set_precision (tool->control, PIKA_CURSOR_PRECISION_PIXEL_BORDER); pika_tool_control_set_cursor (tool->control, PIKA_CURSOR_CROSSHAIR_SMALL); pika_tool_control_set_tool_cursor (tool->control, PIKA_TOOL_CURSOR_CROP); pika_draw_tool_set_default_status (PIKA_DRAW_TOOL (tool), _("Click-Drag to draw a crop rectangle")); } static void pika_crop_tool_constructed (GObject *object) { PikaCropTool *crop_tool = PIKA_CROP_TOOL (object); PikaContext *context; PikaToolInfo *tool_info; G_OBJECT_CLASS (parent_class)->constructed (object); tool_info = PIKA_TOOL (crop_tool)->tool_info; context = pika_get_user_context (tool_info->pika); g_signal_connect_object (context, "image-changed", G_CALLBACK (pika_crop_tool_image_changed), crop_tool, G_CONNECT_SWAPPED); /* Make sure we are connected to "size-changed" for the initial * image. */ pika_crop_tool_image_changed (crop_tool, pika_context_get_image (context), context); } static void pika_crop_tool_dispose (GObject *object) { PikaCropTool *crop_tool = PIKA_CROP_TOOL (object); /* Clean up current_image and current_layers. */ pika_crop_tool_image_changed (crop_tool, NULL, NULL); G_OBJECT_CLASS (parent_class)->dispose (object); } static void pika_crop_tool_control (PikaTool *tool, PikaToolAction action, PikaDisplay *display) { PikaCropTool *crop_tool = PIKA_CROP_TOOL (tool); switch (action) { case PIKA_TOOL_ACTION_PAUSE: case PIKA_TOOL_ACTION_RESUME: break; case PIKA_TOOL_ACTION_HALT: pika_crop_tool_halt (crop_tool); break; case PIKA_TOOL_ACTION_COMMIT: pika_crop_tool_commit (crop_tool); break; } PIKA_TOOL_CLASS (parent_class)->control (tool, action, display); } static void pika_crop_tool_button_press (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonPressType press_type, PikaDisplay *display) { PikaCropTool *crop_tool = PIKA_CROP_TOOL (tool); if (tool->display && display != tool->display) pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display); if (! tool->display) { pika_crop_tool_start (crop_tool, display); pika_tool_widget_hover (crop_tool->widget, coords, state, TRUE); /* HACK: force CREATING on a newly created rectangle; otherwise, * property bindings would cause the rectangle to start with the * size from tool options. */ pika_tool_rectangle_set_function (PIKA_TOOL_RECTANGLE (crop_tool->widget), PIKA_TOOL_RECTANGLE_CREATING); } if (pika_tool_widget_button_press (crop_tool->widget, coords, time, state, press_type)) { crop_tool->grab_widget = crop_tool->widget; } pika_tool_control_activate (tool->control); } static void pika_crop_tool_button_release (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonReleaseType release_type, PikaDisplay *display) { PikaCropTool *crop_tool = PIKA_CROP_TOOL (tool); pika_tool_control_halt (tool->control); if (crop_tool->grab_widget) { pika_tool_widget_button_release (crop_tool->grab_widget, coords, time, state, release_type); crop_tool->grab_widget = NULL; } pika_tool_push_status (tool, display, _("Click or press Enter to crop")); } static void pika_crop_tool_motion (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaDisplay *display) { PikaCropTool *crop_tool = PIKA_CROP_TOOL (tool); if (crop_tool->grab_widget) { pika_tool_widget_motion (crop_tool->grab_widget, coords, time, state); } } static void pika_crop_tool_options_notify (PikaTool *tool, PikaToolOptions *options, const GParamSpec *pspec) { PikaCropTool *crop_tool = PIKA_CROP_TOOL (tool); if (! strcmp (pspec->name, "layer-only") || ! strcmp (pspec->name, "allow-growing")) { if (crop_tool->widget) { pika_tool_rectangle_set_constraint (PIKA_TOOL_RECTANGLE (crop_tool->widget), pika_crop_tool_get_constraint (crop_tool)); } else { pika_crop_tool_update_option_defaults (crop_tool, FALSE); } } } static void pika_crop_tool_rectangle_changed (PikaToolWidget *rectangle, PikaCropTool *crop_tool) { } static void pika_crop_tool_rectangle_response (PikaToolWidget *rectangle, gint response_id, PikaCropTool *crop_tool) { PikaTool *tool = PIKA_TOOL (crop_tool); switch (response_id) { case PIKA_TOOL_WIDGET_RESPONSE_CONFIRM: pika_tool_control (tool, PIKA_TOOL_ACTION_COMMIT, tool->display); break; case PIKA_TOOL_WIDGET_RESPONSE_CANCEL: pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display); break; } } static void pika_crop_tool_rectangle_change_complete (PikaToolRectangle *rectangle, PikaCropTool *crop_tool) { pika_crop_tool_update_option_defaults (crop_tool, FALSE); } static void pika_crop_tool_start (PikaCropTool *crop_tool, PikaDisplay *display) { static const gchar *properties[] = { "highlight", "highlight-opacity", "guide", "x", "y", "width", "height", "fixed-rule-active", "fixed-rule", "desired-fixed-width", "desired-fixed-height", "desired-fixed-size-width", "desired-fixed-size-height", "aspect-numerator", "aspect-denominator", "fixed-center" }; PikaTool *tool = PIKA_TOOL (crop_tool); PikaDisplayShell *shell = pika_display_get_shell (display); PikaCropOptions *options = PIKA_CROP_TOOL_GET_OPTIONS (crop_tool); PikaToolWidget *widget; gint i; tool->display = display; crop_tool->widget = widget = pika_tool_rectangle_new (shell); g_object_set (widget, "status-title", _("Crop to: "), NULL); pika_draw_tool_set_widget (PIKA_DRAW_TOOL (tool), widget); for (i = 0; i < G_N_ELEMENTS (properties); i++) { GBinding *binding = g_object_bind_property (G_OBJECT (options), properties[i], G_OBJECT (widget), properties[i], G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); crop_tool->bindings = g_list_prepend (crop_tool->bindings, binding); } pika_rectangle_options_connect (PIKA_RECTANGLE_OPTIONS (options), pika_display_get_image (shell->display), G_CALLBACK (pika_crop_tool_auto_shrink), crop_tool); pika_tool_rectangle_set_constraint (PIKA_TOOL_RECTANGLE (widget), pika_crop_tool_get_constraint (crop_tool)); g_signal_connect (widget, "changed", G_CALLBACK (pika_crop_tool_rectangle_changed), crop_tool); g_signal_connect (widget, "response", G_CALLBACK (pika_crop_tool_rectangle_response), crop_tool); g_signal_connect (widget, "change-complete", G_CALLBACK (pika_crop_tool_rectangle_change_complete), crop_tool); pika_draw_tool_start (PIKA_DRAW_TOOL (tool), display); } static void pika_crop_tool_commit (PikaCropTool *crop_tool) { PikaTool *tool = PIKA_TOOL (crop_tool); if (tool->display) { PikaCropOptions *options = PIKA_CROP_TOOL_GET_OPTIONS (tool); PikaImage *image = pika_display_get_image (tool->display); gdouble x, y; gdouble x2, y2; gint w, h; pika_tool_rectangle_get_public_rect (PIKA_TOOL_RECTANGLE (crop_tool->widget), &x, &y, &x2, &y2); w = x2 - x; h = y2 - y; pika_tool_pop_status (tool, tool->display); /* if rectangle exists, crop it */ if (w > 0 && h > 0) { if (options->layer_only) { GList *layers = pika_image_get_selected_layers (image); GList *iter; gint off_x, off_y; gchar *undo_text; if (! layers) { pika_tool_message_literal (tool, tool->display, _("There are no selected layers to crop.")); return; } for (iter = layers; iter; iter = iter->next) if (! pika_item_is_content_locked (PIKA_ITEM (iter->data), NULL)) break; if (iter == NULL) { pika_tool_message_literal (tool, tool->display, _("All selected layers' pixels are locked.")); pika_tools_blink_lock_box (tool->display->pika, PIKA_ITEM (layers->data)); return; } undo_text = ngettext ("Resize Layer", "Resize %d layers", g_list_length (layers)); undo_text = g_strdup_printf (undo_text, g_list_length (layers)); pika_image_undo_group_start (image, PIKA_UNDO_GROUP_IMAGE_CROP, undo_text); g_free (undo_text); for (iter = layers; iter; iter = iter->next) { pika_item_get_offset (PIKA_ITEM (iter->data), &off_x, &off_y); off_x -= x; off_y -= y; pika_item_resize (PIKA_ITEM (iter->data), PIKA_CONTEXT (options), options->fill_type, w, h, off_x, off_y); } pika_image_undo_group_end (image); } else { pika_image_crop (image, PIKA_CONTEXT (options), PIKA_FILL_TRANSPARENT, x, y, w, h, options->delete_pixels); } pika_image_flush (image); } } } static void pika_crop_tool_halt (PikaCropTool *crop_tool) { PikaTool *tool = PIKA_TOOL (crop_tool); PikaCropOptions *options = PIKA_CROP_TOOL_GET_OPTIONS (crop_tool); if (tool->display) { PikaDisplayShell *shell = pika_display_get_shell (tool->display); pika_display_shell_set_highlight (shell, NULL, 0.0); pika_rectangle_options_disconnect (PIKA_RECTANGLE_OPTIONS (options), G_CALLBACK (pika_crop_tool_auto_shrink), crop_tool); } if (pika_draw_tool_is_active (PIKA_DRAW_TOOL (tool))) pika_draw_tool_stop (PIKA_DRAW_TOOL (tool)); /* disconnect bindings manually so they are really gone *now*, we * might be in the middle of a signal emission that keeps the * widget and its bindings alive. */ g_list_free_full (crop_tool->bindings, (GDestroyNotify) g_object_unref); crop_tool->bindings = NULL; pika_draw_tool_set_widget (PIKA_DRAW_TOOL (tool), NULL); g_clear_object (&crop_tool->widget); tool->display = NULL; g_list_free (tool->drawables); tool->drawables = NULL; pika_crop_tool_update_option_defaults (crop_tool, TRUE); } /** * pika_crop_tool_update_option_defaults: * @crop_tool: * @ignore_pending: %TRUE to ignore any pending crop rectangle. * * Sets the default Fixed: Aspect ratio and Fixed: Size option * properties. */ static void pika_crop_tool_update_option_defaults (PikaCropTool *crop_tool, gboolean ignore_pending) { PikaTool *tool = PIKA_TOOL (crop_tool); PikaToolRectangle *rectangle = PIKA_TOOL_RECTANGLE (crop_tool->widget); PikaRectangleOptions *options; options = PIKA_RECTANGLE_OPTIONS (PIKA_TOOL_GET_OPTIONS (tool)); if (rectangle && ! ignore_pending) { /* There is a pending rectangle and we should not ignore it, so * set default Fixed: Aspect ratio to the same as the current * pending rectangle width/height. */ pika_tool_rectangle_pending_size_set (rectangle, G_OBJECT (options), "default-aspect-numerator", "default-aspect-denominator"); g_object_set (G_OBJECT (options), "use-string-current", TRUE, NULL); } else { /* There is no pending rectangle, set default Fixed: Aspect * ratio to that of the current image/layer. */ if (! rectangle) { /* ugly hack: if we don't have a widget, construct a temporary one * so that we can use it to call * pika_tool_rectangle_constraint_size_set(). */ PikaContext *context = pika_get_user_context (tool->tool_info->pika); PikaDisplay *display = pika_context_get_display (context); if (display) { PikaDisplayShell *shell = pika_display_get_shell (display); rectangle = PIKA_TOOL_RECTANGLE (pika_tool_rectangle_new (shell)); pika_tool_rectangle_set_constraint ( rectangle, pika_crop_tool_get_constraint (crop_tool)); } } if (rectangle) { pika_tool_rectangle_constraint_size_set (rectangle, G_OBJECT (options), "default-aspect-numerator", "default-aspect-denominator"); if (! crop_tool->widget) g_object_unref (rectangle); } g_object_set (G_OBJECT (options), "use-string-current", FALSE, NULL); } } static PikaRectangleConstraint pika_crop_tool_get_constraint (PikaCropTool *crop_tool) { PikaCropOptions *crop_options = PIKA_CROP_TOOL_GET_OPTIONS (crop_tool); if (crop_options->allow_growing) { return PIKA_RECTANGLE_CONSTRAIN_NONE; } else { return crop_options->layer_only ? PIKA_RECTANGLE_CONSTRAIN_DRAWABLE : PIKA_RECTANGLE_CONSTRAIN_IMAGE; } } static void pika_crop_tool_image_changed (PikaCropTool *crop_tool, PikaImage *image, PikaContext *context) { if (crop_tool->current_image) { g_signal_handlers_disconnect_by_func (crop_tool->current_image, pika_crop_tool_image_size_changed, NULL); g_signal_handlers_disconnect_by_func (crop_tool->current_image, pika_crop_tool_image_selected_layers_changed, NULL); } g_set_weak_pointer (&crop_tool->current_image, image); if (crop_tool->current_image) { g_signal_connect_object (crop_tool->current_image, "size-changed", G_CALLBACK (pika_crop_tool_image_size_changed), crop_tool, G_CONNECT_SWAPPED); g_signal_connect_object (crop_tool->current_image, "selected-layers-changed", G_CALLBACK (pika_crop_tool_image_selected_layers_changed), crop_tool, G_CONNECT_SWAPPED); } /* Make sure we are connected to "size-changed" for the initial * layer. */ pika_crop_tool_image_selected_layers_changed (crop_tool); pika_crop_tool_update_option_defaults (PIKA_CROP_TOOL (crop_tool), FALSE); } static void pika_crop_tool_image_size_changed (PikaCropTool *crop_tool) { pika_crop_tool_update_option_defaults (crop_tool, FALSE); } static void pika_crop_tool_image_selected_layers_changed (PikaCropTool *crop_tool) { GList *iter; if (crop_tool->current_layers) { for (iter = crop_tool->current_layers; iter; iter = iter->next) { if (iter->data) { g_signal_handlers_disconnect_by_func (iter->data, pika_crop_tool_layer_size_changed, NULL); g_clear_weak_pointer (&iter->data); } } g_list_free (crop_tool->current_layers); crop_tool->current_layers = NULL; } if (crop_tool->current_image) { crop_tool->current_layers = pika_image_get_selected_layers (crop_tool->current_image); crop_tool->current_layers = g_list_copy (crop_tool->current_layers); } else { crop_tool->current_layers = NULL; } if (crop_tool->current_layers) { for (iter = crop_tool->current_layers; iter; iter = iter->next) { /* NOT g_set_weak_pointer() because the pointer is already set */ g_object_add_weak_pointer (G_OBJECT (iter->data), (gpointer) &iter->data); g_signal_connect_object (iter->data, "size-changed", G_CALLBACK (pika_crop_tool_layer_size_changed), crop_tool, G_CONNECT_SWAPPED); } } pika_crop_tool_update_option_defaults (crop_tool, FALSE); } static void pika_crop_tool_layer_size_changed (PikaCropTool *crop_tool) { pika_crop_tool_update_option_defaults (crop_tool, FALSE); } static void pika_crop_tool_auto_shrink (PikaCropTool *crop_tool) { gboolean shrink_merged ; g_object_get (pika_tool_get_options (PIKA_TOOL (crop_tool)), "shrink-merged", &shrink_merged, NULL); pika_tool_rectangle_auto_shrink (PIKA_TOOL_RECTANGLE (crop_tool->widget), shrink_merged); }