/* 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 * * PikaGroupLayer * Copyright (C) 2009 Michael Natterer * * 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 #include "libpikabase/pikabase.h" #include "libpikamath/pikamath.h" #include "core-types.h" #include "gegl/pika-babl.h" #include "gegl/pika-gegl-loops.h" #include "pikadrawable-filters.h" #include "pikagrouplayer.h" #include "pikagrouplayerundo.h" #include "pikaimage.h" #include "pikaimage-undo.h" #include "pikaimage-undo-push.h" #include "pikalayerstack.h" #include "pikaobjectqueue.h" #include "pikapickable.h" #include "pikaprogress.h" #include "pikaprojectable.h" #include "pikaprojection.h" #include "pika-intl.h" typedef struct _PikaGroupLayerPrivate PikaGroupLayerPrivate; struct _PikaGroupLayerPrivate { PikaContainer *children; PikaProjection *projection; GeglNode *source_node; GeglNode *parent_source_node; GeglNode *graph; GeglNode *offset_node; GeglRectangle bounding_box; gint suspend_resize; gint suspend_mask; GeglBuffer *suspended_mask_buffer; GeglRectangle suspended_mask_bounds; gint direct_update; gint transforming; gboolean expanded; gboolean pass_through; /* hackish temp states to make the projection/tiles stuff work */ const Babl *convert_format; gboolean reallocate_projection; }; #define GET_PRIVATE(item) ((PikaGroupLayerPrivate *) pika_group_layer_get_instance_private ((PikaGroupLayer *) (item))) static void pika_projectable_iface_init (PikaProjectableInterface *iface); static void pika_pickable_iface_init (PikaPickableInterface *iface); static void pika_group_layer_finalize (GObject *object); static void pika_group_layer_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void pika_group_layer_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); static gint64 pika_group_layer_get_memsize (PikaObject *object, gint64 *gui_size); static void pika_group_layer_ancestry_changed (PikaViewable *viewable); static gboolean pika_group_layer_get_size (PikaViewable *viewable, gint *width, gint *height); static PikaContainer * pika_group_layer_get_children (PikaViewable *viewable); static gboolean pika_group_layer_get_expanded (PikaViewable *viewable); static void pika_group_layer_set_expanded (PikaViewable *viewable, gboolean expanded); static gboolean pika_group_layer_is_position_locked (PikaItem *item, PikaItem **locked_item, gboolean check_children); static PikaItem * pika_group_layer_duplicate (PikaItem *item, GType new_type); static void pika_group_layer_convert (PikaItem *item, PikaImage *dest_image, GType old_type); static void pika_group_layer_start_transform (PikaItem *item, gboolean push_undo); static void pika_group_layer_end_transform (PikaItem *item, gboolean push_undo); static void pika_group_layer_resize (PikaItem *item, PikaContext *context, PikaFillType fill_type, gint new_width, gint new_height, gint offset_x, gint offset_y); static PikaTransformResize pika_group_layer_get_clip (PikaItem *item, PikaTransformResize clip_result); static gint64 pika_group_layer_estimate_memsize (PikaDrawable *drawable, PikaComponentType component_type, gint width, gint height); static void pika_group_layer_update_all (PikaDrawable *drawable); static void pika_group_layer_translate (PikaLayer *layer, gint offset_x, gint offset_y); static void pika_group_layer_scale (PikaLayer *layer, gint new_width, gint new_height, gint new_offset_x, gint new_offset_y, PikaInterpolationType interp_type, PikaProgress *progress); static void pika_group_layer_flip (PikaLayer *layer, PikaContext *context, PikaOrientationType flip_type, gdouble axis, gboolean clip_result); static void pika_group_layer_rotate (PikaLayer *layer, PikaContext *context, PikaRotationType rotate_type, gdouble center_x, gdouble center_y, gboolean clip_result); static void pika_group_layer_transform (PikaLayer *layer, PikaContext *context, const PikaMatrix3 *matrix, PikaTransformDirection direction, PikaInterpolationType interpolation_type, PikaTransformResize clip_result, PikaProgress *progress); static void pika_group_layer_convert_type (PikaLayer *layer, PikaImage *dest_image, const Babl *new_format, PikaColorProfile *src_profile, PikaColorProfile *dest_profile, GeglDitherMethod layer_dither_type, GeglDitherMethod mask_dither_type, gboolean push_undo, PikaProgress *progress); static GeglNode * pika_group_layer_get_source_node (PikaDrawable *drawable); static void pika_group_layer_opacity_changed (PikaLayer *layer); static void pika_group_layer_effective_mode_changed (PikaLayer *layer); static void pika_group_layer_excludes_backdrop_changed (PikaLayer *layer); static GeglRectangle pika_group_layer_get_bounding_box (PikaLayer *layer); static void pika_group_layer_get_effective_mode (PikaLayer *layer, PikaLayerMode *mode, PikaLayerColorSpace *blend_space, PikaLayerColorSpace *composite_space, PikaLayerCompositeMode *composite_mode); static gboolean pika_group_layer_get_excludes_backdrop (PikaLayer *layer); static const Babl * pika_group_layer_get_format (PikaProjectable *projectable); static GeglRectangle pika_group_layer_projectable_get_bounding_box (PikaProjectable *projectable); static GeglNode * pika_group_layer_get_graph (PikaProjectable *projectable); static void pika_group_layer_begin_render (PikaProjectable *projectable); static void pika_group_layer_end_render (PikaProjectable *projectable); static void pika_group_layer_pickable_flush (PikaPickable *pickable); static gdouble pika_group_layer_get_opacity_at (PikaPickable *pickable, gint x, gint y); static void pika_group_layer_child_add (PikaContainer *container, PikaLayer *child, PikaGroupLayer *group); static void pika_group_layer_child_remove (PikaContainer *container, PikaLayer *child, PikaGroupLayer *group); static void pika_group_layer_child_move (PikaLayer *child, GParamSpec *pspec, PikaGroupLayer *group); static void pika_group_layer_child_resize (PikaLayer *child, PikaGroupLayer *group); static void pika_group_layer_child_active_changed (PikaLayer *child, PikaGroupLayer *group); static void pika_group_layer_child_effective_mode_changed (PikaLayer *child, PikaGroupLayer *group); static void pika_group_layer_child_excludes_backdrop_changed (PikaLayer *child, PikaGroupLayer *group); static void pika_group_layer_flush (PikaGroupLayer *group); static void pika_group_layer_update (PikaGroupLayer *group); static void pika_group_layer_update_size (PikaGroupLayer *group); static void pika_group_layer_update_mask_size (PikaGroupLayer *group); static void pika_group_layer_update_source_node (PikaGroupLayer *group); static void pika_group_layer_update_mode_node (PikaGroupLayer *group); static void pika_group_layer_stack_update (PikaDrawableStack *stack, gint x, gint y, gint width, gint height, PikaGroupLayer *group); static void pika_group_layer_proj_update (PikaProjection *proj, gboolean now, gint x, gint y, gint width, gint height, PikaGroupLayer *group); G_DEFINE_TYPE_WITH_CODE (PikaGroupLayer, pika_group_layer, PIKA_TYPE_LAYER, G_ADD_PRIVATE (PikaGroupLayer) G_IMPLEMENT_INTERFACE (PIKA_TYPE_PROJECTABLE, pika_projectable_iface_init) G_IMPLEMENT_INTERFACE (PIKA_TYPE_PICKABLE, pika_pickable_iface_init)) #define parent_class pika_group_layer_parent_class /* disable pass-through groups strength-reduction to normal groups. * see pika_group_layer_get_effective_mode(). */ static gboolean no_pass_through_strength_reduction = FALSE; static void pika_group_layer_class_init (PikaGroupLayerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); PikaObjectClass *pika_object_class = PIKA_OBJECT_CLASS (klass); PikaViewableClass *viewable_class = PIKA_VIEWABLE_CLASS (klass); PikaItemClass *item_class = PIKA_ITEM_CLASS (klass); PikaDrawableClass *drawable_class = PIKA_DRAWABLE_CLASS (klass); PikaLayerClass *layer_class = PIKA_LAYER_CLASS (klass); object_class->set_property = pika_group_layer_set_property; object_class->get_property = pika_group_layer_get_property; object_class->finalize = pika_group_layer_finalize; pika_object_class->get_memsize = pika_group_layer_get_memsize; viewable_class->default_icon_name = "pika-group-layer"; viewable_class->ancestry_changed = pika_group_layer_ancestry_changed; viewable_class->get_size = pika_group_layer_get_size; viewable_class->get_children = pika_group_layer_get_children; viewable_class->set_expanded = pika_group_layer_set_expanded; viewable_class->get_expanded = pika_group_layer_get_expanded; item_class->is_position_locked = pika_group_layer_is_position_locked; item_class->duplicate = pika_group_layer_duplicate; item_class->convert = pika_group_layer_convert; item_class->start_transform = pika_group_layer_start_transform; item_class->end_transform = pika_group_layer_end_transform; item_class->resize = pika_group_layer_resize; item_class->get_clip = pika_group_layer_get_clip; item_class->default_name = _("Layer Group"); item_class->rename_desc = C_("undo-type", "Rename Layer Group"); item_class->translate_desc = C_("undo-type", "Move Layer Group"); item_class->scale_desc = C_("undo-type", "Scale Layer Group"); item_class->resize_desc = C_("undo-type", "Resize Layer Group"); item_class->flip_desc = C_("undo-type", "Flip Layer Group"); item_class->rotate_desc = C_("undo-type", "Rotate Layer Group"); item_class->transform_desc = C_("undo-type", "Transform Layer Group"); drawable_class->estimate_memsize = pika_group_layer_estimate_memsize; drawable_class->update_all = pika_group_layer_update_all; drawable_class->get_source_node = pika_group_layer_get_source_node; layer_class->opacity_changed = pika_group_layer_opacity_changed; layer_class->effective_mode_changed = pika_group_layer_effective_mode_changed; layer_class->excludes_backdrop_changed = pika_group_layer_excludes_backdrop_changed; layer_class->translate = pika_group_layer_translate; layer_class->scale = pika_group_layer_scale; layer_class->flip = pika_group_layer_flip; layer_class->rotate = pika_group_layer_rotate; layer_class->transform = pika_group_layer_transform; layer_class->convert_type = pika_group_layer_convert_type; layer_class->get_bounding_box = pika_group_layer_get_bounding_box; layer_class->get_effective_mode = pika_group_layer_get_effective_mode; layer_class->get_excludes_backdrop = pika_group_layer_get_excludes_backdrop; if (g_getenv ("PIKA_NO_PASS_THROUGH_STRENGTH_REDUCTION")) no_pass_through_strength_reduction = TRUE; } static void pika_projectable_iface_init (PikaProjectableInterface *iface) { iface->get_image = (PikaImage * (*) (PikaProjectable *)) pika_item_get_image; iface->get_format = pika_group_layer_get_format; iface->get_offset = (void (*) (PikaProjectable*, gint*, gint*)) pika_item_get_offset; iface->get_bounding_box = pika_group_layer_projectable_get_bounding_box; iface->get_graph = pika_group_layer_get_graph; iface->begin_render = pika_group_layer_begin_render; iface->end_render = pika_group_layer_end_render; iface->invalidate_preview = (void (*) (PikaProjectable*)) pika_viewable_invalidate_preview; } static void pika_pickable_iface_init (PikaPickableInterface *iface) { iface->flush = pika_group_layer_pickable_flush; iface->get_opacity_at = pika_group_layer_get_opacity_at; } static void pika_group_layer_init (PikaGroupLayer *group) { PikaGroupLayerPrivate *private = GET_PRIVATE (group); private->children = pika_layer_stack_new (PIKA_TYPE_LAYER); private->expanded = TRUE; g_signal_connect (private->children, "add", G_CALLBACK (pika_group_layer_child_add), group); g_signal_connect (private->children, "remove", G_CALLBACK (pika_group_layer_child_remove), group); pika_container_add_handler (private->children, "notify::offset-x", G_CALLBACK (pika_group_layer_child_move), group); pika_container_add_handler (private->children, "notify::offset-y", G_CALLBACK (pika_group_layer_child_move), group); pika_container_add_handler (private->children, "size-changed", G_CALLBACK (pika_group_layer_child_resize), group); pika_container_add_handler (private->children, "bounding-box-changed", G_CALLBACK (pika_group_layer_child_resize), group); pika_container_add_handler (private->children, "active-changed", G_CALLBACK (pika_group_layer_child_active_changed), group); pika_container_add_handler (private->children, "effective-mode-changed", G_CALLBACK (pika_group_layer_child_effective_mode_changed), group); pika_container_add_handler (private->children, "excludes-backdrop-changed", G_CALLBACK (pika_group_layer_child_excludes_backdrop_changed), group); g_signal_connect (private->children, "update", G_CALLBACK (pika_group_layer_stack_update), group); private->projection = pika_projection_new (PIKA_PROJECTABLE (group)); pika_projection_set_priority (private->projection, 1); g_signal_connect (private->projection, "update", G_CALLBACK (pika_group_layer_proj_update), group); } static void pika_group_layer_finalize (GObject *object) { PikaGroupLayerPrivate *private = GET_PRIVATE (object); if (private->children) { g_signal_handlers_disconnect_by_func (private->children, pika_group_layer_child_add, object); g_signal_handlers_disconnect_by_func (private->children, pika_group_layer_child_remove, object); g_signal_handlers_disconnect_by_func (private->children, pika_group_layer_stack_update, object); /* this is particularly important to avoid reallocating the projection * in response to a "bounding-box-changed" signal, which can be emitted * during layer destruction. see issue #4584. */ pika_container_remove_handlers_by_data (private->children, object); g_clear_object (&private->children); } g_clear_object (&private->projection); g_clear_object (&private->source_node); g_clear_object (&private->graph); G_OBJECT_CLASS (parent_class)->finalize (object); } static void pika_group_layer_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void pika_group_layer_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static gint64 pika_group_layer_get_memsize (PikaObject *object, gint64 *gui_size) { PikaGroupLayerPrivate *private = GET_PRIVATE (object); gint64 memsize = 0; memsize += pika_object_get_memsize (PIKA_OBJECT (private->children), gui_size); memsize += pika_object_get_memsize (PIKA_OBJECT (private->projection), gui_size); return memsize + PIKA_OBJECT_CLASS (parent_class)->get_memsize (object, gui_size); } static void pika_group_layer_ancestry_changed (PikaViewable *viewable) { PikaGroupLayerPrivate *private = GET_PRIVATE (viewable); pika_projection_set_priority (private->projection, pika_viewable_get_depth (viewable) + 1); PIKA_VIEWABLE_CLASS (parent_class)->ancestry_changed (viewable); } static gboolean pika_group_layer_get_size (PikaViewable *viewable, gint *width, gint *height) { PikaGroupLayerPrivate *private = GET_PRIVATE (viewable); /* return the size only if there are children ... */ if (! pika_container_is_empty (private->children)) { return PIKA_VIEWABLE_CLASS (parent_class)->get_size (viewable, width, height); } /* ... otherwise, return "no content" */ return FALSE; } static PikaContainer * pika_group_layer_get_children (PikaViewable *viewable) { return GET_PRIVATE (viewable)->children; } static gboolean pika_group_layer_get_expanded (PikaViewable *viewable) { PikaGroupLayer *group = PIKA_GROUP_LAYER (viewable); return GET_PRIVATE (group)->expanded; } static void pika_group_layer_set_expanded (PikaViewable *viewable, gboolean expanded) { PikaGroupLayerPrivate *private = GET_PRIVATE (viewable); if (private->expanded != expanded) { private->expanded = expanded; pika_viewable_expanded_changed (viewable); } } static gboolean pika_group_layer_is_position_locked (PikaItem *item, PikaItem **locked_item, gboolean check_children) { /* Lock position is particular because a locked child locks the group * too. */ if (check_children) { PikaGroupLayerPrivate *private = GET_PRIVATE (item); GList *list; for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaItem *child = list->data; if (pika_item_get_lock_position (child)) { if (locked_item) *locked_item = child; return TRUE; } else if (PIKA_IS_GROUP_LAYER (child) && pika_group_layer_is_position_locked (child, locked_item, TRUE)) { return TRUE; } } } /* And a locked parent locks the group too! Which is handled by parent * implementation of the method. */ return PIKA_ITEM_CLASS (parent_class)->is_position_locked (item, locked_item, FALSE); } static PikaItem * pika_group_layer_duplicate (PikaItem *item, GType new_type) { PikaItem *new_item; g_return_val_if_fail (g_type_is_a (new_type, PIKA_TYPE_DRAWABLE), NULL); new_item = PIKA_ITEM_CLASS (parent_class)->duplicate (item, new_type); if (PIKA_IS_GROUP_LAYER (new_item)) { PikaGroupLayerPrivate *private = GET_PRIVATE (item); PikaGroupLayer *new_group = PIKA_GROUP_LAYER (new_item); PikaGroupLayerPrivate *new_private = GET_PRIVATE (new_item); gint position = 0; GList *list; pika_group_layer_suspend_resize (new_group, FALSE); for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaItem *child = list->data; PikaItem *new_child; PikaLayerMask *mask; new_child = pika_item_duplicate (child, G_TYPE_FROM_INSTANCE (child)); pika_object_set_name (PIKA_OBJECT (new_child), pika_object_get_name (child)); mask = pika_layer_get_mask (PIKA_LAYER (child)); if (mask) { PikaLayerMask *new_mask; new_mask = pika_layer_get_mask (PIKA_LAYER (new_child)); pika_object_set_name (PIKA_OBJECT (new_mask), pika_object_get_name (mask)); } pika_viewable_set_parent (PIKA_VIEWABLE (new_child), PIKA_VIEWABLE (new_group)); pika_container_insert (new_private->children, PIKA_OBJECT (new_child), position++); } /* force the projection to reallocate itself */ GET_PRIVATE (new_group)->reallocate_projection = TRUE; pika_group_layer_resume_resize (new_group, FALSE); } return new_item; } static void pika_group_layer_convert (PikaItem *item, PikaImage *dest_image, GType old_type) { PikaGroupLayerPrivate *private = GET_PRIVATE (item); GList *list; for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaItem *child = list->data; PIKA_ITEM_GET_CLASS (child)->convert (child, dest_image, G_TYPE_FROM_INSTANCE (child)); } PIKA_ITEM_CLASS (parent_class)->convert (item, dest_image, old_type); } static void pika_group_layer_start_transform (PikaItem *item, gboolean push_undo) { _pika_group_layer_start_transform (PIKA_GROUP_LAYER (item), push_undo); if (PIKA_ITEM_CLASS (parent_class)->start_transform) PIKA_ITEM_CLASS (parent_class)->start_transform (item, push_undo); } static void pika_group_layer_end_transform (PikaItem *item, gboolean push_undo) { if (PIKA_ITEM_CLASS (parent_class)->end_transform) PIKA_ITEM_CLASS (parent_class)->end_transform (item, push_undo); _pika_group_layer_end_transform (PIKA_GROUP_LAYER (item), push_undo); } static void pika_group_layer_resize (PikaItem *item, PikaContext *context, PikaFillType fill_type, gint new_width, gint new_height, gint offset_x, gint offset_y) { PikaGroupLayer *group = PIKA_GROUP_LAYER (item); PikaGroupLayerPrivate *private = GET_PRIVATE (item); GList *list; gint x, y; /* we implement PikaItem::resize(), instead of PikaLayer::resize(), so that * PikaLayer doesn't resize the mask. note that pika_item_resize() calls * pika_item_{start,end}_move(), and not pika_item_{start,end}_transform(), * so that mask resizing is handled by pika_group_layer_update_size(). */ x = pika_item_get_offset_x (item) - offset_x; y = pika_item_get_offset_y (item) - offset_y; pika_group_layer_suspend_resize (group, TRUE); list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); while (list) { PikaItem *child = list->data; gint child_width; gint child_height; gint child_x; gint child_y; list = g_list_next (list); if (pika_rectangle_intersect (x, y, new_width, new_height, pika_item_get_offset_x (child), pika_item_get_offset_y (child), pika_item_get_width (child), pika_item_get_height (child), &child_x, &child_y, &child_width, &child_height)) { gint child_offset_x = pika_item_get_offset_x (child) - child_x; gint child_offset_y = pika_item_get_offset_y (child) - child_y; pika_item_resize (child, context, fill_type, child_width, child_height, child_offset_x, child_offset_y); } else if (pika_item_is_attached (item)) { pika_image_remove_layer (pika_item_get_image (item), PIKA_LAYER (child), TRUE, NULL); } else { pika_container_remove (private->children, PIKA_OBJECT (child)); } } pika_group_layer_resume_resize (group, TRUE); } static PikaTransformResize pika_group_layer_get_clip (PikaItem *item, PikaTransformResize clip_result) { /* TODO: add clipping support, by clipping all sublayers as a unit, instead * of individually. */ return PIKA_TRANSFORM_RESIZE_ADJUST; } static gint64 pika_group_layer_estimate_memsize (PikaDrawable *drawable, PikaComponentType component_type, gint width, gint height) { PikaGroupLayerPrivate *private = GET_PRIVATE (drawable); GList *list; PikaImageBaseType base_type; gint64 memsize = 0; for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaDrawable *child = list->data; gint child_width; gint child_height; child_width = (pika_item_get_width (PIKA_ITEM (child)) * width / pika_item_get_width (PIKA_ITEM (drawable))); child_height = (pika_item_get_height (PIKA_ITEM (child)) * height / pika_item_get_height (PIKA_ITEM (drawable))); memsize += pika_drawable_estimate_memsize (child, component_type, child_width, child_height); } base_type = pika_drawable_get_base_type (drawable); memsize += pika_projection_estimate_memsize (base_type, component_type, width, height); return memsize + PIKA_DRAWABLE_CLASS (parent_class)->estimate_memsize (drawable, component_type, width, height); } static void pika_group_layer_update_all (PikaDrawable *drawable) { PikaGroupLayerPrivate *private = GET_PRIVATE (drawable); GList *list; /* redirect stack updates to the drawable, rather than to the projection */ private->direct_update++; for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaFilter *child = list->data; if (pika_filter_get_active (child)) pika_drawable_update_all (PIKA_DRAWABLE (child)); } /* redirect stack updates back to the projection */ private->direct_update--; } static void pika_group_layer_translate (PikaLayer *layer, gint offset_x, gint offset_y) { PikaGroupLayer *group = PIKA_GROUP_LAYER (layer); PikaGroupLayerPrivate *private = GET_PRIVATE (layer); gint x, y; GList *list; /* don't use pika_group_layer_suspend_resize(), but rather increment * private->suspend_resize directly, since we're translating the group layer * here, rather than relying on pika_group_layer_update_size() to do it. */ private->suspend_resize++; /* redirect stack updates to the drawable, rather than to the projection */ private->direct_update++; /* translate the child layers *before* updating the group's offset, so that, * if this is a nested group, the parent's bounds still reflect the original * layer positions. This prevents the original area of the child layers, * which is updated as part of their translation, from being clipped to the * post-translation parent bounds (see issue #3484). The new area of the * child layers, which is likewise updated as part of translation, *may* get * clipped to the old parent bounds, but the corresponding region will be * updated anyway when the parent is resized, once we update the group's * offset. */ for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaItem *child = list->data; /* don't push an undo here because undo will call us again */ pika_item_translate (child, offset_x, offset_y, FALSE); } pika_item_get_offset (PIKA_ITEM (group), &x, &y); x += offset_x; y += offset_y; /* update the offset node */ if (private->offset_node) gegl_node_set (private->offset_node, "x", (gdouble) -x, "y", (gdouble) -y, NULL); /* invalidate the selection boundary because of a layer modification */ pika_drawable_invalidate_boundary (PIKA_DRAWABLE (layer)); /* update the group layer offset */ pika_item_set_offset (PIKA_ITEM (group), x, y); /* redirect stack updates back to the projection */ private->direct_update--; /* don't use pika_group_layer_resume_resize(), but rather decrement * private->suspend_resize directly, so that pika_group_layer_update_size() * isn't called. */ private->suspend_resize--; } static void pika_group_layer_scale (PikaLayer *layer, gint new_width, gint new_height, gint new_offset_x, gint new_offset_y, PikaInterpolationType interpolation_type, PikaProgress *progress) { PikaGroupLayer *group = PIKA_GROUP_LAYER (layer); PikaGroupLayerPrivate *private = GET_PRIVATE (layer); PikaItem *item = PIKA_ITEM (layer); PikaObjectQueue *queue = NULL; GList *list; gdouble width_factor; gdouble height_factor; gint old_offset_x; gint old_offset_y; width_factor = (gdouble) new_width / (gdouble) pika_item_get_width (item); height_factor = (gdouble) new_height / (gdouble) pika_item_get_height (item); old_offset_x = pika_item_get_offset_x (item); old_offset_y = pika_item_get_offset_y (item); if (progress) { queue = pika_object_queue_new (progress); progress = PIKA_PROGRESS (queue); pika_object_queue_push_container (queue, private->children); } pika_group_layer_suspend_resize (group, TRUE); list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); while (list) { PikaItem *child = list->data; list = g_list_next (list); if (queue) pika_object_queue_pop (queue); if (! pika_item_scale_by_factors_with_origin (child, width_factor, height_factor, old_offset_x, old_offset_y, new_offset_x, new_offset_y, interpolation_type, progress)) { /* new width or height are 0; remove item */ if (pika_item_is_attached (item)) { pika_image_remove_layer (pika_item_get_image (item), PIKA_LAYER (child), TRUE, NULL); } else { pika_container_remove (private->children, PIKA_OBJECT (child)); } } } pika_group_layer_resume_resize (group, TRUE); g_clear_object (&queue); } static void pika_group_layer_flip (PikaLayer *layer, PikaContext *context, PikaOrientationType flip_type, gdouble axis, gboolean clip_result) { PikaGroupLayer *group = PIKA_GROUP_LAYER (layer); PikaGroupLayerPrivate *private = GET_PRIVATE (layer); GList *list; pika_group_layer_suspend_resize (group, TRUE); for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaItem *child = list->data; pika_item_flip (child, context, flip_type, axis, clip_result); } pika_group_layer_resume_resize (group, TRUE); } static void pika_group_layer_rotate (PikaLayer *layer, PikaContext *context, PikaRotationType rotate_type, gdouble center_x, gdouble center_y, gboolean clip_result) { PikaGroupLayer *group = PIKA_GROUP_LAYER (layer); PikaGroupLayerPrivate *private = GET_PRIVATE (layer); GList *list; pika_group_layer_suspend_resize (group, TRUE); for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaItem *child = list->data; pika_item_rotate (child, context, rotate_type, center_x, center_y, clip_result); } pika_group_layer_resume_resize (group, TRUE); } static void pika_group_layer_transform (PikaLayer *layer, PikaContext *context, const PikaMatrix3 *matrix, PikaTransformDirection direction, PikaInterpolationType interpolation_type, PikaTransformResize clip_result, PikaProgress *progress) { PikaGroupLayer *group = PIKA_GROUP_LAYER (layer); PikaGroupLayerPrivate *private = GET_PRIVATE (layer); PikaObjectQueue *queue = NULL; GList *list; if (progress) { queue = pika_object_queue_new (progress); progress = PIKA_PROGRESS (queue); pika_object_queue_push_container (queue, private->children); } pika_group_layer_suspend_resize (group, TRUE); for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaItem *child = list->data; if (queue) pika_object_queue_pop (queue); pika_item_transform (child, context, matrix, direction, interpolation_type, clip_result, progress); } pika_group_layer_resume_resize (group, TRUE); g_clear_object (&queue); } static const Babl * get_projection_format (PikaProjectable *projectable, PikaImageBaseType base_type, PikaPrecision precision) { PikaImage *image = pika_item_get_image (PIKA_ITEM (projectable)); switch (base_type) { case PIKA_RGB: case PIKA_INDEXED: return pika_image_get_format (image, PIKA_RGB, precision, TRUE, pika_image_get_layer_space (image)); case PIKA_GRAY: return pika_image_get_format (image, PIKA_GRAY, precision, TRUE, pika_image_get_layer_space (image)); } g_return_val_if_reached (NULL); } static void pika_group_layer_convert_type (PikaLayer *layer, PikaImage *dest_image, const Babl *new_format, PikaColorProfile *src_profile, PikaColorProfile *dest_profile, GeglDitherMethod layer_dither_type, GeglDitherMethod mask_dither_type, gboolean push_undo, PikaProgress *progress) { PikaGroupLayer *group = PIKA_GROUP_LAYER (layer); PikaGroupLayerPrivate *private = GET_PRIVATE (layer); GeglBuffer *buffer; if (push_undo) { PikaImage *image = pika_item_get_image (PIKA_ITEM (group)); pika_image_undo_push_group_layer_convert (image, NULL, group); } /* Need to temporarily set the projectable's format to the new * values so the projection will create its tiles with the right * depth */ private->convert_format = get_projection_format (PIKA_PROJECTABLE (group), pika_babl_format_get_base_type (new_format), pika_babl_format_get_precision (new_format)); pika_projectable_structure_changed (PIKA_PROJECTABLE (group)); pika_group_layer_flush (group); buffer = pika_pickable_get_buffer (PIKA_PICKABLE (private->projection)); pika_drawable_set_buffer_full (PIKA_DRAWABLE (group), FALSE, NULL, buffer, NULL, TRUE); /* reset, the actual format is right now */ private->convert_format = NULL; } static GeglNode * pika_group_layer_get_source_node (PikaDrawable *drawable) { PikaGroupLayerPrivate *private = GET_PRIVATE (drawable); GeglNode *input; g_warn_if_fail (private->source_node == NULL); private->source_node = gegl_node_new (); input = gegl_node_get_input_proxy (private->source_node, "input"); private->parent_source_node = PIKA_DRAWABLE_CLASS (parent_class)->get_source_node (drawable); gegl_node_add_child (private->source_node, private->parent_source_node); g_object_unref (private->parent_source_node); if (gegl_node_has_pad (private->parent_source_node, "input")) { gegl_node_link (input, private->parent_source_node); } /* make sure we have a graph */ (void) pika_group_layer_get_graph (PIKA_PROJECTABLE (drawable)); gegl_node_add_child (private->source_node, private->graph); pika_group_layer_update_source_node (PIKA_GROUP_LAYER (drawable)); return g_object_ref (private->source_node); } static void pika_group_layer_opacity_changed (PikaLayer *layer) { pika_layer_update_effective_mode (layer); if (PIKA_LAYER_CLASS (parent_class)->opacity_changed) PIKA_LAYER_CLASS (parent_class)->opacity_changed (layer); } static void pika_group_layer_effective_mode_changed (PikaLayer *layer) { PikaGroupLayer *group = PIKA_GROUP_LAYER (layer); PikaGroupLayerPrivate *private = GET_PRIVATE (layer); PikaLayerMode mode; gboolean pass_through; gboolean update_bounding_box = FALSE; pika_layer_get_effective_mode (layer, &mode, NULL, NULL, NULL); pass_through = (mode == PIKA_LAYER_MODE_PASS_THROUGH); if (pass_through != private->pass_through) { if (private->pass_through && ! pass_through) { /* when switching from pass-through mode to a non-pass-through mode, * flush the pickable in order to make sure the projection's buffer * gets properly invalidated synchronously, so that it can be used * as a source for the rest of the composition. */ pika_pickable_flush (PIKA_PICKABLE (private->projection)); } private->pass_through = pass_through; update_bounding_box = TRUE; } pika_group_layer_update_source_node (group); pika_group_layer_update_mode_node (group); if (update_bounding_box) pika_drawable_update_bounding_box (PIKA_DRAWABLE (group)); if (PIKA_LAYER_CLASS (parent_class)->effective_mode_changed) PIKA_LAYER_CLASS (parent_class)->effective_mode_changed (layer); } static void pika_group_layer_excludes_backdrop_changed (PikaLayer *layer) { PikaGroupLayer *group = PIKA_GROUP_LAYER (layer); pika_group_layer_update_source_node (group); pika_group_layer_update_mode_node (group); if (PIKA_LAYER_CLASS (parent_class)->excludes_backdrop_changed) PIKA_LAYER_CLASS (parent_class)->excludes_backdrop_changed (layer); } static GeglRectangle pika_group_layer_get_bounding_box (PikaLayer *layer) { PikaGroupLayerPrivate *private = GET_PRIVATE (layer); /* for pass-through groups, use the group's calculated bounding box, instead * of the source-node's bounding box, since we don't update the bounding box * on all events that may affect the latter, and since it includes the * bounding box of the backdrop. this means we can't attach filters that may * affect the bounding box to a pass-through group (since their effect weon't * be reflected by the group's bounding box), but attaching filters to pass- * through groups makes little sense anyway. */ if (private->pass_through) return private->bounding_box; else return PIKA_LAYER_CLASS (parent_class)->get_bounding_box (layer); } static void pika_group_layer_get_effective_mode (PikaLayer *layer, PikaLayerMode *mode, PikaLayerColorSpace *blend_space, PikaLayerColorSpace *composite_space, PikaLayerCompositeMode *composite_mode) { PikaGroupLayerPrivate *private = GET_PRIVATE (layer); /* try to strength-reduce pass-through groups to normal groups, which are * cheaper. */ if (pika_layer_get_mode (layer) == PIKA_LAYER_MODE_PASS_THROUGH && ! no_pass_through_strength_reduction) { /* we perform the strength-reduction if: * * - the group has no active children; * * or, * * - the group has a single active child; or, * * - the effective mode of all the active children is normal, their * effective composite mode is UNION, and their effective blend and * composite spaces are equal; * * - and, * * - the group's opacity is 100%, and it has no mask (or the mask * isn't applied); or, * * - the group's composite space equals the active children's * composite space. */ GList *list; gboolean reduce = TRUE; gboolean first = TRUE; *mode = PIKA_LAYER_MODE_NORMAL; *blend_space = pika_layer_get_real_blend_space (layer); *composite_space = pika_layer_get_real_composite_space (layer); *composite_mode = pika_layer_get_real_composite_mode (layer); for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaLayer *child = list->data; if (! pika_filter_get_active (PIKA_FILTER (child))) continue; if (first) { pika_layer_get_effective_mode (child, mode, blend_space, composite_space, composite_mode); if (*mode == PIKA_LAYER_MODE_NORMAL_LEGACY) *mode = PIKA_LAYER_MODE_NORMAL; first = FALSE; } else { PikaLayerMode other_mode; PikaLayerColorSpace other_blend_space; PikaLayerColorSpace other_composite_space; PikaLayerCompositeMode other_composite_mode; if (*mode != PIKA_LAYER_MODE_NORMAL || *composite_mode != PIKA_LAYER_COMPOSITE_UNION) { reduce = FALSE; break; } pika_layer_get_effective_mode (child, &other_mode, &other_blend_space, &other_composite_space, &other_composite_mode); if (other_mode == PIKA_LAYER_MODE_NORMAL_LEGACY) other_mode = PIKA_LAYER_MODE_NORMAL; if (other_mode != *mode || other_blend_space != *blend_space || other_composite_space != *composite_space || other_composite_mode != *composite_mode) { reduce = FALSE; break; } } } if (reduce) { gboolean has_mask; has_mask = pika_layer_get_mask (layer) && pika_layer_get_apply_mask (layer); if (first || (pika_layer_get_opacity (layer) == PIKA_OPACITY_OPAQUE && ! has_mask) || *composite_space == pika_layer_get_real_composite_space (layer)) { /* strength reduction succeeded! */ return; } } } /* strength-reduction failed. chain up. */ PIKA_LAYER_CLASS (parent_class)->get_effective_mode (layer, mode, blend_space, composite_space, composite_mode); } static gboolean pika_group_layer_get_excludes_backdrop (PikaLayer *layer) { PikaGroupLayerPrivate *private = GET_PRIVATE (layer); if (private->pass_through) { GList *list; for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaFilter *child = list->data; if (pika_filter_get_active (child) && pika_layer_get_excludes_backdrop (PIKA_LAYER (child))) return TRUE; } return FALSE; } else return PIKA_LAYER_CLASS (parent_class)->get_excludes_backdrop (layer); } static const Babl * pika_group_layer_get_format (PikaProjectable *projectable) { PikaGroupLayerPrivate *private = GET_PRIVATE (projectable); PikaImageBaseType base_type; PikaPrecision precision; if (private->convert_format) return private->convert_format; base_type = pika_drawable_get_base_type (PIKA_DRAWABLE (projectable)); precision = pika_drawable_get_precision (PIKA_DRAWABLE (projectable)); return get_projection_format (projectable, base_type, precision); } static GeglRectangle pika_group_layer_projectable_get_bounding_box (PikaProjectable *projectable) { PikaGroupLayerPrivate *private = GET_PRIVATE (projectable); return private->bounding_box; } static GeglNode * pika_group_layer_get_graph (PikaProjectable *projectable) { PikaGroupLayer *group = PIKA_GROUP_LAYER (projectable); PikaGroupLayerPrivate *private = GET_PRIVATE (projectable); GeglNode *input; GeglNode *layers_node; GeglNode *output; gint off_x; gint off_y; if (private->graph) return private->graph; private->graph = gegl_node_new (); input = gegl_node_get_input_proxy (private->graph, "input"); layers_node = pika_filter_stack_get_graph (PIKA_FILTER_STACK (private->children)); gegl_node_add_child (private->graph, layers_node); gegl_node_link (input, layers_node); pika_item_get_offset (PIKA_ITEM (group), &off_x, &off_y); private->offset_node = gegl_node_new_child (private->graph, "operation", "gegl:translate", "x", (gdouble) -off_x, "y", (gdouble) -off_y, NULL); gegl_node_link (layers_node, private->offset_node); output = gegl_node_get_output_proxy (private->graph, "output"); gegl_node_link (private->offset_node, output); return private->graph; } static void pika_group_layer_begin_render (PikaProjectable *projectable) { PikaGroupLayerPrivate *private = GET_PRIVATE (projectable); if (private->source_node == NULL) return; if (private->pass_through) gegl_node_disconnect (private->graph, "input"); } static void pika_group_layer_end_render (PikaProjectable *projectable) { PikaGroupLayerPrivate *private = GET_PRIVATE (projectable); if (private->source_node == NULL) return; if (private->pass_through) { GeglNode *input; input = gegl_node_get_input_proxy (private->source_node, "input"); gegl_node_link (input, private->graph); } } static void pika_group_layer_pickable_flush (PikaPickable *pickable) { PikaGroupLayerPrivate *private = GET_PRIVATE (pickable); pika_pickable_flush (PIKA_PICKABLE (private->projection)); } static gdouble pika_group_layer_get_opacity_at (PikaPickable *pickable, gint x, gint y) { /* Only consider child layers as having content */ return PIKA_OPACITY_TRANSPARENT; } /* public functions */ PikaLayer * pika_group_layer_new (PikaImage *image) { PikaGroupLayer *group; const Babl *format; g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL); format = pika_image_get_layer_format (image, TRUE); group = PIKA_GROUP_LAYER (pika_drawable_new (PIKA_TYPE_GROUP_LAYER, image, NULL, 0, 0, 1, 1, format)); pika_layer_set_mode (PIKA_LAYER (group), pika_image_get_default_new_layer_mode (image), FALSE); return PIKA_LAYER (group); } PikaProjection * pika_group_layer_get_projection (PikaGroupLayer *group) { g_return_val_if_fail (PIKA_IS_GROUP_LAYER (group), NULL); return GET_PRIVATE (group)->projection; } void pika_group_layer_suspend_resize (PikaGroupLayer *group, gboolean push_undo) { PikaItem *item; g_return_if_fail (PIKA_IS_GROUP_LAYER (group)); item = PIKA_ITEM (group); if (! pika_item_is_attached (item)) push_undo = FALSE; if (push_undo) pika_image_undo_push_group_layer_suspend_resize (pika_item_get_image (item), NULL, group); GET_PRIVATE (group)->suspend_resize++; } void pika_group_layer_resume_resize (PikaGroupLayer *group, gboolean push_undo) { PikaGroupLayerPrivate *private; PikaItem *item; PikaItem *mask = NULL; GeglBuffer *mask_buffer; GeglRectangle mask_bounds; PikaUndo *undo; g_return_if_fail (PIKA_IS_GROUP_LAYER (group)); private = GET_PRIVATE (group); g_return_if_fail (private->suspend_resize > 0); item = PIKA_ITEM (group); if (! pika_item_is_attached (item)) push_undo = FALSE; if (push_undo) { undo = pika_image_undo_push_group_layer_resume_resize (pika_item_get_image (item), NULL, group); /* if there were any {suspend,resume}_mask() calls during the time the * group's size was suspended, the resume_mask() calls will not have seen * any changes to the mask, and will therefore won't restore the mask * during undo. if the group's bounding box did change while resize was * suspended, and if there are no other {suspend,resume}_mask() blocks * that will see the resized mask, we have to restore the mask during the * resume_resize() undo. * * we ref the mask buffer here, and compare it to the mask buffer after * updating the size. */ if (private->suspend_resize == 1 && private->suspend_mask == 0) { mask = PIKA_ITEM (pika_layer_get_mask (PIKA_LAYER (group))); if (mask) { mask_buffer = g_object_ref (pika_drawable_get_buffer (PIKA_DRAWABLE (mask))); mask_bounds.x = pika_item_get_offset_x (mask); mask_bounds.y = pika_item_get_offset_y (mask); mask_bounds.width = pika_item_get_width (mask); mask_bounds.height = pika_item_get_height (mask); } } } private->suspend_resize--; if (private->suspend_resize == 0) { pika_group_layer_update_size (group); if (mask) { /* if the mask changed, make sure it's restored during undo, as per * the comment above. */ if (pika_drawable_get_buffer (PIKA_DRAWABLE (mask)) != mask_buffer) { g_return_if_fail (undo != NULL); PIKA_GROUP_LAYER_UNDO (undo)->mask_buffer = mask_buffer; PIKA_GROUP_LAYER_UNDO (undo)->mask_bounds = mask_bounds; } else { g_object_unref (mask_buffer); } } } } void pika_group_layer_suspend_mask (PikaGroupLayer *group, gboolean push_undo) { PikaGroupLayerPrivate *private; PikaItem *item; g_return_if_fail (PIKA_IS_GROUP_LAYER (group)); private = GET_PRIVATE (group); item = PIKA_ITEM (group); /* avoid pushing an undo step if this is a nested suspend_mask() call, since * the value of 'push_undo' in nested calls should be the same as that passed * to the outermost call, and only pushing an undo step for the outermost * call in this case is enough. we can't support cases where the values of * 'push_undo' in nested calls are different in a meaningful way, and * avoiding undo steps for nested calls prevents us from storing multiple * references to the suspend mask buffer on the undo stack. while storing * multiple references to the buffer doesn't waste any memory (since all the * references are to the same buffer), it does cause the undo stack memory- * usage estimation to overshoot, potentially resulting in undo steps being * dropped unnecessarily. */ if (! pika_item_is_attached (item) || private->suspend_mask > 0) push_undo = FALSE; if (push_undo) pika_image_undo_push_group_layer_suspend_mask (pika_item_get_image (item), NULL, group); if (private->suspend_mask == 0) { if (pika_layer_get_mask (PIKA_LAYER (group))) { PikaItem *mask = PIKA_ITEM (pika_layer_get_mask (PIKA_LAYER (group))); private->suspended_mask_buffer = g_object_ref (pika_drawable_get_buffer (PIKA_DRAWABLE (mask))); private->suspended_mask_bounds.x = pika_item_get_offset_x (mask); private->suspended_mask_bounds.y = pika_item_get_offset_y (mask); private->suspended_mask_bounds.width = pika_item_get_width (mask); private->suspended_mask_bounds.height = pika_item_get_height (mask); } else { private->suspended_mask_buffer = NULL; } } private->suspend_mask++; } void pika_group_layer_resume_mask (PikaGroupLayer *group, gboolean push_undo) { PikaGroupLayerPrivate *private; PikaItem *item; g_return_if_fail (PIKA_IS_GROUP_LAYER (group)); private = GET_PRIVATE (group); g_return_if_fail (private->suspend_mask > 0); item = PIKA_ITEM (group); /* avoid pushing an undo step if this is a nested resume_mask() call. see * the comment in pika_group_layer_suspend_mask(). */ if (! pika_item_is_attached (item) || private->suspend_mask > 1) push_undo = FALSE; if (push_undo) pika_image_undo_push_group_layer_resume_mask (pika_item_get_image (item), NULL, group); private->suspend_mask--; if (private->suspend_mask == 0) g_clear_object (&private->suspended_mask_buffer); } /* protected functions */ void _pika_group_layer_set_suspended_mask (PikaGroupLayer *group, GeglBuffer *buffer, const GeglRectangle *bounds) { PikaGroupLayerPrivate *private; g_return_if_fail (PIKA_IS_GROUP_LAYER (group)); g_return_if_fail (buffer != NULL); g_return_if_fail (bounds != NULL); private = GET_PRIVATE (group); g_return_if_fail (private->suspend_mask > 0); g_object_ref (buffer); g_clear_object (&private->suspended_mask_buffer); private->suspended_mask_buffer = buffer; private->suspended_mask_bounds = *bounds; } GeglBuffer * _pika_group_layer_get_suspended_mask (PikaGroupLayer *group, GeglRectangle *bounds) { PikaGroupLayerPrivate *private; PikaLayerMask *mask; g_return_val_if_fail (PIKA_IS_GROUP_LAYER (group), NULL); g_return_val_if_fail (bounds != NULL, NULL); private = GET_PRIVATE (group); mask = pika_layer_get_mask (PIKA_LAYER (group)); g_return_val_if_fail (private->suspend_mask > 0, NULL); if (mask && pika_drawable_get_buffer (PIKA_DRAWABLE (mask)) != private->suspended_mask_buffer) { *bounds = private->suspended_mask_bounds; return private->suspended_mask_buffer; } return NULL; } void _pika_group_layer_start_transform (PikaGroupLayer *group, gboolean push_undo) { PikaGroupLayerPrivate *private; PikaItem *item; g_return_if_fail (PIKA_IS_GROUP_LAYER (group)); private = GET_PRIVATE (group); item = PIKA_ITEM (group); g_return_if_fail (private->suspend_mask == 0); if (! pika_item_is_attached (item)) push_undo = FALSE; if (push_undo) pika_image_undo_push_group_layer_start_transform (pika_item_get_image (item), NULL, group); private->transforming++; } void _pika_group_layer_end_transform (PikaGroupLayer *group, gboolean push_undo) { PikaGroupLayerPrivate *private; PikaItem *item; g_return_if_fail (PIKA_IS_GROUP_LAYER (group)); private = GET_PRIVATE (group); item = PIKA_ITEM (group); g_return_if_fail (private->suspend_mask == 0); g_return_if_fail (private->transforming > 0); if (! pika_item_is_attached (item)) push_undo = FALSE; if (push_undo) pika_image_undo_push_group_layer_end_transform (pika_item_get_image (item), NULL, group); private->transforming--; if (private->transforming == 0) pika_group_layer_update_mask_size (PIKA_GROUP_LAYER (item)); } /* private functions */ static void pika_group_layer_child_add (PikaContainer *container, PikaLayer *child, PikaGroupLayer *group) { pika_group_layer_update (group); if (pika_filter_get_active (PIKA_FILTER (child))) { pika_layer_update_effective_mode (PIKA_LAYER (group)); if (pika_layer_get_excludes_backdrop (child)) pika_layer_update_excludes_backdrop (PIKA_LAYER (group)); } } static void pika_group_layer_child_remove (PikaContainer *container, PikaLayer *child, PikaGroupLayer *group) { pika_group_layer_update (group); if (pika_filter_get_active (PIKA_FILTER (child))) { pika_layer_update_effective_mode (PIKA_LAYER (group)); if (pika_layer_get_excludes_backdrop (child)) pika_layer_update_excludes_backdrop (PIKA_LAYER (group)); } } static void pika_group_layer_child_move (PikaLayer *child, GParamSpec *pspec, PikaGroupLayer *group) { pika_group_layer_update (group); } static void pika_group_layer_child_resize (PikaLayer *child, PikaGroupLayer *group) { pika_group_layer_update (group); } static void pika_group_layer_child_active_changed (PikaLayer *child, PikaGroupLayer *group) { pika_layer_update_effective_mode (PIKA_LAYER (group)); if (pika_layer_get_excludes_backdrop (child)) pika_layer_update_excludes_backdrop (PIKA_LAYER (group)); } static void pika_group_layer_child_effective_mode_changed (PikaLayer *child, PikaGroupLayer *group) { if (pika_filter_get_active (PIKA_FILTER (child))) pika_layer_update_effective_mode (PIKA_LAYER (group)); } static void pika_group_layer_child_excludes_backdrop_changed (PikaLayer *child, PikaGroupLayer *group) { if (pika_filter_get_active (PIKA_FILTER (child))) pika_layer_update_excludes_backdrop (PIKA_LAYER (group)); } static void pika_group_layer_flush (PikaGroupLayer *group) { PikaGroupLayerPrivate *private = GET_PRIVATE (group); if (private->pass_through) { /* flush the projectable, not the pickable, because the source * node of pass-through groups doesn't use the projection's * buffer, hence there's no need to invalidate it synchronously. */ pika_projectable_flush (PIKA_PROJECTABLE (group), TRUE); } else { /* make sure we have a buffer, and stop any idle rendering, which is * initiated when a new buffer is allocated. the call to * pika_pickable_flush() below causes any pending idle rendering to * finish synchronously, so this needs to happen before. */ pika_pickable_get_buffer (PIKA_PICKABLE (private->projection)); pika_projection_stop_rendering (private->projection); /* flush the pickable not the projectable because flushing the * pickable will finish all invalidation on the projection so it * can be used as source (note that it will still be constructed * when the actual read happens, so this it not a performance * problem) */ pika_pickable_flush (PIKA_PICKABLE (private->projection)); } } static void pika_group_layer_update (PikaGroupLayer *group) { if (GET_PRIVATE (group)->suspend_resize == 0) { pika_group_layer_update_size (group); } } static void pika_group_layer_update_size (PikaGroupLayer *group) { PikaGroupLayerPrivate *private = GET_PRIVATE (group); PikaItem *item = PIKA_ITEM (group); PikaLayer *layer = PIKA_LAYER (group); PikaItem *mask = PIKA_ITEM (pika_layer_get_mask (layer)); GeglRectangle old_bounds; GeglRectangle bounds; GeglRectangle old_bounding_box; GeglRectangle bounding_box; gboolean first = TRUE; gboolean size_changed; gboolean resize_mask; GList *list; old_bounds.x = pika_item_get_offset_x (item); old_bounds.y = pika_item_get_offset_y (item); old_bounds.width = pika_item_get_width (item); old_bounds.height = pika_item_get_height (item); bounds.x = 0; bounds.y = 0; bounds.width = 1; bounds.height = 1; old_bounding_box = private->bounding_box; bounding_box = bounds; for (list = pika_item_stack_get_item_iter (PIKA_ITEM_STACK (private->children)); list; list = g_list_next (list)) { PikaItem *child = list->data; GeglRectangle child_bounds; GeglRectangle child_bounding_box; if (! pika_viewable_get_size (PIKA_VIEWABLE (child), &child_bounds.width, &child_bounds.height)) { /* ignore children without content (empty group layers); * see bug 777017 */ continue; } pika_item_get_offset (child, &child_bounds.x, &child_bounds.y); child_bounding_box = pika_drawable_get_bounding_box (PIKA_DRAWABLE (child)); child_bounding_box.x += child_bounds.x; child_bounding_box.y += child_bounds.y; if (first) { bounds = child_bounds; bounding_box = child_bounding_box; first = FALSE; } else { gegl_rectangle_bounding_box (&bounds, &bounds, &child_bounds); gegl_rectangle_bounding_box (&bounding_box, &bounding_box, &child_bounding_box); } } bounding_box.x -= bounds.x; bounding_box.y -= bounds.y; size_changed = ! (gegl_rectangle_equal (&bounds, &old_bounds) && gegl_rectangle_equal (&bounding_box, &old_bounding_box)); resize_mask = mask && ! gegl_rectangle_equal (&bounds, &old_bounds); /* if we show the mask, invalidate the old mask area */ if (resize_mask && pika_layer_get_show_mask (layer)) { pika_drawable_update (PIKA_DRAWABLE (group), pika_item_get_offset_x (mask) - old_bounds.x, pika_item_get_offset_y (mask) - old_bounds.y, pika_item_get_width (mask), pika_item_get_height (mask)); } if (private->reallocate_projection || size_changed) { GeglBuffer *buffer; /* if the graph is already constructed, set the offset node's * coordinates first, so the graph is in the right state when * the projection is reallocated, see bug #730550. */ if (private->offset_node) gegl_node_set (private->offset_node, "x", (gdouble) -bounds.x, "y", (gdouble) -bounds.y, NULL); /* update our offset *before* calling pika_pickable_get_buffer(), so * that if our graph isn't constructed yet, the offset node picks * up the right coordinates in pika_group_layer_get_graph(). */ pika_item_set_offset (item, bounds.x, bounds.y); /* update the bounding box before updating the projection, so that it * picks up the right size. */ private->bounding_box = bounding_box; if (private->reallocate_projection) { private->reallocate_projection = FALSE; pika_projectable_structure_changed (PIKA_PROJECTABLE (group)); } else { /* when there's no need to reallocate the projection, we call * pika_projectable_bounds_changed(), rather than structure_chaned(), * so that the projection simply copies the old content over to the * new buffer with an offset, rather than re-renders the graph. */ pika_projectable_bounds_changed (PIKA_PROJECTABLE (group), old_bounds.x, old_bounds.y); } buffer = pika_pickable_get_buffer (PIKA_PICKABLE (private->projection)); pika_drawable_set_buffer_full (PIKA_DRAWABLE (group), FALSE, NULL, buffer, &bounds, FALSE /* don't update the drawable, the * flush() below will take care of * that. */); pika_group_layer_flush (group); } /* resize the mask if not transforming (in which case, PikaLayer takes care * of the mask) */ if (resize_mask && ! private->transforming) pika_group_layer_update_mask_size (group); /* if we show the mask, invalidate the new mask area */ if (resize_mask && pika_layer_get_show_mask (layer)) { pika_drawable_update (PIKA_DRAWABLE (group), pika_item_get_offset_x (mask) - bounds.x, pika_item_get_offset_y (mask) - bounds.y, pika_item_get_width (mask), pika_item_get_height (mask)); } } static void pika_group_layer_update_mask_size (PikaGroupLayer *group) { PikaGroupLayerPrivate *private = GET_PRIVATE (group); PikaItem *item = PIKA_ITEM (group); PikaItem *mask; GeglBuffer *buffer; GeglBuffer *mask_buffer; GeglRectangle bounds; GeglRectangle mask_bounds; GeglRectangle copy_bounds; gboolean intersect; mask = PIKA_ITEM (pika_layer_get_mask (PIKA_LAYER (group))); if (! mask) return; bounds.x = pika_item_get_offset_x (item); bounds.y = pika_item_get_offset_y (item); bounds.width = pika_item_get_width (item); bounds.height = pika_item_get_height (item); mask_bounds.x = pika_item_get_offset_x (mask); mask_bounds.y = pika_item_get_offset_y (mask); mask_bounds.width = pika_item_get_width (mask); mask_bounds.height = pika_item_get_height (mask); if (gegl_rectangle_equal (&bounds, &mask_bounds)) return; buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, bounds.width, bounds.height), pika_drawable_get_format (PIKA_DRAWABLE (mask))); if (private->suspended_mask_buffer) { /* copy the suspended mask into the new mask */ mask_buffer = private->suspended_mask_buffer; mask_bounds = private->suspended_mask_bounds; } else { /* copy the old mask into the new mask */ mask_buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (mask)); } intersect = pika_rectangle_intersect (bounds.x, bounds.y, bounds.width, bounds.height, mask_bounds.x, mask_bounds.y, mask_bounds.width, mask_bounds.height, ©_bounds.x, ©_bounds.y, ©_bounds.width, ©_bounds.height); if (intersect) { pika_gegl_buffer_copy (mask_buffer, GEGL_RECTANGLE (copy_bounds.x - mask_bounds.x, copy_bounds.y - mask_bounds.y, copy_bounds.width, copy_bounds.height), GEGL_ABYSS_NONE, buffer, GEGL_RECTANGLE (copy_bounds.x - bounds.x, copy_bounds.y - bounds.y, copy_bounds.width, copy_bounds.height)); } pika_drawable_set_buffer_full (PIKA_DRAWABLE (mask), FALSE, NULL, buffer, &bounds, TRUE); g_object_unref (buffer); } static void pika_group_layer_update_source_node (PikaGroupLayer *group) { PikaGroupLayerPrivate *private = GET_PRIVATE (group); GeglNode *input; GeglNode *output; if (private->source_node == NULL) return; input = gegl_node_get_input_proxy (private->source_node, "input"); output = gegl_node_get_output_proxy (private->source_node, "output"); if (private->pass_through) { gegl_node_link (input, private->graph); gegl_node_link (private->graph, output); } else { gegl_node_disconnect (private->graph, "input"); gegl_node_link (private->parent_source_node, output); } } static void pika_group_layer_update_mode_node (PikaGroupLayer *group) { PikaGroupLayerPrivate *private = GET_PRIVATE (group); GeglNode *node; GeglNode *input; GeglNode *mode_node; node = pika_filter_get_node (PIKA_FILTER (group)); input = gegl_node_get_input_proxy (node, "input"); mode_node = pika_drawable_get_mode_node (PIKA_DRAWABLE (group)); if (private->pass_through && pika_layer_get_excludes_backdrop (PIKA_LAYER (group))) { gegl_node_disconnect (mode_node, "input"); } else { gegl_node_link (input, mode_node); } } static void pika_group_layer_stack_update (PikaDrawableStack *stack, gint x, gint y, gint width, gint height, PikaGroupLayer *group) { PikaGroupLayerPrivate *private = GET_PRIVATE (group); #if 0 g_printerr ("%s (%s) %d, %d (%d, %d)\n", G_STRFUNC, pika_object_get_name (group), x, y, width, height); #endif if (! private->direct_update) { /* the layer stack's update signal speaks in image coordinates, * pass to the projection as-is. */ pika_projectable_invalidate (PIKA_PROJECTABLE (group), x, y, width, height); pika_group_layer_flush (group); } if (private->direct_update || private->pass_through) { /* the layer stack's update signal speaks in image coordinates, * transform to layer coordinates when emitting our own update signal. */ pika_drawable_update (PIKA_DRAWABLE (group), x - pika_item_get_offset_x (PIKA_ITEM (group)), y - pika_item_get_offset_y (PIKA_ITEM (group)), width, height); } } static void pika_group_layer_proj_update (PikaProjection *proj, gboolean now, gint x, gint y, gint width, gint height, PikaGroupLayer *group) { PikaGroupLayerPrivate *private = GET_PRIVATE (group); #if 0 g_printerr ("%s (%s) %d, %d (%d, %d)\n", G_STRFUNC, pika_object_get_name (group), x, y, width, height); #endif if (! private->pass_through) { /* TODO: groups can currently have a gegl:transform op attached as a filter * when using a transform tool, in which case the updated region needs * undergo the same transformation. more generally, when a drawable has * filters they may influence the area affected by drawable updates. * * this needs to be addressed much more generally at some point, but for now * we just resort to updating the entire group when it has a filter (i.e., * when it's being used with a transform tool). we restrict this to groups, * and don't do this more generally in pika_drawable_update(), because this * negatively impacts the performance of the warp tool, which does perform * accurate drawable updates while using a filter. */ if (pika_drawable_has_filters (PIKA_DRAWABLE (group))) { width = -1; height = -1; } /* the projection speaks in image coordinates, transform to layer * coordinates when emitting our own update signal. */ pika_drawable_update (PIKA_DRAWABLE (group), x - pika_item_get_offset_x (PIKA_ITEM (group)), y - pika_item_get_offset_y (PIKA_ITEM (group)), width, height); } }