PIKApp/app/core/pikaitemlist.c

755 lines
24 KiB
C

/* 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 <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>
#include "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"
#include "core-types.h"
#include "pika.h"
#include "pikachannel.h"
#include "pikaimage.h"
#include "pikaitem.h"
#include "pikaitemlist.h"
#include "pikalayer.h"
#include "pikamarshal.h"
#include "vectors/pikavectors.h"
#include "pika-intl.h"
enum
{
EMPTY,
LAST_SIGNAL
};
enum
{
PROP_0,
PROP_IMAGE,
PROP_IS_PATTERN,
PROP_SELECT_METHOD,
PROP_ITEMS,
PROP_ITEM_TYPE,
N_PROPS
};
typedef struct _PikaItemListPrivate PikaItemListPrivate;
struct _PikaItemListPrivate
{
PikaImage *image;
gboolean is_pattern; /* Whether a named fixed set or a pattern-search. */
PikaSelectMethod select_method; /* Pattern format if is_pattern is TRUE */
GList *items; /* Fixed item list if is_pattern is TRUE. */
GList *deleted_items; /* Removed item list kept for undoes. */
GType item_type;
};
/* local function prototypes */
static void pika_item_list_constructed (GObject *object);
static void pika_item_list_dispose (GObject *object);
static void pika_item_list_finalize (GObject *object);
static void pika_item_list_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static void pika_item_list_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
static void pika_item_list_item_add (PikaContainer *container,
PikaObject *object,
PikaItemList *set);
static void pika_item_list_item_remove (PikaContainer *container,
PikaObject *object,
PikaItemList *set);
static GList * pika_item_list_get_items_by_substr (PikaItemList *set,
const gchar *pattern,
GError **error);
static GList * pika_item_list_get_items_by_glob (PikaItemList *set,
const gchar *pattern,
GError **error);
static GList * pika_item_list_get_items_by_regexp (PikaItemList *set,
const gchar *pattern,
GError **error);
static void pika_item_list_clean_deleted_items (PikaItemList *set,
PikaItem *searched,
gboolean *found);
static void pika_item_list_free_deleted_item (GWeakRef *item);
G_DEFINE_TYPE_WITH_PRIVATE (PikaItemList, pika_item_list, PIKA_TYPE_OBJECT)
#define parent_class pika_item_list_parent_class
static guint pika_item_list_signals[LAST_SIGNAL] = { 0 };
static GParamSpec *pika_item_list_props[N_PROPS] = { NULL, };
static void
pika_item_list_class_init (PikaItemListClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
/**
* PikaItemList::empty:
*
* Sent when the item set changed and would return an empty set of
* items.
*/
pika_item_list_signals[EMPTY] =
g_signal_new ("empty",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (PikaItemListClass, empty),
NULL, NULL, NULL,
G_TYPE_NONE, 0);
object_class->constructed = pika_item_list_constructed;
object_class->dispose = pika_item_list_dispose;
object_class->finalize = pika_item_list_finalize;
object_class->set_property = pika_item_list_set_property;
object_class->get_property = pika_item_list_get_property;
pika_item_list_props[PROP_IMAGE] = g_param_spec_object ("image", NULL, NULL,
PIKA_TYPE_IMAGE,
PIKA_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY);
pika_item_list_props[PROP_IS_PATTERN] = g_param_spec_boolean ("is-pattern", NULL, NULL,
FALSE,
PIKA_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY);
pika_item_list_props[PROP_SELECT_METHOD] = g_param_spec_enum ("select-method", NULL, NULL,
PIKA_TYPE_SELECT_METHOD,
PIKA_SELECT_PLAIN_TEXT,
PIKA_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY);
pika_item_list_props[PROP_ITEMS] = g_param_spec_pointer ("items",
NULL, NULL,
PIKA_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY);
pika_item_list_props[PROP_ITEM_TYPE] = g_param_spec_gtype ("item-type",
NULL, NULL,
G_TYPE_NONE,
PIKA_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY);
g_object_class_install_properties (object_class, N_PROPS, pika_item_list_props);
}
static void
pika_item_list_init (PikaItemList *set)
{
set->p = pika_item_list_get_instance_private (set);
set->p->items = NULL;
set->p->select_method = PIKA_SELECT_PLAIN_TEXT;
set->p->is_pattern = FALSE;
}
static void
pika_item_list_constructed (GObject *object)
{
PikaItemList *set = PIKA_ITEM_LIST (object);
G_OBJECT_CLASS (parent_class)->constructed (object);
pika_assert (PIKA_IS_IMAGE (set->p->image));
pika_assert (set->p->item_type == PIKA_TYPE_LAYER ||
set->p->item_type == PIKA_TYPE_VECTORS ||
set->p->item_type == PIKA_TYPE_CHANNEL);
if (! set->p->is_pattern)
{
PikaContainer *container;
if (set->p->item_type == PIKA_TYPE_LAYER)
container = pika_image_get_layers (set->p->image);
else if (set->p->item_type == PIKA_TYPE_VECTORS)
container = pika_image_get_vectors (set->p->image);
else
container = pika_image_get_channels (set->p->image);
g_signal_connect (container, "remove",
G_CALLBACK (pika_item_list_item_remove),
set);
g_signal_connect (container, "add",
G_CALLBACK (pika_item_list_item_add),
set);
}
}
static void
pika_item_list_dispose (GObject *object)
{
PikaItemList *set = PIKA_ITEM_LIST (object);
if (! set->p->is_pattern)
{
PikaContainer *container;
if (set->p->item_type == PIKA_TYPE_LAYER)
container = pika_image_get_layers (set->p->image);
else if (set->p->item_type == PIKA_TYPE_VECTORS)
container = pika_image_get_vectors (set->p->image);
else
container = pika_image_get_channels (set->p->image);
g_signal_handlers_disconnect_by_func (container,
G_CALLBACK (pika_item_list_item_remove),
set);
g_signal_handlers_disconnect_by_func (container,
G_CALLBACK (pika_item_list_item_add),
set);
}
}
static void
pika_item_list_finalize (GObject *object)
{
PikaItemList *set = PIKA_ITEM_LIST (object);
g_list_free (set->p->items);
g_list_free_full (set->p->deleted_items,
(GDestroyNotify) pika_item_list_free_deleted_item);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
pika_item_list_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
PikaItemList *set = PIKA_ITEM_LIST (object);
switch (property_id)
{
case PROP_IMAGE:
set->p->image = g_value_get_object (value);
break;
case PROP_IS_PATTERN:
set->p->is_pattern = g_value_get_boolean (value);
break;
case PROP_SELECT_METHOD:
set->p->select_method = g_value_get_enum (value);
break;
case PROP_ITEMS:
set->p->items = g_list_copy (g_value_get_pointer (value));
break;
case PROP_ITEM_TYPE:
set->p->item_type = g_value_get_gtype (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
pika_item_list_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
PikaItemList *set = PIKA_ITEM_LIST (object);
switch (property_id)
{
case PROP_IMAGE:
g_value_set_object (value, set->p->image);
break;
case PROP_IS_PATTERN:
g_value_set_boolean (value, set->p->is_pattern);
break;
case PROP_SELECT_METHOD:
g_value_set_enum (value, set->p->select_method);
break;
case PROP_ITEMS:
g_value_set_pointer (value, set->p->items);
break;
case PROP_ITEM_TYPE:
g_value_set_gtype (value, set->p->item_type);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
/* Public functions */
/**
* pika_item_list_named_new:
* @image: The new item_list's #PikaImage.
* @item_type: The type of #PikaItem in the list.
* @name: The name to assign the item list.
* @items: The items in the list.
*
* Create a fixed list of items made of items. It cannot be edited and
* will only auto-update when items get deleted from @image, until the
* list reaches 0 (in which case, the list will self-destroy).
*
* If @items is %NULL, the current item selection of type @item_type in
* @image is used. If this selection is empty, then %NULL is returned.
*
* Returns: The newly created #PikaItemList of %NULL if it corresponds
* to no items.
*/
PikaItemList *
pika_item_list_named_new (PikaImage *image,
GType item_type,
const gchar *name,
GList *items)
{
PikaItemList *set;
GList *iter;
g_return_val_if_fail (g_type_is_a (item_type, PIKA_TYPE_ITEM), NULL);
g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL);
for (iter = items; iter; iter = iter->next)
g_return_val_if_fail (g_type_is_a (G_OBJECT_TYPE (iter->data), item_type), NULL);
if (! items)
{
if (item_type == PIKA_TYPE_LAYER)
items = pika_image_get_selected_layers (image);
else if (item_type == PIKA_TYPE_VECTORS)
items = pika_image_get_selected_vectors (image);
else if (item_type == PIKA_TYPE_CHANNEL)
items = pika_image_get_selected_channels (image);
if (! items)
return NULL;
}
set = g_object_new (PIKA_TYPE_ITEM_LIST,
"image", image,
"name", name,
"is-pattern", FALSE,
"item-type", item_type,
"items", items,
NULL);
return set;
}
/**
* pika_item_list_pattern_new:
* @image: The new item_list's #PikaImage.
* @item_type: The type of #PikaItem in the list.
* @pattern_syntax: type of patterns we are handling.
* @pattern: The pattern generating the contents of the list.
*
* Create a list of items generated from a pattern. It cannot be edited.
*
* Returns: The newly created #PikaItemList.
*/
PikaItemList *
pika_item_list_pattern_new (PikaImage *image,
GType item_type,
PikaSelectMethod pattern_syntax,
const gchar *pattern)
{
PikaItemList *set;
g_return_val_if_fail (g_type_is_a (item_type, PIKA_TYPE_ITEM), NULL);
g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL);
/* TODO: check pattern first and fail if invalid. */
set = g_object_new (PIKA_TYPE_ITEM_LIST,
"image", image,
"name", pattern,
"is-pattern", TRUE,
"select-method", pattern_syntax,
"item-type", item_type,
NULL);
return set;
}
GType
pika_item_list_get_item_type (PikaItemList *set)
{
g_return_val_if_fail (PIKA_IS_ITEM_LIST (set), FALSE);
return set->p->item_type;
}
/**
* pika_item_list_get_items:
* @set:
*
* Returns: (transfer container): The unordered list of items
* represented by @set to be freed with g_list_free().
*/
GList *
pika_item_list_get_items (PikaItemList *set,
GError **error)
{
GList *items = NULL;
g_return_val_if_fail (PIKA_IS_ITEM_LIST (set), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
if (set->p->is_pattern)
{
switch (set->p->select_method)
{
case PIKA_SELECT_PLAIN_TEXT:
items = pika_item_list_get_items_by_substr (set,
pika_object_get_name (set),
error);
break;
case PIKA_SELECT_GLOB_PATTERN:
items = pika_item_list_get_items_by_glob (set,
pika_object_get_name (set),
error);
break;
case PIKA_SELECT_REGEX_PATTERN:
items = pika_item_list_get_items_by_regexp (set,
pika_object_get_name (set),
error);
break;
}
}
else
{
items = g_list_copy (set->p->items);
}
return items;
}
/**
* pika_item_list_is_pattern:
* @set: The #PikaItemList.
* @pattern_syntax: The type of patterns @set handles.
*
* Indicate if @set is a pattern list. If the returned value is %TRUE,
* then @pattern_syntax will be set to the syntax we are dealing with.
*
* Returns: %TRUE if @set is a pattern list, %FALSE if it is a named
* list.
*/
gboolean
pika_item_list_is_pattern (PikaItemList *set,
PikaSelectMethod *pattern_syntax)
{
g_return_val_if_fail (PIKA_IS_ITEM_LIST (set), FALSE);
if (set->p->is_pattern && pattern_syntax)
*pattern_syntax = set->p->select_method;
return (set->p->is_pattern);
}
/**
* pika_item_list_is_pattern:
* @set: The #PikaItemList.
* @item: #PikaItem to add to @set.
*
* Add @item to the named list @set whose item type must also agree.
*/
void
pika_item_list_add (PikaItemList *set,
PikaItem *item)
{
g_return_if_fail (PIKA_IS_ITEM_LIST (set));
g_return_if_fail (! pika_item_list_is_pattern (set, NULL));
g_return_if_fail (g_type_is_a (G_TYPE_FROM_INSTANCE (item), set->p->item_type));
set->p->items = g_list_prepend (set->p->items, item);
}
/* Private functions */
static void
pika_item_list_item_add (PikaContainer *container,
PikaObject *object,
PikaItemList *set)
{
gboolean found = FALSE;
pika_item_list_clean_deleted_items (set, PIKA_ITEM (object), &found);
if (found)
{
/* Such an item can only have been added back as part of an redo
* step.
*/
set->p->items = g_list_prepend (set->p->items, object);
}
}
static void
pika_item_list_item_remove (PikaContainer *container,
PikaObject *object,
PikaItemList *set)
{
GWeakRef *deleted_item = g_slice_new (GWeakRef);
/* Keep a weak link on object so that it disappears by itself when no
* other piece of code has a reference to it. In particular, we expect
* undo to keep references to deleted items. So if a redo happens we
* will get a "add" signal with the same object.
*/
set->p->items = g_list_remove (set->p->items, object);
g_weak_ref_init (deleted_item, object);
set->p->deleted_items = g_list_prepend (set->p->deleted_items, deleted_item);
}
/*
* @pika_item_list_get_items_by_substr:
* @image:
* @pattern:
* @error: unused #GError.
*
* Replace currently selected items in @image with the items whose
* names match with the @pattern after tokenisation, case-folding and
* normalization.
*
* Returns: %TRUE if some items matched @pattern (even if it turned out
* selected items stay the same), %FALSE otherwise.
*/
static GList *
pika_item_list_get_items_by_substr (PikaItemList *set,
const gchar *pattern,
GError **error)
{
GList *items;
GList *match = NULL;
GList *iter;
g_return_val_if_fail (PIKA_IS_ITEM_LIST (set), FALSE);
g_return_val_if_fail (error && *error == NULL, FALSE);
if (pattern == NULL)
return NULL;
if (set->p->item_type == PIKA_TYPE_LAYER)
{
items = pika_image_get_layer_list (set->p->image);
}
else
{
g_critical ("%s: only list of PikaLayer supported for now.",
G_STRFUNC);
return NULL;
}
for (iter = items; iter; iter = iter->next)
{
if (g_str_match_string (pattern,
pika_object_get_name (iter->data),
TRUE))
match = g_list_prepend (match, iter->data);
}
return match;
}
/*
* @pika_item_list_get_items_by_glob:
* @image:
* @pattern:
* @error: unused #GError.
*
* Replace currently selected items in @image with the items whose
* names match with the @pattern glob expression.
*
* Returns: %TRUE if some items matched @pattern (even if it turned out
* selected items stay the same), %FALSE otherwise.
*/
static GList *
pika_item_list_get_items_by_glob (PikaItemList *set,
const gchar *pattern,
GError **error)
{
GList *items;
GList *match = NULL;
GList *iter;
GPatternSpec *spec;
g_return_val_if_fail (PIKA_IS_ITEM_LIST (set), FALSE);
g_return_val_if_fail (error && *error == NULL, FALSE);
if (pattern == NULL)
return NULL;
if (set->p->item_type == PIKA_TYPE_LAYER)
{
items = pika_image_get_layer_list (set->p->image);
}
else
{
g_critical ("%s: only list of PikaLayer supported for now.",
G_STRFUNC);
return NULL;
}
spec = g_pattern_spec_new (pattern);
for (iter = items; iter; iter = iter->next)
{
if (g_pattern_spec_match_string (spec, pika_object_get_name (iter->data)))
match = g_list_prepend (match, iter->data);
}
g_pattern_spec_free (spec);
return match;
}
/*
* @pika_item_list_get_items_by_regexp:
* @image:
* @pattern:
* @error:
*
* Replace currently selected items in @image with the items whose
* names match with the @pattern regular expression.
*
* Returns: %TRUE if some items matched @pattern (even if it turned out
* selected items stay the same), %FALSE otherwise or if
* @pattern is an invalid regular expression (in which case,
* @error will be filled with the appropriate error).
*/
static GList *
pika_item_list_get_items_by_regexp (PikaItemList *set,
const gchar *pattern,
GError **error)
{
GList *items;
GList *match = NULL;
GList *iter;
GRegex *regex;
g_return_val_if_fail (PIKA_IS_ITEM_LIST (set), FALSE);
g_return_val_if_fail (pattern != NULL, FALSE);
g_return_val_if_fail (error && *error == NULL, FALSE);
regex = g_regex_new (pattern, 0, 0, error);
if (regex == NULL)
return NULL;
if (set->p->item_type == PIKA_TYPE_LAYER)
{
items = pika_image_get_layer_list (set->p->image);
}
else
{
g_critical ("%s: only list of PikaLayer supported for now.",
G_STRFUNC);
return NULL;
}
for (iter = items; iter; iter = iter->next)
{
if (g_regex_match (regex,
pika_object_get_name (iter->data),
0, NULL))
match = g_list_prepend (match, iter->data);
}
g_regex_unref (regex);
return match;
}
/*
* Remove all deleted items which don't have any reference left anywhere
* (only leaving the shell of the weak reference), hence whose deletion
* cannot be undone anyway.
* If @searched is not %NULL, check if it belonged to the deleted item
* list and return TRUE if so. In this case, you must call the function
* with @found set to %FALSE initially.
*/
static void
pika_item_list_clean_deleted_items (PikaItemList *set,
PikaItem *searched,
gboolean *found)
{
GList *iter;
g_return_if_fail (PIKA_IS_ITEM_LIST (set));
g_return_if_fail (! searched || (found && *found == FALSE));
for (iter = set->p->deleted_items; iter; iter = iter->next)
{
PikaItem *item = g_weak_ref_get (iter->data);
if (item == NULL)
{
set->p->deleted_items = g_list_delete_link (set->p->deleted_items,
iter);
break;
}
else
{
if (searched && item == searched)
{
set->p->deleted_items = g_list_delete_link (set->p->deleted_items,
iter);
*found = TRUE;
g_object_unref (item);
break;
}
g_object_unref (item);
}
}
if (iter)
pika_item_list_clean_deleted_items (set,
(found && *found) ? NULL : searched,
found);
}
static void
pika_item_list_free_deleted_item (GWeakRef *item)
{
g_weak_ref_clear (item);
g_slice_free (GWeakRef, item);
}