gimp/plug-ins/filter-browser/filter-browser.c
Ondřej Míchal 4e9c8bc3ee plug-ins: Add GEGL filter API browser
A new browser for exploring the available and supported GEGL operations
for use with the filter API.

This raises a few warnings due to missing support for some GEGL
parameter types. The warnings are harmless because as a consequence the
passed parameters are set to NULL which the browser expects and marks
such parameters as 'unknown'.
2025-06-20 16:33:13 -03:00

626 lines
21 KiB
C

/*
* GIMP plug-in for browsing available GEGL filters.
*
* Copyright (C) 2025 Ondřej Míchal <harrymichal@seznam.cz>
* Copyright (C) 2017 Øyvind Kolås <pippin@gimp.org>
*
* This library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <gegl-plugin.h>
#include <gio/gio.h>
#include <glib-object.h>
#include <glib.h>
#include <gtk/gtk.h>
#include <operation/gegl-operation.h>
#include <string.h>
#include "libgimp/stdplugins-intl.h"
#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>
#include "gegl-filter-info.h"
#define PLUG_IN_GEGL_FILTER_BROWSER "plug-in-gegl-filter-browser"
#define PLUG_IN_BINARY "gegl-filter-browser"
#define PLUG_IN_ROLE "gegl-filter-browser"
#define FILTER_BROWSER_WIDTH 800
#define FILTER_BROWSER_HEIGHT 500
typedef enum
{
SEARCH_NAME,
SEARCH_TITLE,
SEARCH_DESCRIPTION,
SEARCH_CATEGORY,
} FilterBrowserSearchType;
typedef struct
{
GtkWidget *dialog;
GtkWidget *browser;
GtkListBox *filter_list;
GList *filters;
FilterBrowserSearchType search_type;
} FilterBrowserPrivate;
typedef struct _FilterBrowser FilterBrowser;
typedef struct _FilterBrowserClass FilterBrowserClass;
struct _FilterBrowser
{
GimpPlugIn parent_instance;
};
struct _FilterBrowserClass
{
GimpPlugInClass parent_class;
};
#define FILTER_BROWSER_TYPE (filter_browser_get_type ())
#define FILTER_BROWSER(obj) \
(g_type_check_instance_cast ((obj), browser_type, FilterBrowser))
GType filter_browser_get_type (void) G_GNUC_CONST;
G_DEFINE_TYPE (FilterBrowser, filter_browser, GIMP_TYPE_PLUG_IN)
GIMP_MAIN (FILTER_BROWSER_TYPE)
DEFINE_STD_SET_I18N
static GtkWidget *
create_filter_info_view (GimpGeglFilterInfo *filter_info)
{
GtkWidget *frame = NULL;
GtkWidget *label = NULL;
GtkWidget *vbox = NULL;
GtkWidget *view = NULL;
gchar *buf = NULL;
const gchar *name = NULL;
const gchar *title = NULL;
const gchar *description = NULL;
const gchar *categories = NULL;
const gchar *license = NULL;
const GimpValueArray *pspecs = NULL;
g_object_get (G_OBJECT (filter_info),
"name", &name,
"title", &title,
"description", &description,
"categories", &categories,
"license", &license,
"pspecs", &pspecs,
NULL);
/*
* TODO: Information to include
* - what is the are of effect? (Area, Point,..)
* - Special requirements and attributes (needs alpha, is position dependent?,..)
*/
if (description && strlen (description) < 2)
description = NULL;
if (categories && strlen (categories) < 2)
categories = NULL;
if (license && strlen (license) < 2)
license = NULL;
view = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
/*
* Main information
*/
if (title != NULL)
frame = gimp_frame_new (title);
else
frame = gimp_frame_new (name);
gtk_label_set_selectable (GTK_LABEL (gtk_frame_get_label_widget (GTK_FRAME (frame))), TRUE);
gtk_box_pack_start (GTK_BOX (view), frame, FALSE, FALSE, 0);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
gtk_container_add (GTK_CONTAINER (frame), vbox);
buf = g_strdup_printf ("operation name: %s", name);
label = gtk_label_new (buf);
g_free (buf);
gtk_label_set_selectable (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
if (categories != NULL)
{
gchar *categories_buf = NULL;
categories_buf = g_strdelimit (g_strdup (categories), ":", ' ');
buf = g_strdup_printf ("categories: %s", categories_buf);
g_free (categories_buf);
label = gtk_label_new (buf);
g_free (buf);
gtk_label_set_selectable (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
}
if (description != NULL)
{
label = gtk_label_new (description);
gtk_label_set_selectable (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
}
/*
* Parameters
*/
if (pspecs != NULL && gimp_value_array_length (pspecs) != 0)
{
GtkWidget *grid = NULL;
frame = gimp_frame_new (_ ("Parameters"));
gtk_box_pack_start (GTK_BOX (view), frame, FALSE, FALSE, 0);
gtk_widget_show (frame);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
gtk_container_add (GTK_CONTAINER (frame), vbox);
gtk_widget_show (vbox);
grid = gtk_grid_new ();
gtk_grid_set_column_spacing (GTK_GRID (grid), 6);
gtk_grid_set_row_spacing (GTK_GRID (grid), 4);
gtk_box_pack_start (GTK_BOX (vbox), grid, FALSE, FALSE, 0);
gtk_widget_show (grid);
for (gint i = 0; i < gimp_value_array_length (pspecs); i++)
{
GParamSpec *pspec = g_value_get_param (gimp_value_array_index (pspecs, i));
GtkWidget *label = NULL;
/* Some param specs are not supported by the wire. Thankfully, the
* wire is well-behaved and returns NULL in such cases for each
* param spec. */
if (pspec == NULL) {
label = gtk_label_new ("unknown");
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_label_set_yalign (GTK_LABEL (label), 0.0);
gtk_grid_attach (GTK_GRID (grid), label, 0, i, 1, 1);
gtk_widget_show (label);
continue;
}
label = gtk_label_new (g_param_spec_get_name (pspec));
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_label_set_yalign (GTK_LABEL (label), 0.0);
gtk_grid_attach (GTK_GRID (grid), label, 0, i, 1, 1);
gtk_widget_show (label);
label = gtk_label_new (g_type_name (G_PARAM_SPEC_VALUE_TYPE (pspec)));
gimp_label_set_attributes (GTK_LABEL (label), PANGO_ATTR_FAMILY, "monospace",
PANGO_ATTR_STYLE, PANGO_STYLE_ITALIC, -1);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_label_set_yalign (GTK_LABEL (label), 0.0);
gtk_grid_attach (GTK_GRID (grid), label, 1, i, 1, 1);
gtk_widget_show (label);
label = gtk_label_new (g_param_spec_get_blurb (pspec));
gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
gtk_label_set_selectable (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_label_set_yalign (GTK_LABEL (label), 0.0);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_grid_attach (GTK_GRID (grid), label, 2, i, 1, 1);
gtk_widget_show (label);
}
}
/*
* Additional information
*/
if (license != NULL)
{
frame = gimp_frame_new (_("Additional Information"));
gtk_box_pack_start (GTK_BOX (view), frame, FALSE, FALSE, 0);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
gtk_container_add (GTK_CONTAINER (frame), vbox);
buf = g_strdup_printf ("license: %s", license);
label = gtk_label_new (buf);
g_free (buf);
gtk_label_set_selectable (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
}
gtk_widget_show_all (view);
return view;
}
static void
filter_list_row_selected (GtkListBox *filter_list,
GtkListBoxRow *row,
gpointer data)
{
FilterBrowserPrivate *browser = NULL;
GimpGeglFilterInfo *filter_info = NULL;
browser = (FilterBrowserPrivate *) data;
g_return_if_fail (GTK_IS_LIST_BOX (filter_list));
if (row == NULL)
return;
filter_info = g_object_get_data (G_OBJECT (row), "filter-info");
gimp_browser_set_widget (GIMP_BROWSER (browser->browser),
create_filter_info_view (filter_info));
}
static gint
gegl_filter_info_sort_func (gconstpointer a,
gconstpointer b)
{
GimpGeglFilterInfo *info_a = (GimpGeglFilterInfo *) a;
GimpGeglFilterInfo *info_b = (GimpGeglFilterInfo *) b;
gchar *name_a = NULL;
gchar *name_b = NULL;
g_assert_true (GIMP_IS_GEGL_FILTER_INFO (info_a));
g_assert_true (GIMP_IS_GEGL_FILTER_INFO (info_b));
g_object_get (G_OBJECT (info_a), "name", &name_a, NULL);
g_object_get (G_OBJECT (info_b), "name", &name_b, NULL);
return g_strcmp0 (name_a, name_b);
}
static GtkWidget *
create_filter_list_row (gpointer item,
gpointer user_data)
{
GimpGeglFilterInfo *filter_info = item;
GtkWidget *row = NULL;
GtkWidget *box = NULL;
GtkWidget *name_label = NULL;
const gchar *name = NULL;
const gchar *categories = NULL;
row = gtk_list_box_row_new ();
g_object_set_data (G_OBJECT (row), "filter-info", filter_info);
g_object_get (G_OBJECT (filter_info), "name", &name, "categories", &categories, NULL);
box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_focus_on_click(box, FALSE);
gtk_container_add (GTK_CONTAINER (row), box);
name_label = gtk_label_new (name);
gtk_box_pack_start (GTK_BOX (box), name_label, FALSE, FALSE, 0);
gtk_widget_show_all (GTK_WIDGET (row));
return row;
}
static void
insert_gegl_filter_info_into_list_store (gpointer data,
gpointer user_data)
{
GimpGeglFilterInfo *filter_info = (GimpGeglFilterInfo *) data;
GListStore *model = (GListStore *) (user_data);
g_assert_true (GIMP_IS_GEGL_FILTER_INFO (filter_info));
g_assert_true (G_IS_LIST_STORE (model));
g_list_store_append (model, filter_info);
}
static void
filter_browser_search (GimpBrowser *gimp_browser,
const gchar *search_text,
FilterBrowserSearchType search_type,
FilterBrowserPrivate *browser)
{
GListStore *new_model = NULL;
gchar *search_msg = NULL;
gchar *search_summary = NULL;
guint matches;
switch (search_type)
{
case SEARCH_NAME:
search_msg = _("Searching by name");
break;
case SEARCH_TITLE:
search_msg = _("Searching by title");
break;
case SEARCH_DESCRIPTION:
search_msg = _("Searching by description");
break;
case SEARCH_CATEGORY:
search_msg = _("Searching by category");
default:
return;
}
gimp_browser_show_message (GIMP_BROWSER (browser->browser), search_msg);
new_model = g_list_store_new (GIMP_GEGL_FILTER_TYPE_INFO);
if (strlen (search_text) != 0)
{
for (GList *iter = browser->filters; iter != NULL; iter = iter->next)
{
GimpGeglFilterInfo *filter_info = NULL;
const gchar *value = NULL;
filter_info = (GimpGeglFilterInfo *) iter->data;
switch (search_type)
{
case SEARCH_NAME:
g_object_get (G_OBJECT (filter_info), "name", &value, NULL);
break;
case SEARCH_TITLE:
g_object_get (G_OBJECT (filter_info), "title", &value, NULL);
break;
case SEARCH_DESCRIPTION:
g_object_get (G_OBJECT (filter_info), "description", &value, NULL);
break;
case SEARCH_CATEGORY:
g_object_get (G_OBJECT (filter_info), "categories", &value, NULL);
default:
return;
}
if (value == NULL)
continue;
if (! g_str_match_string (search_text, value, TRUE))
continue;
insert_gegl_filter_info_into_list_store (filter_info, new_model);
}
}
else
{
g_list_foreach (browser->filters, insert_gegl_filter_info_into_list_store, new_model);
}
gtk_list_box_bind_model (browser->filter_list, G_LIST_MODEL (new_model),
create_filter_list_row, NULL, NULL);
gtk_list_box_select_row (browser->filter_list,
gtk_list_box_get_row_at_index (browser->filter_list, 0));
matches = g_list_model_get_n_items (G_LIST_MODEL (new_model));
if (search_text != NULL && strlen (search_text) > 0)
{
if (matches == 0)
{
search_summary = g_strdup (_("No matches for your query"));
gimp_browser_show_message (GIMP_BROWSER (browser->browser),
_("No matches"));
}
else
{
search_summary = g_strdup_printf (
ngettext ("%d filter matches your query", "%d filters match your query", matches),
matches);
}
}
else
{
search_summary = g_strdup_printf (ngettext ("%d filter", "%d filters", matches), matches);
}
gimp_browser_set_search_summary (gimp_browser, search_summary);
g_free (search_summary);
}
static void
browser_dialog_response (GtkWidget *widget,
gint response_id,
FilterBrowserPrivate *browser)
{
gtk_widget_destroy (browser->dialog);
g_list_free (browser->filters);
g_free (browser);
g_main_loop_quit (g_main_loop_new (NULL, TRUE));
}
static GimpValueArray *
filter_browser_run (GimpProcedure *procedure,
GimpProcedureConfig *config,
gpointer run_data)
{
FilterBrowserPrivate *browser = NULL;
GtkWidget *scrolled_window = NULL;
GtkStyleContext *style_context = NULL;
GListStore *model = NULL;
gchar **filter_names = NULL;
gimp_ui_init (PLUG_IN_BINARY);
browser = g_new0 (FilterBrowserPrivate, 1);
browser->search_type = SEARCH_NAME;
browser->dialog = gimp_dialog_new (_("GEGL Filter Browser"),
PLUG_IN_ROLE,
NULL,
0,
gimp_standard_help_func,
PLUG_IN_GEGL_FILTER_BROWSER,
_("_Close"),
GTK_RESPONSE_CLOSE,
NULL);
gtk_window_set_default_size (GTK_WINDOW (browser->dialog),
FILTER_BROWSER_WIDTH, FILTER_BROWSER_HEIGHT);
g_signal_connect (browser->dialog, "response",
G_CALLBACK (browser_dialog_response), browser);
browser->browser = gimp_browser_new ();
gimp_browser_add_search_types (GIMP_BROWSER (browser->browser),
_("by name"), SEARCH_NAME,
_("by title"), SEARCH_TITLE,
_("by description"), SEARCH_DESCRIPTION,
NULL);
gtk_container_set_border_width (GTK_CONTAINER (browser->browser), 12);
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (browser->dialog))),
browser->browser, TRUE, TRUE, 0);
gtk_widget_show (browser->browser);
g_signal_connect (browser->browser, "search", G_CALLBACK (filter_browser_search), browser);
scrolled_window = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), GTK_SHADOW_IN);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_box_pack_start (GTK_BOX (gimp_browser_get_left_vbox (GIMP_BROWSER (browser->browser))),
scrolled_window, TRUE, TRUE, 0);
gtk_widget_show (scrolled_window);
/*
* TODO: Extract the data preparation, so that it can run on idle and not
* block the GUI.
*/
filter_names = gimp_drawable_filter_operation_get_available ();
for (gint i = 0; filter_names[i] != NULL; i++)
{
GimpGeglFilterInfo *info = NULL;
gchar *title = NULL;
gchar *description = NULL;
gchar *categories = NULL;
gchar *license = NULL;
GimpValueArray *pspecs = NULL;
gimp_drawable_filter_operation_get_details (filter_names[i],
&title,
&description,
&categories,
&license);
pspecs = gimp_drawable_filter_operation_get_pspecs (filter_names[i]);
info = g_object_new (GIMP_GEGL_FILTER_TYPE_INFO,
"name", filter_names[i],
"title", title,
"description", description,
"categories", categories,
"license", license,
"pspecs", pspecs,
NULL);
browser->filters = g_list_append (browser->filters, info);
g_free (title);
g_free (description);
g_free (categories);
g_free (license);
gimp_value_array_unref (pspecs);
}
g_strfreev (filter_names);
browser->filters = g_list_sort (browser->filters, gegl_filter_info_sort_func);
model = g_list_store_new (GIMP_GEGL_FILTER_TYPE_INFO);
g_list_foreach (browser->filters, insert_gegl_filter_info_into_list_store, model);
browser->filter_list = GTK_LIST_BOX (gtk_list_box_new ());
style_context = gtk_widget_get_style_context(GTK_WIDGET (browser->filter_list));
gtk_style_context_add_class (style_context, "view");
gtk_list_box_set_selection_mode (GTK_LIST_BOX (browser->filter_list), GTK_SELECTION_BROWSE);
gtk_list_box_bind_model (GTK_LIST_BOX (browser->filter_list),
G_LIST_MODEL (model), create_filter_list_row, NULL, NULL);
gtk_container_add (GTK_CONTAINER (scrolled_window), GTK_WIDGET (browser->filter_list));
gtk_widget_show (GTK_WIDGET (browser->filter_list));
g_signal_connect (browser->filter_list, "row-selected",
G_CALLBACK (filter_list_row_selected), browser);
gtk_list_box_select_row (browser->filter_list,
gtk_list_box_get_row_at_index (browser->filter_list, 0));
gtk_widget_show (GTK_WIDGET (browser->dialog));
g_main_loop_run (g_main_loop_new (NULL, TRUE));
return gimp_procedure_new_return_values (procedure, GIMP_PDB_SUCCESS, NULL);
}
static GimpProcedure *
filter_browser_create_procedure (GimpPlugIn *plug_in,
const gchar *procedure_name)
{
GimpProcedure *procedure = NULL;
if (strcmp (procedure_name, PLUG_IN_GEGL_FILTER_BROWSER))
return procedure;
procedure = gimp_procedure_new (plug_in, procedure_name, GIMP_PDB_PROC_TYPE_PLUGIN,
filter_browser_run, NULL, NULL);
gimp_procedure_set_menu_label (procedure, _ ("_GEGL Filter Browser"));
gimp_procedure_set_icon_name (procedure, GIMP_ICON_PLUGIN);
gimp_procedure_add_menu_path (procedure, "<Image>/Help/[Programming]");
gimp_procedure_set_documentation (procedure,
_("Display information about available"
" GEGL operations (i.e., filters)."),
_("Shows a list of all GEGL operations,"
" their details and what parameters they"
" are configured with. The list contains"
" operations provided by GEGL itself,"
" GIMP and plug-ins loaded by GEGL."),
PLUG_IN_GEGL_FILTER_BROWSER);
gimp_procedure_set_attribution (procedure, "Ondřej Míchal", "Ondřej Míchal", "2025");
gimp_procedure_add_enum_argument (procedure, "run-mode", "Run mode",
"The run mode", GIMP_TYPE_RUN_MODE,
GIMP_RUN_INTERACTIVE, G_PARAM_READWRITE);
return procedure;
}
static GList *
filter_browser_query_procedures (GimpPlugIn *plug_in)
{
return g_list_append (NULL, g_strdup (PLUG_IN_GEGL_FILTER_BROWSER));
}
static void
filter_browser_init (FilterBrowser *browser)
{
}
static void
filter_browser_class_init (FilterBrowserClass *klass)
{
GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
plug_in_class->query_procedures = filter_browser_query_procedures;
plug_in_class->create_procedure = filter_browser_create_procedure;
plug_in_class->set_i18n = STD_SET_I18N;
}