[gimp/wip/Jehan/layers-dockable-refresh: 7/78] app: new concept of sets of layers stored in GimpImage.




commit b86900e7d531aab9e3b5d935d7dc40636bcbc9df
Author: Jehan <jehan girinstud io>
Date:   Thu Feb 4 18:54:22 2021 +0100

    app: new concept of sets of layers stored in GimpImage.
    
    The eventual goal is to replace the "linked layers" concept, which is
    why I am using similar vocabulary. The point is that linked layers are
    mostly useless/redundant now with multiplie layer selection, except for
    one thing: they kind of serve like a way to "save" a selection of layers
    (to be moved/transformed together mostly). Apart from this, multiple
    selection is more powerful on any way. You can do much more than
    transforming the layers together (you can reorganize them together,
    delete them, crop them and so on).
    
    Therefore this new feature is the way to fill the only weakness of layer
    selection: its ephemerality. Now we can save a given set of layers, not
    even only one, but as many as we want, and under a meaningful name, for
    later reuse.
    Moreover it will make layer-handling core code much simpler as we
    currently have 2 concepts of layer set: multiple selection and links.
    The new stored links are only a way to recreate multiple selections.
    
    More is to come, for instance right now, these are not stored in the XCF
    format. Also it would be awesome to add logical operators (Shift for
    union of layer sets, Ctrl for subtraction and Shift-Ctrl for
    intersection). And finally I was thinking about a way to select by
    pattern (regular expression? Shell-style glob patterns?) and even store
    these patterns. So if you save a "Marmot .*" selection pattern, then
    when you select it later, new layers matching this pattern will be
    included too (instead of fixed-in-time list of layers).

 app/core/gimpimage-private.h    |   1 +
 app/core/gimpimage.c            | 180 ++++++++++++++++++++++++++++++++
 app/core/gimpimage.h            |  12 +++
 app/widgets/gimplayertreeview.c | 221 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 414 insertions(+)
---
diff --git a/app/core/gimpimage-private.h b/app/core/gimpimage-private.h
index 0c77b9a281..4a1e78f103 100644
--- a/app/core/gimpimage-private.h
+++ b/app/core/gimpimage-private.h
@@ -110,6 +110,7 @@ struct _GimpImagePrivate
   GimpItemTree      *channels;              /*  the tree of masks            */
   GimpItemTree      *vectors;               /*  the tree of vectors          */
   GSList            *layer_stack;           /*  the layers in MRU order      */
+  GHashTable        *linked_layers;
 
   GQuark             layer_offset_x_handler;
   GQuark             layer_offset_y_handler;
diff --git a/app/core/gimpimage.c b/app/core/gimpimage.c
index 510f62cafb..b5f147ec40 100644
--- a/app/core/gimpimage.c
+++ b/app/core/gimpimage.c
@@ -126,6 +126,7 @@ enum
   PARASITE_DETACHED,
   COLORMAP_CHANGED,
   UNDO_EVENT,
+  LAYER_LINKS_CHANGED,
   LAST_SIGNAL
 };
 
@@ -572,6 +573,14 @@ gimp_image_class_init (GimpImageClass *klass)
                   GIMP_TYPE_UNDO_EVENT,
                   GIMP_TYPE_UNDO);
 
+  gimp_image_signals[LAYER_LINKS_CHANGED] =
+    g_signal_new ("layer-links-changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (GimpImageClass, layer_links_changed),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+
   object_class->constructed           = gimp_image_constructed;
   object_class->set_property          = gimp_image_set_property;
   object_class->get_property          = gimp_image_get_property;
@@ -776,6 +785,10 @@ gimp_image_init (GimpImage *image)
                                                      GIMP_TYPE_VECTORS);
   private->layer_stack         = NULL;
 
+  private->linked_layers       = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                        (GDestroyNotify) g_free,
+                                                        (GDestroyNotify) g_list_free);
+
   g_signal_connect (private->projection, "notify::buffer",
                     G_CALLBACK (gimp_image_projection_buffer_notify),
                     image);
@@ -1119,6 +1132,8 @@ gimp_image_finalize (GObject *object)
   g_clear_object (&private->channels);
   g_clear_object (&private->vectors);
 
+  g_hash_table_destroy (private->linked_layers);
+
   if (private->layer_stack)
     {
       g_slist_free_full (private->layer_stack,
@@ -5347,6 +5362,171 @@ gimp_image_add_layers (GimpImage   *image,
   gimp_image_undo_group_end (image);
 }
 
+/*
+ * gimp_image_link_layers:
+ * @image:
+ * @layers:
+ * @link_name:
+ *
+ * Create a new set of @layers under the name @link_name.
+ * If @layers is empty, the currently selected layers are linked
+ * instead.
+ * If a set with the same name existed, this call will silently replace
+ * it with the new set of layers.
+ *
+ * Returns: %TRUE if a new set was created, %FALSE otherwise (e.g. no
+ *          list provided and no layers currently selected).
+ */
+gboolean
+gimp_image_link_layers (GimpImage   *image,
+                        const GList *layers,
+                        const gchar *link_name)
+{
+  GimpImagePrivate *private;
+
+  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  if (! layers)
+    {
+      layers = gimp_image_get_selected_layers (image);
+
+      if (! layers)
+        return FALSE;
+    }
+
+  g_hash_table_insert (private->linked_layers,
+                       g_strdup (link_name),
+                       g_list_copy ((GList *) layers));
+  g_signal_emit (image, gimp_image_signals[LAYER_LINKS_CHANGED], 0);
+
+  return TRUE;
+}
+
+/*
+ * @gimp_image_unlink_layers:
+ * @image:
+ * @link_name:
+ *
+ * Remove the set of layers named @link_name.
+ *
+ * Returns: %TRUE if the set was removed, %FALSE if no sets with this
+ *          name existed.
+ */
+gboolean
+gimp_image_unlink_layers (GimpImage   *image,
+                          const gchar *link_name)
+{
+  GimpImagePrivate *private;
+  gboolean          success;
+
+  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  success = g_hash_table_remove (private->linked_layers, link_name);
+  if (success)
+    g_signal_emit (image, gimp_image_signals[LAYER_LINKS_CHANGED], 0);
+
+  return success;
+}
+
+/*
+ * @gimp_image_select_linked_layers:
+ * @image:
+ * @link_name:
+ *
+ * Replace currently selected layers in @image with the layers belonging
+ * to the set named @link_name (which must exist).
+ *
+ * Returns: %TRUE if the selection change is done (even if it turned out
+ *          selected layers stay the same), %FALSE if no sets with this
+ *          name existed.
+ */
+void
+gimp_image_select_linked_layers (GimpImage   *image,
+                                 const gchar *link_name)
+{
+  GimpImagePrivate *private;
+  GList            *linked;
+
+  g_return_if_fail (GIMP_IS_IMAGE (image));
+  g_return_if_fail (link_name != NULL);
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  linked = g_hash_table_lookup (private->linked_layers,
+                                link_name);
+
+  g_return_if_fail (linked);
+
+  gimp_image_set_selected_layers (image, linked);
+}
+
+/*
+ * @gimp_image_add_linked_layers:
+ * @image:
+ * @link_name:
+ *
+ * Add the layers belonging to the set named @link_name (which must
+ * exist) to the layers currently selected in @image.
+ *
+ * Returns: %TRUE if the selection change is done (even if it turned out
+ *          selected layers stay the same), %FALSE if no sets with this
+ *          name existed.
+ */
+void
+gimp_image_add_linked_layers (GimpImage   *image,
+                              const gchar *link_name)
+{
+  GimpImagePrivate *private;
+  GList            *linked;
+  GList            *layers;
+  GList            *iter;
+
+  g_return_if_fail (GIMP_IS_IMAGE (image));
+  g_return_if_fail (link_name != NULL);
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  linked = g_hash_table_lookup (private->linked_layers,
+                                link_name);
+
+  g_return_if_fail (linked);
+
+  layers = gimp_image_get_selected_layers (image);
+  layers = g_list_copy (layers);
+  for (iter = linked; iter; iter = iter->next)
+    {
+      if (! g_list_find (layers, iter->data))
+        layers = g_list_prepend (layers, iter->data);
+    }
+
+  gimp_image_set_selected_layers (image, layers);
+  g_list_free (layers);
+}
+
+/*
+ * @gimp_image_get_linked_layer_names:
+ * @image:
+ *
+ * Returns: the newly allocated list of all the link names (which you
+ *          should not modify directly). Free the list with
+ *          g_list_free().
+ */
+GList *
+gimp_image_get_linked_layer_names (GimpImage *image)
+{
+  GimpImagePrivate *private;
+
+  g_return_val_if_fail (GIMP_IS_IMAGE (image), NULL);
+
+  private = GIMP_IMAGE_GET_PRIVATE (image);
+
+  return g_hash_table_get_keys (private->linked_layers);
+}
+
 
 /*  channels  */
 
diff --git a/app/core/gimpimage.h b/app/core/gimpimage.h
index 41bfedf153..42406f9191 100644
--- a/app/core/gimpimage.h
+++ b/app/core/gimpimage.h
@@ -104,6 +104,7 @@ struct _GimpImageClass
   void (* undo_event)                   (GimpImage            *image,
                                          GimpUndoEvent         event,
                                          GimpUndo             *undo);
+  void (* layer_links_changed)          (GimpImage            *image);
 };
 
 
@@ -451,6 +452,17 @@ void            gimp_image_add_layers            (GimpImage          *image,
                                                   gint                height,
                                                   const gchar        *undo_desc);
 
+gboolean        gimp_image_link_layers           (GimpImage          *image,
+                                                  const GList        *layers,
+                                                  const gchar        *link_name);
+gboolean        gimp_image_unlink_layers         (GimpImage          *image,
+                                                  const gchar        *link_name);
+void            gimp_image_select_linked_layers  (GimpImage          *image,
+                                                  const gchar        *link_name);
+void            gimp_image_add_linked_layers     (GimpImage          *image,
+                                                  const gchar        *link_name);
+GList         * gimp_image_get_linked_layer_names (GimpImage         *image);
+
 gboolean        gimp_image_add_channel           (GimpImage          *image,
                                                   GimpChannel        *channel,
                                                   GimpChannel        *parent,
diff --git a/app/widgets/gimplayertreeview.c b/app/widgets/gimplayertreeview.c
index 6085738b63..91b550f1b7 100644
--- a/app/widgets/gimplayertreeview.c
+++ b/app/widgets/gimplayertreeview.c
@@ -75,6 +75,10 @@ struct _GimpLayerTreeViewPrivate
   GtkWidget       *lock_alpha_toggle;
   GtkWidget       *anchor_button;
 
+  GtkWidget       *link_button;
+  GtkWidget       *link_list;
+  GtkWidget       *link_entry;
+
   gint             model_column_mask;
   gint             model_column_mask_visible;
 
@@ -141,6 +145,18 @@ static void       gimp_layer_tree_view_set_image                  (GimpItemTreeV
 static GimpItem * gimp_layer_tree_view_item_new                   (GimpImage                  *image);
 static void       gimp_layer_tree_view_floating_selection_changed (GimpImage                  *image,
                                                                    GimpLayerTreeView          *view);
+
+static void       gimp_layer_tree_view_layer_links_changed        (GimpImage                  *image,
+                                                                   GimpLayerTreeView          *view);
+static void       gimp_layer_tree_view_link_activated             (GtkListBox                 *list,
+                                                                   GtkListBoxRow               *row,
+                                                                   GimpLayerTreeView          *view);
+static void       gimp_layer_tree_view_new_link_clicked           (GtkButton                  *button,
+                                                                   GimpLayerTreeView          *view);
+static gboolean   gimp_layer_tree_view_unlink_clicked             (GtkWidget                  *widget,
+                                                                   GdkEvent                   *event,
+                                                                   GimpLayerTreeView          *view);
+
 static void       gimp_layer_tree_view_layer_mode_box_callback    (GtkWidget                  *widget,
                                                                    const GParamSpec           *pspec,
                                                                    GimpLayerTreeView          *view);
@@ -349,6 +365,9 @@ gimp_layer_tree_view_constructed (GObject *object)
   GimpContainerTreeView *tree_view  = GIMP_CONTAINER_TREE_VIEW (object);
   GimpLayerTreeView     *layer_view = GIMP_LAYER_TREE_VIEW (object);
   GtkWidget             *button;
+  GtkWidget             *popover;
+  GtkWidget             *grid;
+  GtkIconSize            button_size;
 
   G_OBJECT_CLASS (parent_class)->constructed (object);
 
@@ -430,6 +449,73 @@ gimp_layer_tree_view_constructed (GObject *object)
                                   GIMP_TYPE_LAYER);
   gtk_box_reorder_child (gimp_editor_get_button_box (GIMP_EDITOR (layer_view)),
                          button, 7);
+
+  /* Link popover menu. */
+
+  layer_view->priv->link_button = gtk_menu_button_new ();
+  gtk_widget_style_get (GTK_WIDGET (layer_view),
+                        "button-icon-size", &button_size,
+                        NULL);
+  gtk_button_set_image (GTK_BUTTON (layer_view->priv->link_button),
+                        gtk_image_new_from_icon_name (GIMP_ICON_LINKED,
+                                                      button_size));
+  gtk_box_pack_start (gimp_editor_get_button_box (GIMP_EDITOR (layer_view)),
+                      layer_view->priv->link_button, TRUE, TRUE, 0);
+  gtk_widget_show (layer_view->priv->link_button);
+  gimp_container_view_enable_dnd (GIMP_CONTAINER_VIEW (layer_view),
+                                  GTK_BUTTON (layer_view->priv->link_button),
+                                  GIMP_TYPE_LAYER);
+  gtk_box_reorder_child (gimp_editor_get_button_box (GIMP_EDITOR (layer_view)),
+                         layer_view->priv->link_button, 8);
+
+  popover = gtk_popover_new (layer_view->priv->link_button);
+  gtk_popover_set_modal (GTK_POPOVER (popover), TRUE);
+  gtk_menu_button_set_popover (GTK_MENU_BUTTON (layer_view->priv->link_button), popover);
+
+  grid = gtk_grid_new ();
+
+  /* Link popover: existing links. */
+  layer_view->priv->link_list = gtk_list_box_new ();
+  gtk_grid_attach (GTK_GRID (grid),
+                   layer_view->priv->link_list,
+                   0, 0, 2, 1);
+  gtk_widget_show (layer_view->priv->link_list);
+
+  gtk_list_box_set_activate_on_single_click (GTK_LIST_BOX (layer_view->priv->link_list),
+                                             TRUE);
+  g_signal_connect (layer_view->priv->link_list,
+                    "row-activated",
+                    G_CALLBACK (gimp_layer_tree_view_link_activated),
+                    layer_view);
+
+  /* Link popover: new links. */
+
+  layer_view->priv->link_entry = gtk_entry_new ();
+  gtk_entry_set_placeholder_text (GTK_ENTRY (layer_view->priv->link_entry),
+                                  _("Named Selection"));
+  gtk_grid_attach (GTK_GRID (grid),
+                   layer_view->priv->link_entry,
+                   0, 1, 1, 1);
+  gtk_widget_show (layer_view->priv->link_entry);
+
+  button = gtk_button_new_from_icon_name (GIMP_ICON_DOCUMENT_SAVE, button_size);
+  gtk_grid_attach (GTK_GRID (grid),
+                   button,
+                   1, 1, 1, 1);
+  g_signal_connect (button,
+                    "clicked",
+                    G_CALLBACK (gimp_layer_tree_view_new_link_clicked),
+                    layer_view);
+  gtk_widget_show (button);
+
+  /* Enter on entry activates the link creation. */
+  g_signal_connect_swapped (layer_view->priv->link_entry,
+                            "activate",
+                            G_CALLBACK (gtk_button_clicked),
+                            button);
+
+  gtk_container_add (GTK_CONTAINER (popover), grid);
+  gtk_widget_show (grid);
 }
 
 static void
@@ -918,6 +1004,9 @@ gimp_layer_tree_view_set_image (GimpItemTreeView *view,
       g_signal_handlers_disconnect_by_func (gimp_item_tree_view_get_image (view),
                                             gimp_layer_tree_view_floating_selection_changed,
                                             view);
+      g_signal_handlers_disconnect_by_func (gimp_item_tree_view_get_image (view),
+                                            G_CALLBACK (gimp_layer_tree_view_layer_links_changed),
+                                            view);
     }
 
   GIMP_ITEM_TREE_VIEW_CLASS (parent_class)->set_image (view, image);
@@ -928,6 +1017,10 @@ gimp_layer_tree_view_set_image (GimpItemTreeView *view,
                         "floating-selection-changed",
                         G_CALLBACK (gimp_layer_tree_view_floating_selection_changed),
                         view);
+      g_signal_connect (gimp_item_tree_view_get_image (view),
+                        "layer-links-changed",
+                        G_CALLBACK (gimp_layer_tree_view_layer_links_changed),
+                        view);
 
       /* call gimp_layer_tree_view_floating_selection_changed() now, to update
        * the floating selection's row attributes.
@@ -936,6 +1029,9 @@ gimp_layer_tree_view_set_image (GimpItemTreeView *view,
         gimp_item_tree_view_get_image (view),
         layer_view);
     }
+  /* Call this even with no image, allowing to empty the link list. */
+  gimp_layer_tree_view_layer_links_changed (gimp_item_tree_view_get_image (view),
+                                            layer_view);
 
   gimp_layer_tree_view_update_highlight (layer_view);
 }
@@ -1015,6 +1111,131 @@ gimp_layer_tree_view_floating_selection_changed (GimpImage         *image,
   gimp_layer_tree_view_update_highlight (layer_view);
 }
 
+static void
+gimp_layer_tree_view_layer_links_changed (GimpImage         *image,
+                                          GimpLayerTreeView *view)
+{
+  GtkWidget    *grid;
+  GtkWidget    *label;
+  GtkWidget    *event_box;
+  GtkWidget    *icon;
+  GtkSizeGroup *label_size;
+  GList        *links;
+  GList        *iter;
+
+  gtk_container_foreach (GTK_CONTAINER (view->priv->link_list),
+                         (GtkCallback) gtk_widget_destroy, NULL);
+  gtk_widget_set_sensitive (view->priv->link_button, image != NULL);
+
+  if (! image)
+    return;
+
+  links = gimp_image_get_linked_layer_names (image);
+
+  label_size = gtk_size_group_new (GTK_SIZE_GROUP_BOTH);
+  for (iter = links; iter; iter = iter->next)
+    {
+      grid = gtk_grid_new ();
+
+      label = gtk_label_new (iter->data);
+      g_object_set_data (G_OBJECT (grid), "link-name",
+                         (gpointer) gtk_label_get_text (GTK_LABEL (label)));
+      gtk_widget_set_hexpand (GTK_WIDGET (label), TRUE);
+      gtk_widget_set_halign (GTK_WIDGET (label), GTK_ALIGN_START);
+      gtk_size_group_add_widget (label_size, label);
+      gtk_grid_attach (GTK_GRID (grid), label, 0, 1, 1, 1);
+      gtk_widget_show (label);
+
+      /* I don't use a GtkButton because the minimum size is 16 which is
+       * weird and ugly here. And somehow if I force smaller GtkImage
+       * size then add it to the GtkButton, I still get a giant button
+       * with a small image in it, which is even worse. XXX
+       */
+      event_box = gtk_event_box_new ();
+      gtk_event_box_set_above_child (GTK_EVENT_BOX (event_box), TRUE);
+      gtk_widget_add_events (event_box, GDK_BUTTON_RELEASE_MASK);
+      g_object_set_data (G_OBJECT (event_box), "link-name",
+                         (gpointer) gtk_label_get_text (GTK_LABEL (label)));
+      g_signal_connect (event_box, "button-release-event",
+                        G_CALLBACK (gimp_layer_tree_view_unlink_clicked),
+                        view);
+      gtk_grid_attach (GTK_GRID (grid), event_box, 2, 0, 1, 1);
+      gtk_widget_show (event_box);
+
+      icon = gtk_image_new_from_icon_name (GIMP_ICON_EDIT_DELETE, GTK_ICON_SIZE_MENU);
+      gtk_image_set_pixel_size (GTK_IMAGE (icon), 10);
+      gtk_container_add (GTK_CONTAINER (event_box), icon);
+      gtk_widget_show (icon);
+
+      gtk_list_box_prepend (GTK_LIST_BOX (view->priv->link_list), grid);
+      gtk_widget_show (grid);
+    }
+  g_object_unref (label_size);
+  gtk_list_box_unselect_all (GTK_LIST_BOX (view->priv->link_list));
+
+  g_list_free (links);
+}
+
+static void
+gimp_layer_tree_view_link_activated (GtkListBox        *list,
+                                     GtkListBoxRow      *row,
+                                     GimpLayerTreeView *view)
+{
+  GimpImage *image;
+  GtkWidget *grid;
+
+  image = gimp_item_tree_view_get_image (GIMP_ITEM_TREE_VIEW (view));
+  grid = gtk_bin_get_child (GTK_BIN (row));
+
+  g_return_if_fail (GIMP_IS_IMAGE (image));
+  g_return_if_fail (GTK_IS_GRID (grid));
+
+  gimp_image_select_linked_layers (image,
+                                   g_object_get_data (G_OBJECT (grid), "link-name"));
+}
+
+static void
+gimp_layer_tree_view_new_link_clicked (GtkButton         *button,
+                                       GimpLayerTreeView *view)
+{
+  GimpImage   *image;
+  const gchar *name;
+
+  image = gimp_item_tree_view_get_image (GIMP_ITEM_TREE_VIEW (view));
+
+  if (! image)
+    return;
+
+  name = gtk_entry_get_text (GTK_ENTRY (view->priv->link_entry));
+  if (name && strlen (name) > 0)
+    {
+      if (gimp_image_link_layers (image, NULL, name))
+        gtk_entry_set_text (GTK_ENTRY (view->priv->link_entry), "");
+    }
+  else
+    {
+      gimp_widget_blink (view->priv->link_entry);
+    }
+}
+
+static gboolean
+gimp_layer_tree_view_unlink_clicked (GtkWidget         *widget,
+                                     GdkEvent          *event,
+                                     GimpLayerTreeView *view)
+{
+  GimpImage *image;
+
+  image = gimp_item_tree_view_get_image (GIMP_ITEM_TREE_VIEW (view));
+
+  g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
+
+  gimp_image_unlink_layers (image,
+                            g_object_get_data (G_OBJECT (widget),
+                                               "link-name"));
+
+  return TRUE;
+}
+
 
 /*  Paint Mode, Opacity and Lock alpha callbacks  */
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]