/* 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 "core-types.h" #include "config/pikacoreconfig.h" #include "pika.h" #include "pika-utils.h" #include "pikaimage.h" #include "pikaimage-private.h" #include "pikaimage-undo.h" #include "pikaitem.h" #include "pikalist.h" #include "pikaundostack.h" /* local function prototypes */ static void pika_image_undo_pop_stack (PikaImage *image, PikaUndoStack *undo_stack, PikaUndoStack *redo_stack, PikaUndoMode undo_mode); static void pika_image_undo_free_space (PikaImage *image); static void pika_image_undo_free_redo (PikaImage *image); static PikaDirtyMask pika_image_undo_dirty_from_type (PikaUndoType undo_type); /* public functions */ gboolean pika_image_undo_is_enabled (PikaImage *image) { g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); return (PIKA_IMAGE_GET_PRIVATE (image)->undo_freeze_count == 0); } gboolean pika_image_undo_enable (PikaImage *image) { g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); /* Free all undo steps as they are now invalidated */ pika_image_undo_free (image); return pika_image_undo_thaw (image); } gboolean pika_image_undo_disable (PikaImage *image) { g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); return pika_image_undo_freeze (image); } gboolean pika_image_undo_freeze (PikaImage *image) { PikaImagePrivate *private; g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); private = PIKA_IMAGE_GET_PRIVATE (image); private->undo_freeze_count++; if (private->undo_freeze_count == 1) pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_FREEZE, NULL); return TRUE; } gboolean pika_image_undo_thaw (PikaImage *image) { PikaImagePrivate *private; g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); private = PIKA_IMAGE_GET_PRIVATE (image); g_return_val_if_fail (private->undo_freeze_count > 0, FALSE); private->undo_freeze_count--; if (private->undo_freeze_count == 0) pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_THAW, NULL); return TRUE; } gboolean pika_image_undo (PikaImage *image) { PikaImagePrivate *private; g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); private = PIKA_IMAGE_GET_PRIVATE (image); g_return_val_if_fail (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE, FALSE); pika_image_undo_pop_stack (image, private->undo_stack, private->redo_stack, PIKA_UNDO_MODE_UNDO); return TRUE; } gboolean pika_image_redo (PikaImage *image) { PikaImagePrivate *private; g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); private = PIKA_IMAGE_GET_PRIVATE (image); g_return_val_if_fail (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE, FALSE); pika_image_undo_pop_stack (image, private->redo_stack, private->undo_stack, PIKA_UNDO_MODE_REDO); return TRUE; } /* * this function continues to undo as long as it only sees certain * undo types, in particular visibility changes. */ gboolean pika_image_strong_undo (PikaImage *image) { PikaImagePrivate *private; PikaUndo *undo; g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); private = PIKA_IMAGE_GET_PRIVATE (image); g_return_val_if_fail (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE, FALSE); undo = pika_undo_stack_peek (private->undo_stack); pika_image_undo (image); while (pika_undo_is_weak (undo)) { undo = pika_undo_stack_peek (private->undo_stack); if (pika_undo_is_weak (undo)) pika_image_undo (image); } return TRUE; } /* * this function continues to redo as long as it only sees certain * undo types, in particular visibility changes. Note that the * order of events is set up to make it exactly reverse * pika_image_strong_undo(). */ gboolean pika_image_strong_redo (PikaImage *image) { PikaImagePrivate *private; PikaUndo *undo; g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); private = PIKA_IMAGE_GET_PRIVATE (image); g_return_val_if_fail (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE, FALSE); undo = pika_undo_stack_peek (private->redo_stack); pika_image_redo (image); while (pika_undo_is_weak (undo)) { undo = pika_undo_stack_peek (private->redo_stack); if (pika_undo_is_weak (undo)) pika_image_redo (image); } return TRUE; } PikaUndoStack * pika_image_get_undo_stack (PikaImage *image) { g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL); return PIKA_IMAGE_GET_PRIVATE (image)->undo_stack; } PikaUndoStack * pika_image_get_redo_stack (PikaImage *image) { g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL); return PIKA_IMAGE_GET_PRIVATE (image)->redo_stack; } void pika_image_undo_free (PikaImage *image) { PikaImagePrivate *private; g_return_if_fail (PIKA_IS_IMAGE (image)); private = PIKA_IMAGE_GET_PRIVATE (image); /* Emit the UNDO_FREE event before actually freeing everything * so the views can properly detach from the undo items */ pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_FREE, NULL); pika_undo_free (PIKA_UNDO (private->undo_stack), PIKA_UNDO_MODE_UNDO); pika_undo_free (PIKA_UNDO (private->redo_stack), PIKA_UNDO_MODE_REDO); /* If the image was dirty, but could become clean by redo-ing * some actions, then it should now become 'infinitely' dirty. * This is because we've just nuked the actions that would allow * the image to become clean again. */ if (private->dirty < 0) private->dirty = 100000; /* The same applies to the case where the image would become clean * due to undo actions, but since user can't undo without an undo * stack, that's not so much a problem. */ } gint pika_image_get_undo_group_count (PikaImage *image) { g_return_val_if_fail (PIKA_IS_IMAGE (image), 0); return PIKA_IMAGE_GET_PRIVATE (image)->group_count; } gboolean pika_image_undo_group_start (PikaImage *image, PikaUndoType undo_type, const gchar *name) { PikaImagePrivate *private; PikaUndoStack *undo_group; PikaDirtyMask dirty_mask; g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); g_return_val_if_fail (undo_type > PIKA_UNDO_GROUP_FIRST && undo_type <= PIKA_UNDO_GROUP_LAST, FALSE); private = PIKA_IMAGE_GET_PRIVATE (image); if (! name) name = pika_undo_type_to_name (undo_type); dirty_mask = pika_image_undo_dirty_from_type (undo_type); /* Notify listeners that the image will be modified */ if (private->group_count == 0 && dirty_mask != PIKA_DIRTY_NONE) pika_image_dirty (image, dirty_mask); if (private->undo_freeze_count > 0) return FALSE; private->group_count++; /* If we're already in a group...ignore */ if (private->group_count > 1) return TRUE; /* nuke the redo stack */ pika_image_undo_free_redo (image); undo_group = pika_undo_stack_new (image); pika_object_set_name (PIKA_OBJECT (undo_group), name); PIKA_UNDO (undo_group)->undo_type = undo_type; PIKA_UNDO (undo_group)->dirty_mask = dirty_mask; pika_undo_stack_push_undo (private->undo_stack, PIKA_UNDO (undo_group)); private->pushing_undo_group = undo_type; return TRUE; } gboolean pika_image_undo_group_end (PikaImage *image) { PikaImagePrivate *private; g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE); private = PIKA_IMAGE_GET_PRIVATE (image); if (private->undo_freeze_count > 0) return FALSE; g_return_val_if_fail (private->group_count > 0, FALSE); private->group_count--; if (private->group_count == 0) { private->pushing_undo_group = PIKA_UNDO_GROUP_NONE; /* Do it here, since undo_push doesn't emit this event while in * the middle of a group */ pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_PUSHED, pika_undo_stack_peek (private->undo_stack)); pika_image_undo_free_space (image); } return TRUE; } PikaUndo * pika_image_undo_push (PikaImage *image, GType object_type, PikaUndoType undo_type, const gchar *name, PikaDirtyMask dirty_mask, ...) { PikaImagePrivate *private; gint n_properties = 0; gchar **names = NULL; GValue *values = NULL; va_list args; PikaUndo *undo; g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL); g_return_val_if_fail (g_type_is_a (object_type, PIKA_TYPE_UNDO), NULL); g_return_val_if_fail (undo_type > PIKA_UNDO_GROUP_LAST, NULL); private = PIKA_IMAGE_GET_PRIVATE (image); /* Does this undo dirty the image? If so, we always want to mark * image dirty, even if we can't actually push the undo. */ if (dirty_mask != PIKA_DIRTY_NONE) pika_image_dirty (image, dirty_mask); if (private->undo_freeze_count > 0) return NULL; if (! name) name = pika_undo_type_to_name (undo_type); names = pika_properties_append (object_type, &n_properties, names, &values, "name", name, "image", image, "undo-type", undo_type, "dirty-mask", dirty_mask, NULL); va_start (args, dirty_mask); names = pika_properties_append_valist (object_type, &n_properties, names, &values, args); va_end (args); undo = (PikaUndo *) g_object_new_with_properties (object_type, n_properties, (const gchar **) names, (const GValue *) values); pika_properties_free (n_properties, names, values); /* nuke the redo stack */ pika_image_undo_free_redo (image); if (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE) { pika_undo_stack_push_undo (private->undo_stack, undo); pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_PUSHED, undo); pika_image_undo_free_space (image); /* freeing undo space may have freed the newly pushed undo */ if (pika_undo_stack_peek (private->undo_stack) == undo) return undo; } else { PikaUndoStack *undo_group; undo_group = PIKA_UNDO_STACK (pika_undo_stack_peek (private->undo_stack)); pika_undo_stack_push_undo (undo_group, undo); return undo; } return NULL; } PikaUndo * pika_image_undo_can_compress (PikaImage *image, GType object_type, PikaUndoType undo_type) { PikaImagePrivate *private; g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL); private = PIKA_IMAGE_GET_PRIVATE (image); if (pika_image_is_dirty (image) && ! pika_undo_stack_peek (private->redo_stack)) { PikaUndo *undo = pika_undo_stack_peek (private->undo_stack); if (undo && undo->undo_type == undo_type && g_type_is_a (G_TYPE_FROM_INSTANCE (undo), object_type)) { return undo; } } return NULL; } /* private functions */ static void pika_image_undo_pop_stack (PikaImage *image, PikaUndoStack *undo_stack, PikaUndoStack *redo_stack, PikaUndoMode undo_mode) { PikaUndo *undo; PikaUndoAccumulator accum = { 0, }; g_object_freeze_notify (G_OBJECT (image)); undo = pika_undo_stack_pop_undo (undo_stack, undo_mode, &accum); if (undo) { if (PIKA_IS_UNDO_STACK (undo)) pika_list_reverse (PIKA_LIST (PIKA_UNDO_STACK (undo)->undos)); pika_undo_stack_push_undo (redo_stack, undo); if (accum.mode_changed) pika_image_mode_changed (image); if (accum.precision_changed) pika_image_precision_changed (image); if (accum.size_changed) pika_image_size_changed_detailed (image, accum.previous_origin_x, accum.previous_origin_y, accum.previous_width, accum.previous_height); if (accum.resolution_changed) pika_image_resolution_changed (image); if (accum.unit_changed) pika_image_unit_changed (image); /* let others know that we just popped an action */ pika_image_undo_event (image, (undo_mode == PIKA_UNDO_MODE_UNDO) ? PIKA_UNDO_EVENT_UNDO : PIKA_UNDO_EVENT_REDO, undo); } g_object_thaw_notify (G_OBJECT (image)); } static void pika_image_undo_free_space (PikaImage *image) { PikaImagePrivate *private = PIKA_IMAGE_GET_PRIVATE (image); PikaContainer *container; gint min_undo_levels; gint max_undo_levels; gint64 undo_size; container = private->undo_stack->undos; min_undo_levels = image->pika->config->levels_of_undo; max_undo_levels = 1024; /* FIXME */ undo_size = image->pika->config->undo_size; #ifdef DEBUG_IMAGE_UNDO g_printerr ("undo_steps: %d undo_bytes: %ld\n", pika_container_get_n_children (container), (glong) pika_object_get_memsize (PIKA_OBJECT (container), NULL)); #endif /* keep at least min_undo_levels undo steps */ if (pika_container_get_n_children (container) <= min_undo_levels) return; while ((pika_object_get_memsize (PIKA_OBJECT (container), NULL) > undo_size) || (pika_container_get_n_children (container) > max_undo_levels)) { PikaUndo *freed = pika_undo_stack_free_bottom (private->undo_stack, PIKA_UNDO_MODE_UNDO); #ifdef DEBUG_IMAGE_UNDO g_printerr ("freed one step: undo_steps: %d undo_bytes: %ld\n", pika_container_get_n_children (container), (glong) pika_object_get_memsize (PIKA_OBJECT (container), NULL)); #endif pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_EXPIRED, freed); g_object_unref (freed); if (pika_container_get_n_children (container) <= min_undo_levels) return; } } static void pika_image_undo_free_redo (PikaImage *image) { PikaImagePrivate *private = PIKA_IMAGE_GET_PRIVATE (image); PikaContainer *container = private->redo_stack->undos; #ifdef DEBUG_IMAGE_UNDO g_printerr ("redo_steps: %d redo_bytes: %ld\n", pika_container_get_n_children (container), (glong) pika_object_get_memsize (PIKA_OBJECT (container), NULL)); #endif if (pika_container_is_empty (container)) return; while (pika_container_get_n_children (container) > 0) { PikaUndo *freed = pika_undo_stack_free_bottom (private->redo_stack, PIKA_UNDO_MODE_REDO); #ifdef DEBUG_IMAGE_UNDO g_printerr ("freed one step: redo_steps: %d redo_bytes: %ld\n", pika_container_get_n_children (container), (glong )pika_object_get_memsize (PIKA_OBJECT (container), NULL)); #endif pika_image_undo_event (image, PIKA_UNDO_EVENT_REDO_EXPIRED, freed); g_object_unref (freed); } /* We need to use <= here because the undo counter has already been * incremented at this point. */ if (private->dirty <= 0) { /* If the image was dirty, but could become clean by redo-ing * some actions, then it should now become 'infinitely' dirty. * This is because we've just nuked the actions that would allow * the image to become clean again. */ private->dirty = 100000; } } static PikaDirtyMask pika_image_undo_dirty_from_type (PikaUndoType undo_type) { switch (undo_type) { case PIKA_UNDO_GROUP_IMAGE_SCALE: case PIKA_UNDO_GROUP_IMAGE_RESIZE: case PIKA_UNDO_GROUP_IMAGE_FLIP: case PIKA_UNDO_GROUP_IMAGE_ROTATE: case PIKA_UNDO_GROUP_IMAGE_TRANSFORM: case PIKA_UNDO_GROUP_IMAGE_CROP: return PIKA_DIRTY_IMAGE | PIKA_DIRTY_IMAGE_SIZE; case PIKA_UNDO_GROUP_IMAGE_CONVERT: return PIKA_DIRTY_IMAGE | PIKA_DIRTY_DRAWABLE; case PIKA_UNDO_GROUP_IMAGE_COLORMAP_REMAP: return PIKA_DIRTY_IMAGE | PIKA_DIRTY_DRAWABLE; case PIKA_UNDO_GROUP_IMAGE_LAYERS_MERGE: return PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_DRAWABLE; case PIKA_UNDO_GROUP_IMAGE_VECTORS_MERGE: return PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_VECTORS; case PIKA_UNDO_GROUP_IMAGE_QUICK_MASK: /* FIXME */ return PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_SELECTION; case PIKA_UNDO_GROUP_IMAGE_GRID: case PIKA_UNDO_GROUP_GUIDE: return PIKA_DIRTY_IMAGE_META; case PIKA_UNDO_GROUP_DRAWABLE: case PIKA_UNDO_GROUP_DRAWABLE_MOD: return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE; case PIKA_UNDO_GROUP_MASK: /* FIXME */ return PIKA_DIRTY_SELECTION; case PIKA_UNDO_GROUP_ITEM_VISIBILITY: case PIKA_UNDO_GROUP_ITEM_PROPERTIES: return PIKA_DIRTY_ITEM_META; case PIKA_UNDO_GROUP_ITEM_DISPLACE: /* FIXME */ return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE | PIKA_DIRTY_VECTORS; case PIKA_UNDO_GROUP_ITEM_SCALE: /* FIXME */ case PIKA_UNDO_GROUP_ITEM_RESIZE: /* FIXME */ return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE | PIKA_DIRTY_VECTORS; case PIKA_UNDO_GROUP_LAYER_ADD_MASK: case PIKA_UNDO_GROUP_LAYER_APPLY_MASK: return PIKA_DIRTY_IMAGE_STRUCTURE; case PIKA_UNDO_GROUP_FS_TO_LAYER: case PIKA_UNDO_GROUP_FS_FLOAT: case PIKA_UNDO_GROUP_FS_ANCHOR: return PIKA_DIRTY_IMAGE_STRUCTURE; case PIKA_UNDO_GROUP_EDIT_PASTE: return PIKA_DIRTY_IMAGE_STRUCTURE; case PIKA_UNDO_GROUP_EDIT_CUT: return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE; case PIKA_UNDO_GROUP_TEXT: return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE; case PIKA_UNDO_GROUP_TRANSFORM: /* FIXME */ return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE | PIKA_DIRTY_VECTORS; case PIKA_UNDO_GROUP_PAINT: return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE; case PIKA_UNDO_GROUP_PARASITE_ATTACH: case PIKA_UNDO_GROUP_PARASITE_REMOVE: return PIKA_DIRTY_IMAGE_META | PIKA_DIRTY_ITEM_META; case PIKA_UNDO_GROUP_VECTORS_IMPORT: return PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_VECTORS; case PIKA_UNDO_GROUP_MISC: return PIKA_DIRTY_ALL; default: break; } return PIKA_DIRTY_ALL; }