/* 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 "libpikabase/pikabase.h" #include "libpikawidgets/pikawidgets.h" #include "widgets-types.h" #include "config/pikacoreconfig.h" #include "core/pika.h" #include "core/pikalist.h" #include "core/pikaimage.h" #include "core/pikaimage-undo.h" #include "core/pikaundostack.h" #include "pikacontainertreeview.h" #include "pikacontainerview.h" #include "pikadocked.h" #include "pikahelp-ids.h" #include "pikamenufactory.h" #include "pikaundoeditor.h" #include "pika-intl.h" enum { PROP_0, PROP_VIEW_SIZE }; static void pika_undo_editor_docked_iface_init (PikaDockedInterface *iface); static void pika_undo_editor_constructed (GObject *object); static void pika_undo_editor_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void pika_undo_editor_set_image (PikaImageEditor *editor, PikaImage *image); static void pika_undo_editor_set_context (PikaDocked *docked, PikaContext *context); static void pika_undo_editor_fill (PikaUndoEditor *editor); static void pika_undo_editor_clear (PikaUndoEditor *editor); static void pika_undo_editor_undo_event (PikaImage *image, PikaUndoEvent event, PikaUndo *undo, PikaUndoEditor *editor); static gboolean pika_undo_editor_select_items (PikaContainerView *view, GList *undos, GList *paths, PikaUndoEditor *editor); G_DEFINE_TYPE_WITH_CODE (PikaUndoEditor, pika_undo_editor, PIKA_TYPE_IMAGE_EDITOR, G_IMPLEMENT_INTERFACE (PIKA_TYPE_DOCKED, pika_undo_editor_docked_iface_init)) #define parent_class pika_undo_editor_parent_class static PikaDockedInterface *parent_docked_iface = NULL; static void pika_undo_editor_class_init (PikaUndoEditorClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); PikaImageEditorClass *image_editor_class = PIKA_IMAGE_EDITOR_CLASS (klass); object_class->constructed = pika_undo_editor_constructed; object_class->set_property = pika_undo_editor_set_property; image_editor_class->set_image = pika_undo_editor_set_image; g_object_class_install_property (object_class, PROP_VIEW_SIZE, g_param_spec_enum ("view-size", NULL, NULL, PIKA_TYPE_VIEW_SIZE, PIKA_VIEW_SIZE_LARGE, PIKA_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); } static void pika_undo_editor_docked_iface_init (PikaDockedInterface *iface) { parent_docked_iface = g_type_interface_peek_parent (iface); if (! parent_docked_iface) parent_docked_iface = g_type_default_interface_peek (PIKA_TYPE_DOCKED); iface->set_context = pika_undo_editor_set_context; } static void pika_undo_editor_init (PikaUndoEditor *undo_editor) { } static void pika_undo_editor_constructed (GObject *object) { PikaUndoEditor *undo_editor = PIKA_UNDO_EDITOR (object); G_OBJECT_CLASS (parent_class)->constructed (object); undo_editor->view = pika_container_tree_view_new (NULL, NULL, undo_editor->view_size, 1); gtk_box_pack_start (GTK_BOX (undo_editor), undo_editor->view, TRUE, TRUE, 0); gtk_widget_show (undo_editor->view); g_signal_connect (undo_editor->view, "select-items", G_CALLBACK (pika_undo_editor_select_items), undo_editor); undo_editor->undo_button = pika_editor_add_action_button (PIKA_EDITOR (undo_editor), "edit", "edit-undo", NULL); undo_editor->redo_button = pika_editor_add_action_button (PIKA_EDITOR (undo_editor), "edit", "edit-redo", NULL); undo_editor->clear_button = pika_editor_add_action_button (PIKA_EDITOR (undo_editor), "edit", "edit-undo-clear", NULL); } static void pika_undo_editor_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PikaUndoEditor *undo_editor = PIKA_UNDO_EDITOR (object); switch (property_id) { case PROP_VIEW_SIZE: undo_editor->view_size = g_value_get_enum (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void pika_undo_editor_set_image (PikaImageEditor *image_editor, PikaImage *image) { PikaUndoEditor *editor = PIKA_UNDO_EDITOR (image_editor); if (image_editor->image) { pika_undo_editor_clear (editor); g_signal_handlers_disconnect_by_func (image_editor->image, pika_undo_editor_undo_event, editor); } PIKA_IMAGE_EDITOR_CLASS (parent_class)->set_image (image_editor, image); if (image_editor->image) { if (pika_image_undo_is_enabled (image_editor->image)) pika_undo_editor_fill (editor); g_signal_connect (image_editor->image, "undo-event", G_CALLBACK (pika_undo_editor_undo_event), editor); } } static void pika_undo_editor_set_context (PikaDocked *docked, PikaContext *context) { PikaUndoEditor *editor = PIKA_UNDO_EDITOR (docked); if (editor->context) g_object_unref (editor->context); editor->context = context; if (editor->context) g_object_ref (editor->context); /* This calls pika_undo_editor_set_image(), so make sure that it * isn't called before editor->context has been initialized. */ parent_docked_iface->set_context (docked, context); pika_container_view_set_context (PIKA_CONTAINER_VIEW (editor->view), context); } /* public functions */ GtkWidget * pika_undo_editor_new (PikaCoreConfig *config, PikaMenuFactory *menu_factory) { g_return_val_if_fail (PIKA_IS_CORE_CONFIG (config), NULL); g_return_val_if_fail (PIKA_IS_MENU_FACTORY (menu_factory), NULL); return g_object_new (PIKA_TYPE_UNDO_EDITOR, "menu-factory", menu_factory, "menu-identifier", "", "ui-path", "/undo-popup", "view-size", config->undo_preview_size, NULL); } /* private functions */ static void pika_undo_editor_fill (PikaUndoEditor *editor) { PikaImage *image = PIKA_IMAGE_EDITOR (editor)->image; PikaUndoStack *undo_stack = pika_image_get_undo_stack (image); PikaUndoStack *redo_stack = pika_image_get_redo_stack (image); PikaUndo *top_undo_item; GList *list; /* create a container as model for the undo history list */ editor->container = pika_list_new (PIKA_TYPE_UNDO, FALSE); editor->base_item = g_object_new (PIKA_TYPE_UNDO, "image", image, "name", _("[ Base Image ]"), NULL); /* the list prepends its items, so first add the redo items in * reverse (ascending) order... */ for (list = PIKA_LIST (redo_stack->undos)->queue->tail; list; list = g_list_previous (list)) { pika_container_add (editor->container, PIKA_OBJECT (list->data)); } /* ...then add the undo items in descending order... */ for (list = PIKA_LIST (undo_stack->undos)->queue->head; list; list = g_list_next (list)) { /* Don't add the topmost item if it is an open undo group, * it will be added upon closing of the group. */ if (list->prev || ! PIKA_IS_UNDO_STACK (list->data) || pika_image_get_undo_group_count (image) == 0) { pika_container_add (editor->container, PIKA_OBJECT (list->data)); } } /* ...finally, the first item is the special "base_item" which stands * for the image with no more undos available to pop */ pika_container_add (editor->container, PIKA_OBJECT (editor->base_item)); /* display the container */ pika_container_view_set_container (PIKA_CONTAINER_VIEW (editor->view), editor->container); top_undo_item = pika_undo_stack_peek (undo_stack); g_signal_handlers_block_by_func (editor->view, pika_undo_editor_select_items, editor); /* select the current state of the image */ if (top_undo_item) { pika_container_view_select_item (PIKA_CONTAINER_VIEW (editor->view), PIKA_VIEWABLE (top_undo_item)); pika_undo_create_preview (top_undo_item, editor->context, FALSE); } else { pika_container_view_select_item (PIKA_CONTAINER_VIEW (editor->view), PIKA_VIEWABLE (editor->base_item)); pika_undo_create_preview (editor->base_item, editor->context, TRUE); } g_signal_handlers_unblock_by_func (editor->view, pika_undo_editor_select_items, editor); } static void pika_undo_editor_clear (PikaUndoEditor *editor) { if (editor->container) { pika_container_view_set_container (PIKA_CONTAINER_VIEW (editor->view), NULL); g_clear_object (&editor->container); } g_clear_object (&editor->base_item); } static void pika_undo_editor_undo_event (PikaImage *image, PikaUndoEvent event, PikaUndo *undo, PikaUndoEditor *editor) { PikaUndoStack *undo_stack = pika_image_get_undo_stack (image); PikaUndo *top_undo_item = pika_undo_stack_peek (undo_stack); switch (event) { case PIKA_UNDO_EVENT_UNDO_PUSHED: g_signal_handlers_block_by_func (editor->view, pika_undo_editor_select_items, editor); pika_container_insert (editor->container, PIKA_OBJECT (undo), -1); pika_container_view_select_item (PIKA_CONTAINER_VIEW (editor->view), PIKA_VIEWABLE (undo)); pika_undo_create_preview (undo, editor->context, FALSE); g_signal_handlers_unblock_by_func (editor->view, pika_undo_editor_select_items, editor); break; case PIKA_UNDO_EVENT_UNDO_EXPIRED: case PIKA_UNDO_EVENT_REDO_EXPIRED: pika_container_remove (editor->container, PIKA_OBJECT (undo)); break; case PIKA_UNDO_EVENT_UNDO: case PIKA_UNDO_EVENT_REDO: g_signal_handlers_block_by_func (editor->view, pika_undo_editor_select_items, editor); if (top_undo_item) { pika_container_view_select_item (PIKA_CONTAINER_VIEW (editor->view), PIKA_VIEWABLE (top_undo_item)); pika_undo_create_preview (top_undo_item, editor->context, FALSE); } else { pika_container_view_select_item (PIKA_CONTAINER_VIEW (editor->view), PIKA_VIEWABLE (editor->base_item)); pika_undo_create_preview (editor->base_item, editor->context, TRUE); } g_signal_handlers_unblock_by_func (editor->view, pika_undo_editor_select_items, editor); break; case PIKA_UNDO_EVENT_UNDO_FREE: if (pika_image_undo_is_enabled (image)) pika_undo_editor_clear (editor); break; case PIKA_UNDO_EVENT_UNDO_FREEZE: pika_undo_editor_clear (editor); break; case PIKA_UNDO_EVENT_UNDO_THAW: pika_undo_editor_fill (editor); break; } } static gboolean pika_undo_editor_select_items (PikaContainerView *view, GList *undos, GList *paths, PikaUndoEditor *editor) { PikaImage *image = PIKA_IMAGE_EDITOR (editor)->image; PikaUndoStack *undo_stack = pika_image_get_undo_stack (image); PikaUndoStack *redo_stack = pika_image_get_redo_stack (image); PikaUndo *top_undo_item; PikaUndo *undo; g_return_val_if_fail (g_list_length (undos) < 2, FALSE); if (! undos) return TRUE; undo = undos->data; top_undo_item = pika_undo_stack_peek (undo_stack); if (undo == editor->base_item) { /* the base_item was selected, pop all available undo items */ while (top_undo_item != NULL) { if (! pika_image_undo (image)) break; top_undo_item = pika_undo_stack_peek (undo_stack); } } else if (pika_container_have (undo_stack->undos, PIKA_OBJECT (undo))) { /* the selected item is on the undo stack, pop undos until it * is on top of the undo stack */ while (top_undo_item != undo) { if(! pika_image_undo (image)) break; top_undo_item = pika_undo_stack_peek (undo_stack); } } else if (pika_container_have (redo_stack->undos, PIKA_OBJECT (undo))) { /* the selected item is on the redo stack, pop redos until it * is on top of the undo stack */ while (top_undo_item != undo) { if (! pika_image_redo (image)) break; top_undo_item = pika_undo_stack_peek (undo_stack); } } pika_image_flush (image); return TRUE; }