From 33bfd5a12bf34c9e3c4ea3c8d91f278242eee720 Mon Sep 17 00:00:00 2001 From: Michael Natterer Date: Tue, 5 Aug 2025 16:08:55 +0200 Subject: [PATCH] app: implement GimpContainerListView, using a GtkListBox It completely relies on GListModel and doesn't manage any list items itself. The port should be pretty much feature-complete. Add a ton of stuff to GimpRow in order to look and behave like a GimpContainerTreeView row. Add a playground switch to use the new widgets in all views that can be switched between list and grid view (brushes, patterns etc.) Please test! --- app/config/gimpguiconfig.c | 15 + app/config/gimpguiconfig.h | 1 + app/config/gimprc-blurbs.h | 3 + app/dialogs/preferences-dialog.c | 3 + app/widgets/gimpcontainereditor.c | 21 +- app/widgets/gimpcontainerlistview.c | 517 ++++++++++++++++++++++++++++ app/widgets/gimpcontainerlistview.h | 52 +++ app/widgets/gimpcontrollerlist.c | 5 +- app/widgets/gimprow-utils.c | 58 ++++ app/widgets/gimprow-utils.h | 29 ++ app/widgets/gimprow.c | 176 ++++++++-- app/widgets/gimprow.h | 5 - app/widgets/meson.build | 2 + app/widgets/widgets-types.h | 1 + 14 files changed, 852 insertions(+), 36 deletions(-) create mode 100644 app/widgets/gimpcontainerlistview.c create mode 100644 app/widgets/gimpcontainerlistview.h create mode 100644 app/widgets/gimprow-utils.c create mode 100644 app/widgets/gimprow-utils.h diff --git a/app/config/gimpguiconfig.c b/app/config/gimpguiconfig.c index 111a10e617..faaddacd2e 100644 --- a/app/config/gimpguiconfig.c +++ b/app/config/gimpguiconfig.c @@ -92,6 +92,7 @@ enum PROP_PLAYGROUND_NPD_TOOL, PROP_PLAYGROUND_SEAMLESS_CLONE_TOOL, PROP_PLAYGROUND_PAINT_SELECT_TOOL, + PROP_PLAYGROUND_USE_LIST_BOX, PROP_HIDE_DOCKS, PROP_SINGLE_WINDOW_MODE, @@ -470,6 +471,14 @@ gimp_gui_config_class_init (GimpGuiConfigClass *klass) GIMP_PARAM_STATIC_STRINGS | GIMP_CONFIG_PARAM_RESTART); + GIMP_CONFIG_PROP_BOOLEAN (object_class, + PROP_PLAYGROUND_USE_LIST_BOX, + "playground-use-list-box", + "Playground Use List Box", + PLAYGROUND_USE_LIST_BOX_BLURB, + FALSE, + GIMP_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_HIDE_DOCKS, g_param_spec_boolean ("hide-docks", NULL, @@ -753,6 +762,9 @@ gimp_gui_config_set_property (GObject *object, case PROP_PLAYGROUND_PAINT_SELECT_TOOL: gui_config->playground_paint_select_tool = g_value_get_boolean (value); break; + case PROP_PLAYGROUND_USE_LIST_BOX: + gui_config->playground_use_list_box = g_value_get_boolean (value); + break; case PROP_HIDE_DOCKS: gui_config->hide_docks = g_value_get_boolean (value); @@ -934,6 +946,9 @@ gimp_gui_config_get_property (GObject *object, case PROP_PLAYGROUND_PAINT_SELECT_TOOL: g_value_set_boolean (value, gui_config->playground_paint_select_tool); break; + case PROP_PLAYGROUND_USE_LIST_BOX: + g_value_set_boolean (value, gui_config->playground_use_list_box); + break; case PROP_HIDE_DOCKS: g_value_set_boolean (value, gui_config->hide_docks); diff --git a/app/config/gimpguiconfig.h b/app/config/gimpguiconfig.h index 695d440521..0bbd67ee4e 100644 --- a/app/config/gimpguiconfig.h +++ b/app/config/gimpguiconfig.h @@ -86,6 +86,7 @@ struct _GimpGuiConfig gboolean playground_npd_tool; gboolean playground_seamless_clone_tool; gboolean playground_paint_select_tool; + gboolean playground_use_list_box; /* saved in sessionrc */ gboolean hide_docks; diff --git a/app/config/gimprc-blurbs.h b/app/config/gimprc-blurbs.h index 27839ec405..bd143ec7b4 100644 --- a/app/config/gimprc-blurbs.h +++ b/app/config/gimprc-blurbs.h @@ -510,6 +510,9 @@ _("Enable the Seamless Clone tool.") #define PLAYGROUND_PAINT_SELECT_TOOL_BLURB \ _("Enable the Paint Select tool.") +#define PLAYGROUND_USE_LIST_BOX_BLURB \ +_("Use the new GtkListBox widget for simple lists.") + #define SPACE_BAR_ACTION_BLURB \ _("What to do when the space bar is pressed in the image window.") diff --git a/app/dialogs/preferences-dialog.c b/app/dialogs/preferences-dialog.c index 80e549f986..f084405fcb 100644 --- a/app/dialogs/preferences-dialog.c +++ b/app/dialogs/preferences-dialog.c @@ -1695,6 +1695,9 @@ prefs_dialog_new (Gimp *gimp, */ gimp_help_set_help_data (button, "Missing GEGL operation 'gegl:paint-select'.", NULL); } + button = prefs_check_button_add (object, "playground-use-list-box", + _("Use GtkListBox in simple lists"), + GTK_BOX (vbox2)); } diff --git a/app/widgets/gimpcontainereditor.c b/app/widgets/gimpcontainereditor.c index aedc04ae1d..be35b81515 100644 --- a/app/widgets/gimpcontainereditor.c +++ b/app/widgets/gimpcontainereditor.c @@ -28,6 +28,9 @@ #include "widgets-types.h" +#include "config/gimpguiconfig.h" /* playground */ + +#include "core/gimp.h" /* playground */ #include "core/gimpasyncset.h" #include "core/gimpcontext.h" #include "core/gimplist.h" @@ -35,6 +38,7 @@ #include "gimpcontainereditor.h" #include "gimpcontainericonview.h" +#include "gimpcontainerlistview.h" #include "gimpcontainertreeview.h" #include "gimpcontainerview.h" #include "gimpdocked.h" @@ -230,11 +234,18 @@ gimp_container_editor_constructed (GObject *object) break; case GIMP_VIEW_TYPE_LIST: - editor->view = - GIMP_CONTAINER_VIEW (gimp_container_tree_view_new (editor->priv->container, - editor->priv->context, - editor->priv->view_size, - editor->priv->view_border_width)); + if (GIMP_GUI_CONFIG (editor->priv->context->gimp->config)->playground_use_list_box) + editor->view = + GIMP_CONTAINER_VIEW (gimp_container_list_view_new (editor->priv->container, + editor->priv->context, + editor->priv->view_size, + editor->priv->view_border_width)); + else + editor->view = + GIMP_CONTAINER_VIEW (gimp_container_tree_view_new (editor->priv->container, + editor->priv->context, + editor->priv->view_size, + editor->priv->view_border_width)); break; default: diff --git a/app/widgets/gimpcontainerlistview.c b/app/widgets/gimpcontainerlistview.c new file mode 100644 index 0000000000..e9be18aa5c --- /dev/null +++ b/app/widgets/gimpcontainerlistview.c @@ -0,0 +1,517 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * gimpcontainerlistview.c + * Copyright (C) 2025 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 "libgimpwidgets/gimpwidgets.h" + +#include "widgets-types.h" + +#include "core/gimpcontainer.h" +#include "core/gimpcontext.h" +#include "core/gimpviewable.h" + +#include "gimpcontainerlistview.h" +#include "gimpcontainerview.h" +#include "gimprow.h" +#include "gimprow-utils.h" +#include "gimpviewrenderer.h" + +#include "gimp-intl.h" + + +typedef struct _GimpContainerListViewPrivate GimpContainerListViewPrivate; + +struct _GimpContainerListViewPrivate +{ + GtkListBox *view; +}; + +#define GET_PRIVATE(obj) \ + ((GimpContainerListViewPrivate *) gimp_container_list_view_get_instance_private ((GimpContainerListView *) obj)) + + +static void gimp_container_list_view_view_iface_init (GimpContainerViewInterface *iface); + +static void gimp_container_list_view_constructed (GObject *object); +static void gimp_container_list_view_finalize (GObject *object); + +static gboolean gimp_container_list_view_popup_menu (GtkWidget *widget); + +static void gimp_container_list_view_set_container (GimpContainerView *view, + GimpContainer *container); +static void gimp_container_list_view_set_context (GimpContainerView *view, + GimpContext *context); +static void gimp_container_list_view_set_selection_mode(GimpContainerView *view, + GtkSelectionMode mode); +static void gimp_container_list_view_set_view_size (GimpContainerView *view); +static gboolean gimp_container_list_view_set_selected (GimpContainerView *view, + GList *items); +static gint gimp_container_list_view_get_selected (GimpContainerView *view, + GList **items); + +static void gimp_container_list_view_selected_rows_changed + (GtkListBox *box, + GimpContainerListView *list_view); +static void gimp_container_list_view_row_activated (GtkListBox *box, + GimpRow *row, + GimpContainerListView *list_view); + +static void gimp_container_list_view_monitor_changed (GimpContainerListView *view); + + +G_DEFINE_TYPE_WITH_CODE (GimpContainerListView, + gimp_container_list_view, + GIMP_TYPE_CONTAINER_BOX, + G_ADD_PRIVATE (GimpContainerListView) + G_IMPLEMENT_INTERFACE (GIMP_TYPE_CONTAINER_VIEW, + gimp_container_list_view_view_iface_init)) + +#define parent_class gimp_container_list_view_parent_class + +static GimpContainerViewInterface *parent_view_iface = NULL; + + +static void +gimp_container_list_view_class_init (GimpContainerListViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = gimp_container_list_view_constructed; + object_class->finalize = gimp_container_list_view_finalize; + + widget_class->popup_menu = gimp_container_list_view_popup_menu; +} + +static void +gimp_container_list_view_view_iface_init (GimpContainerViewInterface *iface) +{ + parent_view_iface = g_type_interface_peek_parent (iface); + + if (! parent_view_iface) + parent_view_iface = g_type_default_interface_peek (GIMP_TYPE_CONTAINER_VIEW); + + iface->set_container = gimp_container_list_view_set_container; + iface->set_context = gimp_container_list_view_set_context; + iface->set_selection_mode = gimp_container_list_view_set_selection_mode; + iface->set_view_size = gimp_container_list_view_set_view_size; + iface->set_selected = gimp_container_list_view_set_selected; + iface->get_selected = gimp_container_list_view_get_selected; + + iface->use_list_model = TRUE; +} + +static void +gimp_container_list_view_init (GimpContainerListView *list_view) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (list_view); + GimpContainerBox *box = GIMP_CONTAINER_BOX (list_view); + + priv->view = GTK_LIST_BOX (gtk_list_box_new ()); + gtk_list_box_set_activate_on_single_click (priv->view, FALSE); + gtk_container_add (GTK_CONTAINER (box->scrolled_win), + GTK_WIDGET (priv->view)); + gtk_widget_show (GTK_WIDGET (priv->view)); + + g_signal_connect (priv->view, "selected-rows-changed", + G_CALLBACK (gimp_container_list_view_selected_rows_changed), + list_view); + g_signal_connect (priv->view, "row-activated", + G_CALLBACK (gimp_container_list_view_row_activated), + list_view); + + gimp_container_view_set_dnd_widget (GIMP_CONTAINER_VIEW (list_view), + GTK_WIDGET (priv->view)); + + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (box->scrolled_win), + GTK_SHADOW_IN); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box->scrolled_win), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + + gimp_widget_track_monitor (GTK_WIDGET (list_view), + G_CALLBACK (gimp_container_list_view_monitor_changed), + NULL, NULL); +} + +static void +gimp_container_list_view_constructed (GObject *object) +{ + G_OBJECT_CLASS (parent_class)->constructed (object); +} + +static void +gimp_container_list_view_finalize (GObject *object) +{ + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static gboolean +gimp_container_list_view_popup_menu (GtkWidget *widget) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (widget); + GList *rows; + GtkWidget *row; + GtkAllocation allocation; + + rows = gtk_list_box_get_selected_rows (priv->view); + + if (g_list_length (rows) != 1) + { + g_list_free (rows); + + return FALSE; + } + + row = rows->data; + + g_list_free (rows); + + gtk_widget_get_allocation (row, &allocation); + + return gimp_editor_popup_menu_at_rect (GIMP_EDITOR (widget), + gtk_widget_get_window (row), + (GdkRectangle *) &allocation, + GDK_GRAVITY_CENTER, + GDK_GRAVITY_NORTH_WEST, + NULL); +} + +GtkWidget * +gimp_container_list_view_new (GimpContainer *container, + GimpContext *context, + gint view_size, + gint view_border_width) +{ + GimpContainerListView *list_view; + GimpContainerView *view; + + g_return_val_if_fail (container == NULL || GIMP_IS_CONTAINER (container), + NULL); + g_return_val_if_fail (context == NULL || GIMP_IS_CONTEXT (context), NULL); + g_return_val_if_fail (view_size > 0 && + view_size <= GIMP_VIEWABLE_MAX_PREVIEW_SIZE, NULL); + g_return_val_if_fail (view_border_width >= 0 && + view_border_width <= GIMP_VIEW_MAX_BORDER_WIDTH, + NULL); + + list_view = g_object_new (GIMP_TYPE_CONTAINER_LIST_VIEW, NULL); + + view = GIMP_CONTAINER_VIEW (list_view); + + gimp_container_view_set_view_size (view, view_size, view_border_width); + + if (container) + gimp_container_view_set_container (view, container); + + if (context) + gimp_container_view_set_context (view, context); + + return GTK_WIDGET (list_view); +} + + +/* GimpContainerView methods */ + +static void +gimp_container_list_view_set_container (GimpContainerView *view, + GimpContainer *container) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (view); + GimpContainer *old_container; + + old_container = gimp_container_view_get_container (view); + + if (old_container) + { +#if 0 + list_view->priv->dnd_renderer = NULL; + + g_signal_handlers_disconnect_by_func (list_view->view, + gimp_container_list_view_row_expanded, + list_view); + if (! container) + { + if (gimp_dnd_viewable_list_source_remove (GTK_WIDGET (list_view->view), + gimp_container_get_child_type (old_container))) + { + if (GIMP_VIEWABLE_CLASS (g_type_class_peek (gimp_container_get_child_type (old_container)))->get_size) + gimp_dnd_pixbuf_source_remove (GTK_WIDGET (list_view->view)); + + gtk_drag_source_unset (GTK_WIDGET (list_view->view)); + } + + g_signal_handlers_disconnect_by_func (list_view->view, + gimp_container_list_view_button, + list_view); + } +#endif + + gtk_list_box_bind_model (priv->view, NULL, NULL, NULL, NULL); + } + else if (container) + { +#if 0 + if (gimp_dnd_drag_source_set_by_type (GTK_WIDGET (list_view->view), + GDK_BUTTON1_MASK | GDK_BUTTON2_MASK, + gimp_container_get_child_type (container), + GDK_ACTION_COPY)) + { + gimp_dnd_viewable_list_source_add (GTK_WIDGET (list_view->view), + gimp_container_get_child_type (container), + gimp_container_list_view_drag_viewable_list, + list_view); + gimp_dnd_viewable_source_add (GTK_WIDGET (list_view->view), + gimp_container_get_child_type (container), + gimp_container_list_view_drag_viewable, + list_view); + + if (GIMP_VIEWABLE_CLASS (g_type_class_peek (gimp_container_get_child_type (container)))->get_size) + gimp_dnd_pixbuf_source_add (GTK_WIDGET (list_view->view), + gimp_container_list_view_drag_pixbuf, + list_view); + } + + /* connect button_press_event after DND so we can keep the list from + * selecting the item on button2 + */ + g_signal_connect (list_view->view, "button-press-event", + G_CALLBACK (gimp_container_list_view_button), + list_view); + g_signal_connect (list_view->view, "button-release-event", + G_CALLBACK (gimp_container_list_view_button), + list_view); +#endif + } + + parent_view_iface->set_container (view, container); + + if (container) + { + gtk_list_box_bind_model (priv->view, G_LIST_MODEL (container), + gimp_row_create_for_container_view, + view, NULL); + +#if 0 + gimp_container_list_view_expand_rows (list_view->model, + list_view->view, + NULL); + + g_signal_connect (list_view->view, + "row-collapsed", + G_CALLBACK (gimp_container_list_view_row_expanded), + list_view); + g_signal_connect (list_view->view, + "row-expanded", + G_CALLBACK (gimp_container_list_view_row_expanded), + list_view); +#endif + } + +#if 0 + gtk_list_view_columns_autosize (list_view->view); +#endif +} + +static void +gimp_container_list_view_set_context (GimpContainerView *view, + GimpContext *context) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (view); + GList *children; + GList *child; + + parent_view_iface->set_context (view, context); + + children = gtk_container_get_children (GTK_CONTAINER (priv->view)); + + for (child = children; child; child = g_list_next (child)) + { + gimp_row_set_context (child->data, context); + } + + g_list_free (children); +} + +static void +gimp_container_list_view_set_selection_mode (GimpContainerView *view, + GtkSelectionMode mode) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (view); + + parent_view_iface->set_selection_mode (view, mode); + + gtk_list_box_set_selection_mode (priv->view, mode); +} + +static void +gimp_container_list_view_set_view_size (GimpContainerView *view) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (view); + GList *children; + GList *child; + gint view_size; + gint border_width; + + view_size = gimp_container_view_get_view_size (view, &border_width); + + children = gtk_container_get_children (GTK_CONTAINER (priv->view)); + + for (child = children; child; child = g_list_next (child)) + { + gimp_row_set_view_size (child->data, view_size, border_width); + } + + g_list_free (children); +} + +static gboolean +gimp_container_list_view_set_selected (GimpContainerView *view, + GList *items) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (view); + GtkAdjustment *adjustment; + GtkAllocation allocation; + GList *children; + GList *child; + gdouble scroll_offset; + gint top_selected = G_MAXINT; + gint bottom_selected = 0; + + adjustment = gtk_list_box_get_adjustment (priv->view); + + scroll_offset = gtk_adjustment_get_value (adjustment); + + g_signal_handlers_block_by_func (priv->view, + gimp_container_list_view_selected_rows_changed, + view); + + gtk_list_box_unselect_all (priv->view); + + adjustment = gtk_list_box_get_adjustment (priv->view); + + children = gtk_container_get_children (GTK_CONTAINER (priv->view)); + + for (child = children; child; child = g_list_next (child)) + { + GimpViewable *viewable = gimp_row_get_viewable (child->data); + + if (g_list_find (items, viewable)) + { + + gtk_list_box_select_row (priv->view, child->data); + + gtk_widget_get_allocation (child->data, &allocation); + + top_selected = MIN (top_selected, allocation.y); + bottom_selected = MAX (bottom_selected, allocation.y + allocation.height); + } + } + + g_list_free (children); + + g_signal_handlers_unblock_by_func (priv->view, + gimp_container_list_view_selected_rows_changed, + view); + + gtk_widget_get_allocation (gtk_widget_get_parent (GTK_WIDGET (priv->view)), + &allocation); + + if (top_selected < scroll_offset) + { + gtk_adjustment_set_value (adjustment, top_selected); + } + else if (bottom_selected > scroll_offset + allocation.height) + { + gtk_adjustment_set_value (adjustment, bottom_selected - allocation.height); + } + + _gimp_container_view_selection_changed (view); + + return TRUE; +} + +static gint +gimp_container_list_view_get_selected (GimpContainerView *view, + GList **items) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (view); + GList *rows; + gint n_selected; + + rows = gtk_list_box_get_selected_rows (priv->view); + + n_selected = g_list_length (rows); + + if (items) + { + GList *row; + + for (row = rows; row; row = g_list_next (row)) + { + *items = g_list_prepend (*items, gimp_row_get_viewable (row->data)); + } + + *items = g_list_reverse (*items); + } + + g_list_free (rows); + + return n_selected; +} + + +static void +gimp_container_list_view_selected_rows_changed (GtkListBox *box, + GimpContainerListView *list_view) +{ + _gimp_container_view_selection_changed (GIMP_CONTAINER_VIEW (list_view)); +} + +static void +gimp_container_list_view_row_activated (GtkListBox *box, + GimpRow *row, + GimpContainerListView *list_view) +{ + _gimp_container_view_item_activated (GIMP_CONTAINER_VIEW (list_view), + gimp_row_get_viewable (row)); +} + +static void +gimp_container_list_view_monitor_changed (GimpContainerListView *view) +{ + GimpContainerListViewPrivate *priv = GET_PRIVATE (view); + GList *children; + GList *child; + + children = gtk_container_get_children (GTK_CONTAINER (priv->view)); + + for (child = children; child; child = g_list_next (child)) + { + gimp_row_monitor_changed (child->data); + } + + g_list_free (children); +} diff --git a/app/widgets/gimpcontainerlistview.h b/app/widgets/gimpcontainerlistview.h new file mode 100644 index 0000000000..c201dbc8e4 --- /dev/null +++ b/app/widgets/gimpcontainerlistview.h @@ -0,0 +1,52 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * gimpcontainerlistview.h + * Copyright (C) 2025 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 . + */ + +#pragma once + +#include "gimpcontainerbox.h" + + +#define GIMP_TYPE_CONTAINER_LIST_VIEW (gimp_container_list_view_get_type ()) +#define GIMP_CONTAINER_LIST_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_CONTAINER_LIST_VIEW, GimpContainerListView)) +#define GIMP_CONTAINER_LIST_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_CONTAINER_LIST_VIEW, GimpContainerListViewClass)) +#define GIMP_IS_CONTAINER_LIST_VIEW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_CONTAINER_LIST_VIEW)) +#define GIMP_IS_CONTAINER_LIST_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_CONTAINER_LIST_VIEW)) +#define GIMP_CONTAINER_LIST_VIEW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_CONTAINER_LIST_VIEW, GimpContainerListViewClass)) + + +typedef struct _GimpContainerListViewClass GimpContainerListViewClass; + +struct _GimpContainerListView +{ + GimpContainerBox parent_instance; +}; + +struct _GimpContainerListViewClass +{ + GimpContainerBoxClass parent_class; +}; + + +GType gimp_container_list_view_get_type (void) G_GNUC_CONST; + +GtkWidget * gimp_container_list_view_new (GimpContainer *container, + GimpContext *context, + gint view_size, + gint view_border_width); diff --git a/app/widgets/gimpcontrollerlist.c b/app/widgets/gimpcontrollerlist.c index ddde20de50..8c9845daff 100644 --- a/app/widgets/gimpcontrollerlist.c +++ b/app/widgets/gimpcontrollerlist.c @@ -51,6 +51,7 @@ #include "gimpmessagedialog.h" #include "gimppropwidgets.h" #include "gimprow.h" +#include "gimprow-utils.h" #include "gimpuimanager.h" #include "gimpwidgets-utils.h" @@ -290,13 +291,13 @@ gimp_controller_list_constructed (GObject *object) categories = gimp_controller_manager_get_categories (list->controller_manager); gtk_list_box_bind_model (GTK_LIST_BOX (list->available_controllers), categories, - gimp_row_create, + gimp_row_create_for_context, gimp_get_user_context (gimp_controller_manager_get_gimp (list->controller_manager)), NULL); gtk_list_box_bind_model (GTK_LIST_BOX (list->active_controllers), G_LIST_MODEL (list->controller_manager), - gimp_row_create, + gimp_row_create_for_context, gimp_get_user_context (gimp_controller_manager_get_gimp (list->controller_manager)), NULL); } diff --git a/app/widgets/gimprow-utils.c b/app/widgets/gimprow-utils.c new file mode 100644 index 0000000000..a8e9894751 --- /dev/null +++ b/app/widgets/gimprow-utils.c @@ -0,0 +1,58 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * gimprow-utils.c + * Copyright (C) 2025 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 "widgets-types.h" + +#include "core/gimpcontext.h" +#include "core/gimpviewable.h" + +#include "gimpcontainerview.h" +#include "gimprow.h" +#include "gimprow-utils.h" + + +GtkWidget * +gimp_row_create_for_context (gpointer item, + gpointer context) +{ + g_return_val_if_fail (GIMP_IS_VIEWABLE (item), NULL); + g_return_val_if_fail (GIMP_IS_CONTEXT (context), NULL); + + return gimp_row_new (context, item); +} + +GtkWidget * +gimp_row_create_for_container_view (gpointer item, + gpointer container_view) +{ + GimpContext *context; + + g_return_val_if_fail (GIMP_IS_VIEWABLE (item), NULL); + g_return_val_if_fail (GIMP_IS_CONTAINER_VIEW (container_view), NULL); + + context = gimp_container_view_get_context (container_view); + + return gimp_row_new (context, item); +} diff --git a/app/widgets/gimprow-utils.h b/app/widgets/gimprow-utils.h new file mode 100644 index 0000000000..9e5f112f35 --- /dev/null +++ b/app/widgets/gimprow-utils.h @@ -0,0 +1,29 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * gimprow-utils.h + * Copyright (C) 2025 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 . + */ + +#pragma once + + +/* GtkListBoxCreateWidgetFunc */ + +GtkWidget * gimp_row_create_for_context (gpointer item, + gpointer context); +GtkWidget * gimp_row_create_for_container_view (gpointer item, + gpointer container_view); diff --git a/app/widgets/gimprow.c b/app/widgets/gimprow.c index 52dd84a259..74678e24d6 100644 --- a/app/widgets/gimprow.c +++ b/app/widgets/gimprow.c @@ -30,6 +30,8 @@ #include "core/gimpcontext.h" #include "core/gimpviewable.h" +#include "gimpdnd.h" +#include "gimpeditor.h" #include "gimprow.h" #include "gimpview.h" #include "gimpviewrenderer.h" @@ -91,6 +93,8 @@ static void gimp_row_get_property (GObject *object, GValue *value, GParamSpec *pspec); +static gboolean gimp_row_button_press_event (GtkWidget *widget, + GdkEventButton *bevent); static void gimp_row_style_updated (GtkWidget *widget); static gboolean gimp_row_query_tooltip (GtkWidget *widget, gint x, @@ -121,6 +125,13 @@ static void gimp_row_label_long_pressed (GtkGesture *gesture, static void gimp_row_rename_entry_activate (GtkEntry *entry, GimpRow *row); +static GimpViewable * + gimp_row_drag_viewable (GtkWidget *widget, + GimpContext **context, + gpointer data); +static GdkPixbuf *gimp_row_drag_pixbuf (GtkWidget *widget, + gpointer data); + G_DEFINE_TYPE_WITH_PRIVATE (GimpRow, gimp_row, GTK_TYPE_LIST_BOX_ROW) @@ -136,20 +147,21 @@ gimp_row_class_init (GimpRowClass *klass) GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkBindingSet *binding_set; - object_class->constructed = gimp_row_constructed; - object_class->dispose = gimp_row_dispose; - object_class->set_property = gimp_row_set_property; - object_class->get_property = gimp_row_get_property; + object_class->constructed = gimp_row_constructed; + object_class->dispose = gimp_row_dispose; + object_class->set_property = gimp_row_set_property; + object_class->get_property = gimp_row_get_property; - widget_class->style_updated = gimp_row_style_updated; - widget_class->query_tooltip = gimp_row_query_tooltip; + widget_class->button_press_event = gimp_row_button_press_event; + widget_class->style_updated = gimp_row_style_updated; + widget_class->query_tooltip = gimp_row_query_tooltip; - klass->set_context = gimp_row_real_set_context; - klass->set_viewable = gimp_row_real_set_viewable; - klass->set_view_size = gimp_row_real_set_view_size; - klass->monitor_changed = gimp_row_real_monitor_changed; - klass->edit_name = gimp_row_real_edit_name; - klass->name_edited = gimp_row_real_name_edited; + klass->set_context = gimp_row_real_set_context; + klass->set_viewable = gimp_row_real_set_viewable; + klass->set_view_size = gimp_row_real_set_view_size; + klass->monitor_changed = gimp_row_real_monitor_changed; + klass->edit_name = gimp_row_real_edit_name; + klass->name_edited = gimp_row_real_name_edited; row_signals[EDIT_NAME] = g_signal_new ("edit-name", @@ -235,7 +247,8 @@ gimp_row_init (GimpRow *row) static void gimp_row_constructed (GObject *object) { - GimpRowPrivate *priv = GET_PRIVATE (object); + GimpRowPrivate *priv = GET_PRIVATE (object); + gboolean preview = FALSE; G_OBJECT_CLASS (parent_class)->constructed (object); @@ -243,9 +256,16 @@ gimp_row_constructed (GObject *object) priv->view_size, priv->view_border_width, FALSE); + GIMP_VIEW (priv->view)->eat_button_events = FALSE; + GIMP_VIEW (priv->view)->show_popup = TRUE; gtk_box_pack_start (GTK_BOX (priv->box), priv->view, FALSE, FALSE, 0); - gtk_widget_set_visible (priv->view, TRUE); + if (priv->viewable) + preview = (GIMP_VIEWABLE_GET_CLASS (priv->viewable)->get_preview != NULL || + GIMP_VIEWABLE_GET_CLASS (priv->viewable)->get_new_preview != NULL); + + gtk_widget_set_visible (priv->icon, ! preview); + gtk_widget_set_visible (priv->view, preview); } static void @@ -340,6 +360,43 @@ gimp_row_get_property (GObject *object, } } +static gboolean +gimp_row_button_press_event (GtkWidget *widget, + GdkEventButton *bevent) +{ + GdkEvent *event = (GdkEvent *) bevent; + gboolean context_menu; + + GTK_WIDGET_CLASS (parent_class)->button_press_event (widget, bevent); + + context_menu = gdk_event_triggers_context_menu (event); + + if (bevent->button == 1 || context_menu) + { + GtkWidget *list_box = gtk_widget_get_parent (widget); + + if (GTK_IS_LIST_BOX (list_box)) + { + gtk_list_box_select_row (GTK_LIST_BOX (list_box), + GTK_LIST_BOX_ROW (widget)); + + if (context_menu) + { + GtkWidget *editor = gtk_widget_get_ancestor (widget, + GIMP_TYPE_EDITOR); + + if (editor) + { + return gimp_editor_popup_menu_at_pointer (GIMP_EDITOR (editor), + event); + } + } + } + } + + return FALSE; +} + static void gimp_row_style_updated (GtkWidget *widget) { @@ -400,7 +457,12 @@ static void gimp_row_real_set_viewable (GimpRow *row, GimpViewable *viewable) { - GimpRowPrivate *priv = GET_PRIVATE (row); + GimpRowPrivate *priv = GET_PRIVATE (row); + GType viewable_type = G_TYPE_NONE; + gboolean preview = FALSE; + + if (viewable) + viewable_type = G_TYPE_FROM_INSTANCE (viewable); if (priv->viewable) { @@ -410,6 +472,36 @@ gimp_row_real_set_viewable (GimpRow *row, g_signal_handlers_disconnect_by_func (priv->viewable, gimp_row_viewable_name_changed, row); + + if (! viewable) + { + if (gimp_dnd_viewable_source_remove (GTK_WIDGET (row), + G_TYPE_FROM_INSTANCE (priv->viewable))) + { + if (gimp_viewable_get_size (priv->viewable, NULL, NULL)) + gimp_dnd_pixbuf_source_remove (GTK_WIDGET (row)); + + gtk_drag_source_unset (GTK_WIDGET (row)); + } + } + } + else if (viewable) + { + if (gimp_dnd_drag_source_set_by_type (GTK_WIDGET (row), + GDK_BUTTON1_MASK | GDK_BUTTON2_MASK, + viewable_type, + GDK_ACTION_COPY)) + { + gimp_dnd_viewable_source_add (GTK_WIDGET (row), + viewable_type, + gimp_row_drag_viewable, + NULL); + + if (gimp_viewable_get_size (viewable, NULL, NULL)) + gimp_dnd_pixbuf_source_add (GTK_WIDGET (row), + gimp_row_drag_pixbuf, + NULL); + } } g_set_object (&priv->viewable, viewable); @@ -423,13 +515,24 @@ gimp_row_real_set_viewable (GimpRow *row, g_signal_connect (priv->viewable, "notify::icon-name", G_CALLBACK (gimp_row_viewable_icon_changed), row); + + preview = (GIMP_VIEWABLE_GET_CLASS (priv->viewable)->get_preview != NULL || + GIMP_VIEWABLE_GET_CLASS (priv->viewable)->get_new_preview != NULL); } gimp_row_viewable_name_changed (priv->viewable, row); - gimp_row_viewable_icon_changed (priv->viewable, NULL, row); + + if (priv->icon) + { + gtk_widget_set_visible (priv->icon, ! preview); + gimp_row_viewable_icon_changed (priv->viewable, NULL, row); + } if (priv->view) - gimp_view_set_viewable (GIMP_VIEW (priv->view), priv->viewable); + { + gtk_widget_set_visible (priv->view, preview); + gimp_view_set_viewable (GIMP_VIEW (priv->view), priv->viewable); + } } static void @@ -634,13 +737,6 @@ gimp_row_monitor_changed (GimpRow *row) GIMP_ROW_GET_CLASS (row)->monitor_changed (row); } -GtkWidget * -gimp_row_create (gpointer item, - gpointer user_data) -{ - return gimp_row_new (user_data, item); -} - /* private functions */ @@ -702,3 +798,35 @@ gimp_row_rename_entry_activate (GtkEntry *entry, gtk_popover_popdown (GTK_POPOVER (priv->popover)); } + +static GimpViewable * +gimp_row_drag_viewable (GtkWidget *widget, + GimpContext **context, + gpointer data) +{ + GimpRowPrivate *priv = GET_PRIVATE (widget); + + if (context) + *context = priv->context; + + return priv->viewable; +} + +static GdkPixbuf * +gimp_row_drag_pixbuf (GtkWidget *widget, + gpointer data) +{ + GimpRowPrivate *priv = GET_PRIVATE (widget); + gint width; + gint height; + + if (priv->viewable && + gimp_viewable_get_size (priv->viewable, &width, &height)) + { + return gimp_viewable_get_new_pixbuf (priv->viewable, + priv->context, + width, height, NULL, NULL); + } + + return NULL; +} diff --git a/app/widgets/gimprow.h b/app/widgets/gimprow.h index e259a50316..e6cda2879b 100644 --- a/app/widgets/gimprow.h +++ b/app/widgets/gimprow.h @@ -67,8 +67,3 @@ gint gimp_row_get_view_size (GimpRow *row, gint *view_border_width); void gimp_row_monitor_changed (GimpRow *row); - - -/* a generic GtkListBoxCreateWidgetFunc */ -GtkWidget * gimp_row_create (gpointer item, - gpointer user_data); diff --git a/app/widgets/meson.build b/app/widgets/meson.build index 1cd8bafb5b..3a930effb1 100644 --- a/app/widgets/meson.build +++ b/app/widgets/meson.build @@ -65,6 +65,7 @@ libappwidgets_sources = [ 'gimpcontainereditor.c', 'gimpcontainerentry.c', 'gimpcontainericonview.c', + 'gimpcontainerlistview.c', 'gimpcontainerpopup.c', 'gimpcontainertreestore.c', 'gimpcontainertreeview-dnd.c', @@ -190,6 +191,7 @@ libappwidgets_sources = [ 'gimpradioaction.c', 'gimprender.c', 'gimprow.c', + 'gimprow-utils.c', 'gimpsamplepointeditor.c', 'gimpsavedialog.c', 'gimpsearchpopup.c', diff --git a/app/widgets/widgets-types.h b/app/widgets/widgets-types.h index 6ddca18aee..d7f3fdfe4e 100644 --- a/app/widgets/widgets-types.h +++ b/app/widgets/widgets-types.h @@ -88,6 +88,7 @@ typedef struct _GimpContainerBox GimpContainerBox; typedef struct _GimpContainerComboBox GimpContainerComboBox; typedef struct _GimpContainerEntry GimpContainerEntry; typedef struct _GimpContainerIconView GimpContainerIconView; +typedef struct _GimpContainerListView GimpContainerListView; typedef struct _GimpContainerTreeStore GimpContainerTreeStore; typedef struct _GimpContainerTreeView GimpContainerTreeView; typedef struct _GimpContainerView GimpContainerView; /* dummy typedef */