gimp/libgimp/gimpplugin.c

1421 lines
41 KiB
C
Raw Normal View History

/* LIBGIMP - The GIMP Library
* Copyright (C) 1995-2003 Peter Mattis and Spencer Kimball
*
* gimpplugin.c
* Copyright (C) 2019 Michael Natterer <mitch@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
* Lesser 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 <errno.h>
#include <string.h>
#define GIMP_DISABLE_COMPAT_CRUFT
#include "gimp.h"
#include "libgimpbase/gimpprotocol.h"
#include "libgimpbase/gimpwire.h"
#include "gimp-private.h"
#include "gimp-shm.h"
#include "gimpgpparams.h"
#include "gimpplugin-private.h"
#include "gimpplugin_pdb.h"
#include "gimpprocedure-private.h"
/**
* SECTION: gimpplugin
* @title: GimpPlugIn
* @short_description: The base class for plug-ins to derive from
*
* The base class for plug-ins to derive from. Manages the plug-in's
* #GimpProcedure objects.
*
* Since: 3.0
**/
#define WRITE_BUFFER_SIZE 1024
enum
{
PROP_0,
PROP_READ_CHANNEL,
PROP_WRITE_CHANNEL,
N_PROPS
};
typedef struct _GimpPlugInMenuBranch GimpPlugInMenuBranch;
struct _GimpPlugInMenuBranch
{
gchar *menu_path;
gchar *menu_label;
};
struct _GimpPlugInPrivate
{
GIOChannel *read_channel;
GIOChannel *write_channel;
gchar write_buffer[WRITE_BUFFER_SIZE];
gulong write_buffer_index;
guint extension_source_id;
gchar *translation_domain_name;
GFile *translation_domain_path;
gchar *help_domain_name;
GFile *help_domain_uri;
GList *menu_branches;
GList *temp_procedures;
GList *procedure_stack;
GHashTable *displays;
GHashTable *images;
GHashTable *items;
};
static void gimp_plug_in_constructed (GObject *object);
static void gimp_plug_in_finalize (GObject *object);
static void gimp_plug_in_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static void gimp_plug_in_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
static void gimp_plug_in_register (GimpPlugIn *plug_in,
GList *procedures);
static gboolean gimp_plug_in_write (GIOChannel *channel,
const guint8 *buf,
gulong count,
gpointer user_data);
static gboolean gimp_plug_in_flush (GIOChannel *channel,
gpointer user_data);
static gboolean gimp_plug_in_io_error_handler (GIOChannel *channel,
GIOCondition cond,
gpointer data);
static void gimp_plug_in_loop (GimpPlugIn *plug_in);
static void gimp_plug_in_single_message (GimpPlugIn *plug_in);
static void gimp_plug_in_process_message (GimpPlugIn *plug_in,
GimpWireMessage *msg);
static void gimp_plug_in_proc_run (GimpPlugIn *plug_in,
GPProcRun *proc_run);
static void gimp_plug_in_temp_proc_run (GimpPlugIn *plug_in,
GPProcRun *proc_run);
static void gimp_plug_in_proc_run_internal (GimpPlugIn *plug_in,
GPProcRun *proc_run,
GimpProcedure *procedure,
GPProcReturn *proc_return);
static gboolean gimp_plug_in_extension_read (GIOChannel *channel,
GIOCondition condition,
gpointer data);
static void gimp_plug_in_push_procedure (GimpPlugIn *plug_in,
GimpProcedure *procedure);
static void gimp_plug_in_pop_procedure (GimpPlugIn *plug_in,
GimpProcedure *procedure);
static void gimp_plug_in_destroy_hashes (GimpPlugIn *plug_in);
static void gimp_plug_in_destroy_proxies (GHashTable *hash_table,
gboolean finalize);
G_DEFINE_TYPE_WITH_PRIVATE (GimpPlugIn, gimp_plug_in, G_TYPE_OBJECT)
#define parent_class gimp_plug_in_parent_class
static GParamSpec *props[N_PROPS] = { NULL, };
static void
gimp_plug_in_class_init (GimpPlugInClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructed = gimp_plug_in_constructed;
object_class->finalize = gimp_plug_in_finalize;
object_class->set_property = gimp_plug_in_set_property;
object_class->get_property = gimp_plug_in_get_property;
props[PROP_READ_CHANNEL] =
g_param_spec_boxed ("read-channel",
"Read channel",
"The GIOChanel to read from GIMP",
G_TYPE_IO_CHANNEL,
GIMP_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY);
props[PROP_WRITE_CHANNEL] =
g_param_spec_boxed ("write-channel",
"Write channel",
"The GIOChanel to write to GIMP",
G_TYPE_IO_CHANNEL,
GIMP_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY);
g_object_class_install_properties (object_class, N_PROPS, props);
}
static void
gimp_plug_in_init (GimpPlugIn *plug_in)
{
plug_in->priv = gimp_plug_in_get_instance_private (plug_in);
}
static void
gimp_plug_in_constructed (GObject *object)
{
GimpPlugIn *plug_in = GIMP_PLUG_IN (object);
G_OBJECT_CLASS (parent_class)->constructed (object);
g_assert (plug_in->priv->read_channel != NULL);
g_assert (plug_in->priv->write_channel != NULL);
gp_init ();
gimp_wire_set_writer (gimp_plug_in_write);
gimp_wire_set_flusher (gimp_plug_in_flush);
}
static void
gimp_plug_in_finalize (GObject *object)
{
GimpPlugIn *plug_in = GIMP_PLUG_IN (object);
GList *list;
if (plug_in->priv->extension_source_id)
{
g_source_remove (plug_in->priv->extension_source_id);
plug_in->priv->extension_source_id = 0;
}
if (plug_in->priv->temp_procedures)
{
g_list_free_full (plug_in->priv->temp_procedures, g_object_unref);
plug_in->priv->temp_procedures = NULL;
}
g_clear_pointer (&plug_in->priv->translation_domain_name, g_free);
g_clear_object (&plug_in->priv->translation_domain_path);
g_clear_pointer (&plug_in->priv->help_domain_name, g_free);
g_clear_object (&plug_in->priv->help_domain_uri);
for (list = plug_in->priv->menu_branches; list; list = g_list_next (list))
{
GimpPlugInMenuBranch *branch = list->data;
g_free (branch->menu_path);
g_free (branch->menu_label);
g_slice_free (GimpPlugInMenuBranch, branch);
}
g_clear_pointer (&plug_in->priv->menu_branches, g_list_free);
gimp_plug_in_destroy_proxies (plug_in->priv->displays, TRUE);
gimp_plug_in_destroy_proxies (plug_in->priv->images, TRUE);
gimp_plug_in_destroy_proxies (plug_in->priv->items, TRUE);
gimp_plug_in_destroy_hashes (plug_in);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gimp_plug_in_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GimpPlugIn *plug_in = GIMP_PLUG_IN (object);
switch (property_id)
{
case PROP_READ_CHANNEL:
plug_in->priv->read_channel = g_value_get_boxed (value);
break;
case PROP_WRITE_CHANNEL:
plug_in->priv->write_channel = g_value_get_boxed (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gimp_plug_in_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GimpPlugIn *plug_in = GIMP_PLUG_IN (object);
switch (property_id)
{
case PROP_READ_CHANNEL:
g_value_set_boxed (value, plug_in->priv->read_channel);
break;
case PROP_WRITE_CHANNEL:
g_value_set_boxed (value, plug_in->priv->write_channel);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
app, libgimp, libgimpbase: plug-in and PDB protocol refactoring part two - Change the wire protocol's GPProcInstall to transmit the entire information needed for constructing all GParamSpecs we use, don't use GimpPDBArgType in GPProcInstall but an enum private to the wire protocol plus the GParamSpec's GType name. Bump the wire protocol version. - Add gimpgpparamspecs.[ch] in both app/plug-in/ and libgimp/ which take care of converting between GPParamDef and GParamSpec. They share code as far as possible. - Change pluginrc writing and parsing to re-use GPParamDef and the utility functions from gimpgpparamspecs. - Remove gimp_pdb_compat_param_spec() from app/pdb/gimp-pdb-compat.[ch], the entire core uses proper GParamSpecs from the wire protocol now, the whole file will follow down the drain once we use a GValue representation on the wire too. - In gimp_plug_in_handle_proc_install(), change the "run-mode" parameter to a GParamSpecEnum(GIMP_TYPE_RUN_MODE) (if it is not already an enum). and change all places in app/ to treat it as an enum value. - plug-ins: fix cml-explorer to register correctly, a typo in "run-mode" was never noticed until now. - Add gimpgpcompat.[ch] in libgimp to deal with all the transforms between old-style wire communication and using GParamSpec and GValue, it contains some functions that are subject to change or even removal in the next steps. - Change the libgimp GimpProcedure and GimpPlugIn in many ways to be able to actually install procedures the new way. - plug-ins: change goat-exercise to completely use the new GimpPlugIn and GimpProcedure API, look here to see how plug-ins will look in the future, of course subject to change until this is finished. - Next: changing GPParam to transmit all information about a GValue.
2019-07-27 22:37:55 +08:00
app, libgimp, libgimpbase: plug-in and PDB protocol refactoring part two - Change the wire protocol's GPProcInstall to transmit the entire information needed for constructing all GParamSpecs we use, don't use GimpPDBArgType in GPProcInstall but an enum private to the wire protocol plus the GParamSpec's GType name. Bump the wire protocol version. - Add gimpgpparamspecs.[ch] in both app/plug-in/ and libgimp/ which take care of converting between GPParamDef and GParamSpec. They share code as far as possible. - Change pluginrc writing and parsing to re-use GPParamDef and the utility functions from gimpgpparamspecs. - Remove gimp_pdb_compat_param_spec() from app/pdb/gimp-pdb-compat.[ch], the entire core uses proper GParamSpecs from the wire protocol now, the whole file will follow down the drain once we use a GValue representation on the wire too. - In gimp_plug_in_handle_proc_install(), change the "run-mode" parameter to a GParamSpecEnum(GIMP_TYPE_RUN_MODE) (if it is not already an enum). and change all places in app/ to treat it as an enum value. - plug-ins: fix cml-explorer to register correctly, a typo in "run-mode" was never noticed until now. - Add gimpgpcompat.[ch] in libgimp to deal with all the transforms between old-style wire communication and using GParamSpec and GValue, it contains some functions that are subject to change or even removal in the next steps. - Change the libgimp GimpProcedure and GimpPlugIn in many ways to be able to actually install procedures the new way. - plug-ins: change goat-exercise to completely use the new GimpPlugIn and GimpProcedure API, look here to see how plug-ins will look in the future, of course subject to change until this is finished. - Next: changing GPParam to transmit all information about a GValue.
2019-07-27 22:37:55 +08:00
/* public functions */
/**
* gimp_plug_in_set_translation_domain:
* @plug_in: A #GimpPlugIn.
* @domain_name: The name of the textdomain (must be unique).
* @domain_path: (nullable): A #GFile pointing to the compiled message catalog
* (may be %NULL).
*
* Sets a textdomain for localisation for the @plug_in.
*
* This function adds a textdomain to the list of domains Gimp
* searches for strings when translating its menu entries. There is no
* need to call this function for plug-ins that have their strings
* included in the 'gimp-std-plugins' domain as that is used by
* default. If the compiled message catalog is not in the standard
* location, you may specify an absolute path to another
* location. This function can only be called in the
* GimpPlugIn::query() function of a plug-in and it has to be called
* before any procedure is installed.
*
* Since: 3.0
**/
void
gimp_plug_in_set_translation_domain (GimpPlugIn *plug_in,
const gchar *domain_name,
GFile *domain_path)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
g_return_if_fail (domain_name != NULL);
g_return_if_fail (domain_path == NULL || G_IS_FILE (domain_path));
g_free (plug_in->priv->translation_domain_name);
plug_in->priv->translation_domain_name = g_strdup (domain_name);
g_set_object (&plug_in->priv->translation_domain_path, domain_path);
}
/**
* gimp_plug_in_set_help_domain:
* @plug_in: A #GimpPlugIn.
* @domain_name: The XML namespace of the plug-in's help pages.
* @domain_uri: The root URI of the plug-in's help pages.
*
* Set a help domain and path for the @plug_in.
*
* This function registers user documentation for the calling plug-in
* with the GIMP help system. The domain_uri parameter points to the
* root directory where the plug-in help is installed. For each
* supported language there should be a file called 'gimp-help.xml'
* that maps the help IDs to the actual help files.
*
* Since: 3.0
**/
void
gimp_plug_in_set_help_domain (GimpPlugIn *plug_in,
const gchar *domain_name,
GFile *domain_uri)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
g_return_if_fail (domain_name != NULL);
g_return_if_fail (G_IS_FILE (domain_uri));
g_free (plug_in->priv->help_domain_name);
plug_in->priv->help_domain_name = g_strdup (domain_name);
g_set_object (&plug_in->priv->help_domain_uri, domain_uri);
}
/**
* gimp_plug_in_add_menu_branch:
* @plug_in: A #GimpPlugIn
* @menu_path: The sub-menu's menu path.
* @menu_label: The menu label of the sub-menu.
*
* Add a new sub-menu to thr GIMP menus.
*
* This function installs a sub-menu which does not belong to any
* procedure at the location @menu_path.
*
* For translations of tooltips to work properly, @menu_label should
* only be marked for translation but passed to this function
* untranslated, for example using N_("Submenu"). GIMP will look up
* the translation in the textdomain registered for the plug-in.
*
* See also: gimp_procedure_add_menu_path().
*
* Since: 3.0
**/
void
gimp_plug_in_add_menu_branch (GimpPlugIn *plug_in,
const gchar *menu_path,
const gchar *menu_label)
{
GimpPlugInMenuBranch *branch;
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
g_return_if_fail (menu_path != NULL);
g_return_if_fail (menu_label != NULL);
branch = g_slice_new (GimpPlugInMenuBranch);
branch->menu_path = g_strdup (menu_path);
branch->menu_label = g_strdup (menu_label);
plug_in->priv->menu_branches = g_list_append (plug_in->priv->menu_branches,
branch);
}
/**
* gimp_plug_in_create_procedure:
* @plug_in: A #GimpPlugIn
* @procedure_name: A procedure name.
*
* This functiond creates a new procedure and is called when a plug-in
* instance is started by GIMP when one of the %GIMP_PLUGIN or
* %GIMP_EXTENSION procedures it implements is invoked.
*
* This function will only ever be called with names returned by
* implementations of GimpPlugInClass::init_procedures() or
* GimpPlugInClass::query_procedures().
*
* Returns: (transfer full): The newly created #GimpProcedure.
**/
app, libgimp, libgimpbase: plug-in and PDB protocol refactoring part two - Change the wire protocol's GPProcInstall to transmit the entire information needed for constructing all GParamSpecs we use, don't use GimpPDBArgType in GPProcInstall but an enum private to the wire protocol plus the GParamSpec's GType name. Bump the wire protocol version. - Add gimpgpparamspecs.[ch] in both app/plug-in/ and libgimp/ which take care of converting between GPParamDef and GParamSpec. They share code as far as possible. - Change pluginrc writing and parsing to re-use GPParamDef and the utility functions from gimpgpparamspecs. - Remove gimp_pdb_compat_param_spec() from app/pdb/gimp-pdb-compat.[ch], the entire core uses proper GParamSpecs from the wire protocol now, the whole file will follow down the drain once we use a GValue representation on the wire too. - In gimp_plug_in_handle_proc_install(), change the "run-mode" parameter to a GParamSpecEnum(GIMP_TYPE_RUN_MODE) (if it is not already an enum). and change all places in app/ to treat it as an enum value. - plug-ins: fix cml-explorer to register correctly, a typo in "run-mode" was never noticed until now. - Add gimpgpcompat.[ch] in libgimp to deal with all the transforms between old-style wire communication and using GParamSpec and GValue, it contains some functions that are subject to change or even removal in the next steps. - Change the libgimp GimpProcedure and GimpPlugIn in many ways to be able to actually install procedures the new way. - plug-ins: change goat-exercise to completely use the new GimpPlugIn and GimpProcedure API, look here to see how plug-ins will look in the future, of course subject to change until this is finished. - Next: changing GPParam to transmit all information about a GValue.
2019-07-27 22:37:55 +08:00
GimpProcedure *
gimp_plug_in_create_procedure (GimpPlugIn *plug_in,
const gchar *procedure_name)
app, libgimp, libgimpbase: plug-in and PDB protocol refactoring part two - Change the wire protocol's GPProcInstall to transmit the entire information needed for constructing all GParamSpecs we use, don't use GimpPDBArgType in GPProcInstall but an enum private to the wire protocol plus the GParamSpec's GType name. Bump the wire protocol version. - Add gimpgpparamspecs.[ch] in both app/plug-in/ and libgimp/ which take care of converting between GPParamDef and GParamSpec. They share code as far as possible. - Change pluginrc writing and parsing to re-use GPParamDef and the utility functions from gimpgpparamspecs. - Remove gimp_pdb_compat_param_spec() from app/pdb/gimp-pdb-compat.[ch], the entire core uses proper GParamSpecs from the wire protocol now, the whole file will follow down the drain once we use a GValue representation on the wire too. - In gimp_plug_in_handle_proc_install(), change the "run-mode" parameter to a GParamSpecEnum(GIMP_TYPE_RUN_MODE) (if it is not already an enum). and change all places in app/ to treat it as an enum value. - plug-ins: fix cml-explorer to register correctly, a typo in "run-mode" was never noticed until now. - Add gimpgpcompat.[ch] in libgimp to deal with all the transforms between old-style wire communication and using GParamSpec and GValue, it contains some functions that are subject to change or even removal in the next steps. - Change the libgimp GimpProcedure and GimpPlugIn in many ways to be able to actually install procedures the new way. - plug-ins: change goat-exercise to completely use the new GimpPlugIn and GimpProcedure API, look here to see how plug-ins will look in the future, of course subject to change until this is finished. - Next: changing GPParam to transmit all information about a GValue.
2019-07-27 22:37:55 +08:00
{
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
g_return_val_if_fail (gimp_is_canonical_identifier (procedure_name), NULL);
app, libgimp, libgimpbase: plug-in and PDB protocol refactoring part two - Change the wire protocol's GPProcInstall to transmit the entire information needed for constructing all GParamSpecs we use, don't use GimpPDBArgType in GPProcInstall but an enum private to the wire protocol plus the GParamSpec's GType name. Bump the wire protocol version. - Add gimpgpparamspecs.[ch] in both app/plug-in/ and libgimp/ which take care of converting between GPParamDef and GParamSpec. They share code as far as possible. - Change pluginrc writing and parsing to re-use GPParamDef and the utility functions from gimpgpparamspecs. - Remove gimp_pdb_compat_param_spec() from app/pdb/gimp-pdb-compat.[ch], the entire core uses proper GParamSpecs from the wire protocol now, the whole file will follow down the drain once we use a GValue representation on the wire too. - In gimp_plug_in_handle_proc_install(), change the "run-mode" parameter to a GParamSpecEnum(GIMP_TYPE_RUN_MODE) (if it is not already an enum). and change all places in app/ to treat it as an enum value. - plug-ins: fix cml-explorer to register correctly, a typo in "run-mode" was never noticed until now. - Add gimpgpcompat.[ch] in libgimp to deal with all the transforms between old-style wire communication and using GParamSpec and GValue, it contains some functions that are subject to change or even removal in the next steps. - Change the libgimp GimpProcedure and GimpPlugIn in many ways to be able to actually install procedures the new way. - plug-ins: change goat-exercise to completely use the new GimpPlugIn and GimpProcedure API, look here to see how plug-ins will look in the future, of course subject to change until this is finished. - Next: changing GPParam to transmit all information about a GValue.
2019-07-27 22:37:55 +08:00
if (GIMP_PLUG_IN_GET_CLASS (plug_in)->create_procedure)
return GIMP_PLUG_IN_GET_CLASS (plug_in)->create_procedure (plug_in,
procedure_name);
app, libgimp, libgimpbase: plug-in and PDB protocol refactoring part two - Change the wire protocol's GPProcInstall to transmit the entire information needed for constructing all GParamSpecs we use, don't use GimpPDBArgType in GPProcInstall but an enum private to the wire protocol plus the GParamSpec's GType name. Bump the wire protocol version. - Add gimpgpparamspecs.[ch] in both app/plug-in/ and libgimp/ which take care of converting between GPParamDef and GParamSpec. They share code as far as possible. - Change pluginrc writing and parsing to re-use GPParamDef and the utility functions from gimpgpparamspecs. - Remove gimp_pdb_compat_param_spec() from app/pdb/gimp-pdb-compat.[ch], the entire core uses proper GParamSpecs from the wire protocol now, the whole file will follow down the drain once we use a GValue representation on the wire too. - In gimp_plug_in_handle_proc_install(), change the "run-mode" parameter to a GParamSpecEnum(GIMP_TYPE_RUN_MODE) (if it is not already an enum). and change all places in app/ to treat it as an enum value. - plug-ins: fix cml-explorer to register correctly, a typo in "run-mode" was never noticed until now. - Add gimpgpcompat.[ch] in libgimp to deal with all the transforms between old-style wire communication and using GParamSpec and GValue, it contains some functions that are subject to change or even removal in the next steps. - Change the libgimp GimpProcedure and GimpPlugIn in many ways to be able to actually install procedures the new way. - plug-ins: change goat-exercise to completely use the new GimpPlugIn and GimpProcedure API, look here to see how plug-ins will look in the future, of course subject to change until this is finished. - Next: changing GPParam to transmit all information about a GValue.
2019-07-27 22:37:55 +08:00
return NULL;
}
/**
* gimp_plug_in_add_temp_procedure:
* @plug_in: A #GimpPlugIn
* @procedure: A #GimpProcedure of type %GIMP_TEMPORARY.
*
* This function adds a temporary procedure to @plug_in. It is usually
* called from a %GIMP_EXTENSION procedure's GimpProcedure::run().
*
* A temporary procedure is a procedure which is only available while
* one of your plug-in's "real" procedures is running.
*
* The procedure's type <emphasis>must</emphasis> be %GIMP_TEMPORARY
* or the function will fail.
*
* NOTE: Normally, plug-in communication is triggered by the plug-in
* and the GIMP core only responds to the plug-in's requests. You must
* explicitly enable receiving of temporary procedure run requests
* using either gimp_plug_in_extension_enable() or
* gimp_plug_in_extension_process(). See this functions' documentation
* for details.
*
* Since: 3.0
**/
void
gimp_plug_in_add_temp_procedure (GimpPlugIn *plug_in,
GimpProcedure *procedure)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
g_return_if_fail (GIMP_IS_PROCEDURE (procedure));
g_return_if_fail (gimp_procedure_get_proc_type (procedure) ==
GIMP_TEMPORARY);
plug_in->priv->temp_procedures =
g_list_prepend (plug_in->priv->temp_procedures,
g_object_ref (procedure));
GIMP_PROCEDURE_GET_CLASS (procedure)->install (procedure);
}
/**
2019-08-02 09:21:59 +08:00
* gimp_plug_in_remove_temp_procedure:
* @plug_in: A #GimpPlugIn
* @procedure_name: The name of a #GimpProcedure added to @plug_in.
*
* This function removes a temporary procedure from @plug_in by the
* procedure's @procedure_name.
*
* Since: 3.0
**/
void
gimp_plug_in_remove_temp_procedure (GimpPlugIn *plug_in,
const gchar *procedure_name)
{
GimpProcedure *procedure;
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
g_return_if_fail (gimp_is_canonical_identifier (procedure_name));
procedure = gimp_plug_in_get_temp_procedure (plug_in, procedure_name);
if (procedure)
{
GIMP_PROCEDURE_GET_CLASS (procedure)->uninstall (procedure);
plug_in->priv->temp_procedures =
g_list_remove (plug_in->priv->temp_procedures,
procedure);
g_object_unref (procedure);
}
}
/**
* gimp_plug_in_get_temp_procedures:
* @plug_in: A #GimpPlugIn
*
* This function retrieves the list of temporary procedure of @plug_in as
* added with gimp_plug_in_add_temp_procedure().
*
* Returns: (transfer none) (element-type GimpProcedure): The list of
* procedures.
*
* Since: 3.0
**/
GList *
gimp_plug_in_get_temp_procedures (GimpPlugIn *plug_in)
{
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
return plug_in->priv->temp_procedures;
}
/**
* gimp_plug_in_get_temp_procedure:
* @plug_in: A #GimpPlugIn
* @procedure_name: The name of a #GimpProcedure added to @plug_in.
*
* This function retrieves a temporary procedure from @plug_in by the
* procedure's @procedure_name.
*
* Returns: (nullable) (transfer none): The procedure if registered, or %NULL.
*
* Since: 3.0
**/
GimpProcedure *
gimp_plug_in_get_temp_procedure (GimpPlugIn *plug_in,
const gchar *procedure_name)
{
GList *list;
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
g_return_val_if_fail (gimp_is_canonical_identifier (procedure_name), NULL);
for (list = plug_in->priv->temp_procedures; list; list = g_list_next (list))
{
GimpProcedure *procedure = list->data;
if (! strcmp (procedure_name, gimp_procedure_get_name (procedure)))
return procedure;
}
return NULL;
}
/**
* gimp_plug_in_extension_enable:
* @plug_in: A #GimpPlugIn
*
* Enables asynchronous processing of messages from the main GIMP
* application.
*
* Normally, a plug-in is not called by GIMP except for the call to
* the procedure it implements. All subsequent communication is
* triggered by the plug-in and all messages sent from GIMP to the
* plug-in are just answers to requests the plug-in made.
*
* If the plug-in however registered temporary procedures using
* gimp_plug_in_add_temp_procedure(), it needs to be able to receive
* requests to execute them. Usually this will be done by running
* gimp_plug_in_extension_process() in an endless loop.
*
* If the plug-in cannot use gimp_plug_in_extension_process(), i.e. if
* it has a GUI and is hanging around in a #GMainLoop, it must call
* gimp_plug_in_extension_enable().
*
* Note that the plug-in does not need to be a #GIMP_EXTENSION to
* register temporary procedures.
*
* See also: gimp_plug_in_add_temp_procedure().
*
* Since: 3.0
**/
void
gimp_plug_in_extension_enable (GimpPlugIn *plug_in)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
if (! plug_in->priv->extension_source_id)
{
plug_in->priv->extension_source_id =
g_io_add_watch (plug_in->priv->read_channel, G_IO_IN | G_IO_PRI,
gimp_plug_in_extension_read,
plug_in);
}
}
/**
* gimp_plug_in_extension_process:
* @plug_in: A #GimpPlugIn.
* @timeout: The timeout (in ms) to use for the select() call.
*
* Processes one message sent by GIMP and returns.
*
* Call this function in an endless loop after calling
* gimp_procedure_extension_ready() to process requests for running
* temporary procedures.
*
* See gimp_plug_in_extension_enable() for an asynchronous way of
* doing the same if running an endless loop is not an option.
*
* See also: gimp_plug_in_add_temp_procedure().
*
* Since: 3.0
**/
void
gimp_plug_in_extension_process (GimpPlugIn *plug_in,
guint timeout)
{
#ifndef G_OS_WIN32
gint select_val;
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
do
{
fd_set readfds;
struct timeval tv;
struct timeval *tvp;
if (timeout)
{
tv.tv_sec = timeout / 1000;
tv.tv_usec = (timeout % 1000) * 1000;
tvp = &tv;
}
else
tvp = NULL;
FD_ZERO (&readfds);
FD_SET (g_io_channel_unix_get_fd (plug_in->priv->read_channel),
&readfds);
if ((select_val = select (FD_SETSIZE, &readfds, NULL, NULL, tvp)) > 0)
{
gimp_plug_in_single_message (plug_in);
}
else if (select_val == -1 && errno != EINTR)
{
perror ("gimp_plug_in_extension_process");
gimp_quit ();
}
}
while (select_val == -1 && errno == EINTR);
#else
/* Zero means infinite wait for us, but g_poll and
* g_io_channel_win32_poll use -1 to indicate
* infinite wait.
*/
GPollFD pollfd;
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
if (timeout == 0)
timeout = -1;
g_io_channel_win32_make_pollfd (plug_in->priv->read_channel, G_IO_IN,
&pollfd);
if (g_io_channel_win32_poll (&pollfd, 1, timeout) == 1)
{
gimp_plug_in_single_message (plug_in);
}
#endif
}
/**
* gimp_plug_in_set_pdb_error_handler:
* @plug_in: A #GimpPlugIn
* @handler: Who is responsible for handling procedure call errors.
*
* Sets an error handler for procedure calls.
*
* This procedure changes the way that errors in procedure calls are
* handled. By default GIMP will raise an error dialog if a procedure
* call made by a plug-in fails. Using this procedure the plug-in can
* change this behavior. If the error handler is set to
* %GIMP_PDB_ERROR_HANDLER_PLUGIN, then the plug-in is responsible for
* calling gimp_pdb_get_last_error() and handling the error whenever
* one if its procedure calls fails. It can do this by displaying the
* error message or by forwarding it in its own return values.
*
* Since: 3.0
**/
void
gimp_plug_in_set_pdb_error_handler (GimpPlugIn *plug_in,
GimpPDBErrorHandler handler)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
_gimp_plugin_set_pdb_error_handler (handler);
}
/**
* gimp_plug_in_get_pdb_error_handler:
* @plug_in: A #GimpPlugIn
*
* Retrieves the active error handler for procedure calls.
*
* This procedure retrieves the currently active error handler for
* procedure calls made by the calling plug-in. See
* gimp_plugin_set_pdb_error_handler() for details.
*
* Returns: Who is responsible for handling procedure call errors.
*
* Since: 3.0
**/
GimpPDBErrorHandler
gimp_plug_in_get_pdb_error_handler (GimpPlugIn *plug_in)
{
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in),
GIMP_PDB_ERROR_HANDLER_INTERNAL);
return _gimp_plugin_get_pdb_error_handler ();
}
/* internal functions */
void
_gimp_plug_in_query (GimpPlugIn *plug_in)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
if (GIMP_PLUG_IN_GET_CLASS (plug_in)->init_procedures)
gp_has_init_write (plug_in->priv->write_channel, plug_in);
if (GIMP_PLUG_IN_GET_CLASS (plug_in)->query_procedures)
{
GList *procedures =
GIMP_PLUG_IN_GET_CLASS (plug_in)->query_procedures (plug_in);
gimp_plug_in_register (plug_in, procedures);
}
}
void
_gimp_plug_in_init (GimpPlugIn *plug_in)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
if (GIMP_PLUG_IN_GET_CLASS (plug_in)->init_procedures)
{
GList *procedures =
GIMP_PLUG_IN_GET_CLASS (plug_in)->init_procedures (plug_in);
gimp_plug_in_register (plug_in, procedures);
}
}
void
_gimp_plug_in_run (GimpPlugIn *plug_in)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
g_io_add_watch (plug_in->priv->read_channel,
G_IO_ERR | G_IO_HUP,
gimp_plug_in_io_error_handler,
NULL);
gimp_plug_in_loop (plug_in);
}
void
_gimp_plug_in_quit (GimpPlugIn *plug_in)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
if (GIMP_PLUG_IN_GET_CLASS (plug_in)->quit)
GIMP_PLUG_IN_GET_CLASS (plug_in)->quit (plug_in);
_gimp_shm_close ();
gp_quit_write (plug_in->priv->write_channel, plug_in);
}
GIOChannel *
_gimp_plug_in_get_read_channel (GimpPlugIn *plug_in)
{
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
return plug_in->priv->read_channel;
}
GIOChannel *
_gimp_plug_in_get_write_channel (GimpPlugIn *plug_in)
{
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
return plug_in->priv->write_channel;
}
void
_gimp_plug_in_read_expect_msg (GimpPlugIn *plug_in,
GimpWireMessage *msg,
gint type)
{
g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
while (TRUE)
{
if (! gimp_wire_read_msg (plug_in->priv->read_channel, msg, NULL))
gimp_quit ();
if (msg->type == type)
return; /* up to the caller to call wire_destroy() */
if (msg->type == GP_TEMP_PROC_RUN || msg->type == GP_QUIT)
{
gimp_plug_in_process_message (plug_in, msg);
}
else
{
g_error ("unexpected message: %d", msg->type);
}
gimp_wire_destroy (msg);
}
}
/* private functions */
static void
gimp_plug_in_register (GimpPlugIn *plug_in,
GList *procedures)
{
GList *list;
for (list = procedures; list; list = g_list_next (list))
{
const gchar *name = list->data;
GimpProcedure *procedure;
procedure = gimp_plug_in_create_procedure (plug_in, name);
if (procedure)
{
GIMP_PROCEDURE_GET_CLASS (procedure)->install (procedure);
g_object_unref (procedure);
}
else
{
g_warning ("Plug-in failed to create procedure '%s'\n", name);
}
}
g_list_free_full (procedures, g_free);
if (plug_in->priv->translation_domain_name)
{
gchar *path = NULL;
if (plug_in->priv->translation_domain_path)
path = g_file_get_path (plug_in->priv->translation_domain_path);
_gimp_plugin_domain_register (plug_in->priv->translation_domain_name,
path);
g_free (path);
}
if (plug_in->priv->help_domain_name)
{
gchar *uri = g_file_get_uri (plug_in->priv->help_domain_uri);
_gimp_plugin_help_register (plug_in->priv->help_domain_name,
uri);
g_free (uri);
}
for (list = plug_in->priv->menu_branches; list; list = g_list_next (list))
{
GimpPlugInMenuBranch *branch = list->data;
_gimp_plugin_menu_branch_register (branch->menu_path,
branch->menu_label);
}
}
static gboolean
gimp_plug_in_write (GIOChannel *channel,
const guint8 *buf,
gulong count,
gpointer user_data)
{
GimpPlugIn *plug_in = user_data;
while (count > 0)
{
gulong bytes;
if ((plug_in->priv->write_buffer_index + count) >= WRITE_BUFFER_SIZE)
{
bytes = WRITE_BUFFER_SIZE - plug_in->priv->write_buffer_index;
memcpy (&plug_in->priv->write_buffer[plug_in->priv->write_buffer_index],
buf, bytes);
plug_in->priv->write_buffer_index += bytes;
if (! gimp_wire_flush (channel, plug_in))
return FALSE;
}
else
{
bytes = count;
memcpy (&plug_in->priv->write_buffer[plug_in->priv->write_buffer_index],
buf, bytes);
plug_in->priv->write_buffer_index += bytes;
}
buf += bytes;
count -= bytes;
}
return TRUE;
}
static gboolean
gimp_plug_in_flush (GIOChannel *channel,
gpointer user_data)
{
GimpPlugIn *plug_in = user_data;
if (plug_in->priv->write_buffer_index > 0)
{
gsize count = 0;
while (count != plug_in->priv->write_buffer_index)
{
GIOStatus status;
gsize bytes;
GError *error = NULL;
do
{
bytes = 0;
status = g_io_channel_write_chars (channel,
&plug_in->priv->write_buffer[count],
(plug_in->priv->write_buffer_index - count),
&bytes,
&error);
}
while (status == G_IO_STATUS_AGAIN);
if (status != G_IO_STATUS_NORMAL)
{
if (error)
{
g_warning ("%s: gimp_flush(): error: %s",
g_get_prgname (), error->message);
g_error_free (error);
}
else
{
g_warning ("%s: gimp_flush(): error", g_get_prgname ());
}
return FALSE;
}
count += bytes;
}
plug_in->priv->write_buffer_index = 0;
}
return TRUE;
}
static gboolean
gimp_plug_in_io_error_handler (GIOChannel *channel,
GIOCondition cond,
gpointer data)
{
g_printerr ("%s: fatal error: GIMP crashed\n", gimp_get_progname ());
gimp_quit ();
/* never reached */
return TRUE;
}
static void
gimp_plug_in_loop (GimpPlugIn *plug_in)
{
while (TRUE)
{
GimpWireMessage msg;
if (! gimp_wire_read_msg (plug_in->priv->read_channel, &msg, NULL))
return;
switch (msg.type)
{
case GP_QUIT:
gimp_wire_destroy (&msg);
return;
case GP_CONFIG:
_gimp_config (msg.data);
break;
case GP_TILE_REQ:
case GP_TILE_ACK:
case GP_TILE_DATA:
g_warning ("unexpected tile message received (should not happen)");
break;
case GP_PROC_RUN:
gimp_plug_in_proc_run (plug_in, msg.data);
gimp_wire_destroy (&msg);
return;
case GP_PROC_RETURN:
g_warning ("unexpected proc return message received (should not happen)");
break;
case GP_TEMP_PROC_RUN:
g_warning ("unexpected temp proc run message received (should not happen");
break;
case GP_TEMP_PROC_RETURN:
g_warning ("unexpected temp proc return message received (should not happen");
break;
case GP_PROC_INSTALL:
g_warning ("unexpected proc install message received (should not happen)");
break;
case GP_HAS_INIT:
g_warning ("unexpected has init message received (should not happen)");
break;
}
gimp_wire_destroy (&msg);
}
}
static void
gimp_plug_in_single_message (GimpPlugIn *plug_in)
{
GimpWireMessage msg;
/* Run a temp function */
if (! gimp_wire_read_msg (plug_in->priv->read_channel, &msg, NULL))
gimp_quit ();
gimp_plug_in_process_message (plug_in, &msg);
gimp_wire_destroy (&msg);
}
static void
gimp_plug_in_process_message (GimpPlugIn *plug_in,
GimpWireMessage *msg)
{
switch (msg->type)
{
case GP_QUIT:
gimp_quit ();
break;
case GP_CONFIG:
_gimp_config (msg->data);
break;
case GP_TILE_REQ:
case GP_TILE_ACK:
case GP_TILE_DATA:
g_warning ("unexpected tile message received (should not happen)");
break;
case GP_PROC_RUN:
g_warning ("unexpected proc run message received (should not happen)");
break;
case GP_PROC_RETURN:
g_warning ("unexpected proc return message received (should not happen)");
break;
case GP_TEMP_PROC_RUN:
gimp_plug_in_temp_proc_run (plug_in, msg->data);
break;
case GP_TEMP_PROC_RETURN:
g_warning ("unexpected temp proc return message received (should not happen)");
break;
case GP_PROC_INSTALL:
g_warning ("unexpected proc install message received (should not happen)");
break;
case GP_HAS_INIT:
g_warning ("unexpected has init message received (should not happen)");
break;
}
}
static void
gimp_plug_in_proc_run (GimpPlugIn *plug_in,
GPProcRun *proc_run)
{
GPProcReturn proc_return;
GimpProcedure *procedure;
procedure = gimp_plug_in_create_procedure (plug_in, proc_run->name);
if (procedure)
{
gimp_plug_in_proc_run_internal (plug_in,
proc_run, procedure,
&proc_return);
g_object_unref (procedure);
}
if (! gp_proc_return_write (plug_in->priv->write_channel,
&proc_return, plug_in))
gimp_quit ();
}
static void
gimp_plug_in_temp_proc_run (GimpPlugIn *plug_in,
GPProcRun *proc_run)
{
GPProcReturn proc_return;
GimpProcedure *procedure;
procedure = gimp_plug_in_get_temp_procedure (plug_in, proc_run->name);
if (procedure)
{
gimp_plug_in_proc_run_internal (plug_in,
proc_run, procedure,
&proc_return);
}
if (! gp_temp_proc_return_write (plug_in->priv->write_channel,
&proc_return, plug_in))
gimp_quit ();
}
static void
gimp_plug_in_proc_run_internal (GimpPlugIn *plug_in,
GPProcRun *proc_run,
GimpProcedure *procedure,
GPProcReturn *proc_return)
{
GimpValueArray *arguments;
GimpValueArray *return_values = NULL;
arguments = _gimp_gp_params_to_value_array (NULL,
NULL, 0,
proc_run->params,
proc_run->nparams,
FALSE, FALSE);
gimp_plug_in_push_procedure (plug_in, procedure);
return_values = gimp_procedure_run (procedure, arguments);
gimp_plug_in_pop_procedure (plug_in, procedure);
gimp_value_array_unref (arguments);
proc_return->name = proc_run->name;
proc_return->nparams = gimp_value_array_length (return_values);
proc_return->params = _gimp_value_array_to_gp_params (return_values, TRUE);
gimp_value_array_unref (return_values);
}
static gboolean
gimp_plug_in_extension_read (GIOChannel *channel,
GIOCondition condition,
gpointer data)
{
GimpPlugIn *plug_in = data;
gimp_plug_in_single_message (plug_in);
return G_SOURCE_CONTINUE;
}
/* procedure stack / display-, image-, item-cache */
GimpProcedure *
_gimp_plug_in_get_procedure (GimpPlugIn *plug_in)
{
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
g_return_val_if_fail (plug_in->priv->procedure_stack != NULL, NULL);
return plug_in->priv->procedure_stack->data;
}
static void
gimp_plug_in_push_procedure (GimpPlugIn *plug_in,
GimpProcedure *procedure)
{
plug_in->priv->procedure_stack =
g_list_prepend (plug_in->priv->procedure_stack, procedure);
}
static void
gimp_plug_in_pop_procedure (GimpPlugIn *plug_in,
GimpProcedure *procedure)
{
plug_in->priv->procedure_stack =
g_list_remove (plug_in->priv->procedure_stack, procedure);
_gimp_procedure_destroy_proxies (procedure);
gimp_plug_in_destroy_proxies (plug_in->priv->displays, FALSE);
gimp_plug_in_destroy_proxies (plug_in->priv->images, FALSE);
gimp_plug_in_destroy_proxies (plug_in->priv->items, FALSE);
if (! plug_in->priv->procedure_stack)
gimp_plug_in_destroy_hashes (plug_in);
}
GimpDisplay *
_gimp_plug_in_get_display (GimpPlugIn *plug_in,
gint32 display_id)
{
GimpDisplay *display = NULL;
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
if (G_UNLIKELY (! plug_in->priv->displays))
plug_in->priv->displays =
g_hash_table_new_full (g_direct_hash,
g_direct_equal,
NULL,
(GDestroyNotify) g_object_unref);
display = g_hash_table_lookup (plug_in->priv->displays,
GINT_TO_POINTER (display_id));
if (! display)
{
display = g_object_new (GIMP_TYPE_DISPLAY,
"id", display_id,
NULL);
g_hash_table_insert (plug_in->priv->displays,
GINT_TO_POINTER (display_id),
g_object_ref (display) /* add debug ref */);
}
return display;
}
GimpImage *
_gimp_plug_in_get_image (GimpPlugIn *plug_in,
gint32 image_id)
{
GimpImage *image = NULL;
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
if (G_UNLIKELY (! plug_in->priv->images))
plug_in->priv->images =
g_hash_table_new_full (g_direct_hash,
g_direct_equal,
NULL,
(GDestroyNotify) g_object_unref);
image = g_hash_table_lookup (plug_in->priv->images,
GINT_TO_POINTER (image_id));
if (! image)
{
image = g_object_new (GIMP_TYPE_IMAGE,
"id", image_id,
NULL);
g_hash_table_insert (plug_in->priv->images,
GINT_TO_POINTER (image_id),
g_object_ref (image) /* add debug ref */);
}
return image;
}
GimpItem *
_gimp_plug_in_get_item (GimpPlugIn *plug_in,
gint32 item_id)
{
GimpItem *item = NULL;
g_return_val_if_fail (GIMP_IS_PLUG_IN (plug_in), NULL);
if (G_UNLIKELY (! plug_in->priv->items))
plug_in->priv->items =
g_hash_table_new_full (g_direct_hash,
g_direct_equal,
NULL,
(GDestroyNotify) g_object_unref);
item = g_hash_table_lookup (plug_in->priv->items,
GINT_TO_POINTER (item_id));
if (! item)
{
if (gimp_item_id_is_layer (item_id))
{
item = g_object_new (GIMP_TYPE_LAYER,
"id", item_id,
NULL);
}
else if (gimp_item_id_is_layer_mask (item_id))
{
item = g_object_new (GIMP_TYPE_LAYER_MASK,
"id", item_id,
NULL);
}
else if (gimp_item_id_is_selection (item_id))
{
item = g_object_new (GIMP_TYPE_SELECTION,
"id", item_id,
NULL);
}
else if (gimp_item_id_is_channel (item_id))
{
item = g_object_new (GIMP_TYPE_CHANNEL,
"id", item_id,
NULL);
}
else if (gimp_item_id_is_vectors (item_id))
{
item = g_object_new (GIMP_TYPE_VECTORS,
"id", item_id,
NULL);
}
if (item)
g_hash_table_insert (plug_in->priv->items,
GINT_TO_POINTER (item_id),
g_object_ref (item) /* add debug ref */);
}
return item;
}
static void
gimp_plug_in_destroy_hashes (GimpPlugIn *plug_in)
{
g_clear_pointer (&plug_in->priv->displays, g_hash_table_unref);
g_clear_pointer (&plug_in->priv->images, g_hash_table_unref);
g_clear_pointer (&plug_in->priv->items, g_hash_table_unref);
}
static void
gimp_plug_in_destroy_proxies (GHashTable *hash_table,
gboolean finalize)
{
GHashTableIter iter;
gpointer key, value;
if (! hash_table)
return;
g_hash_table_iter_init (&iter, hash_table);
while (g_hash_table_iter_next (&iter, &key, &value))
{
GObject *object = value;
if (object->ref_count == 2)
{
/* this is the normal case for an unused proxy */
g_object_unref (object);
g_hash_table_iter_remove (&iter);
}
else if (object->ref_count == 1)
{
/* this is debug code, a plug-in MUST NOT unref a proxy, we
* only catch one unref here, more then one will crash right
* in this function
*/
gint id;
g_object_get (object, "id", &id, NULL);
g_printerr ("%s: ERROR: %s proxy with ID %d was unrefed "
"by plug-in, it MUST NOT do that!\n",
G_STRFUNC, G_OBJECT_TYPE_NAME (object), id);
g_hash_table_iter_remove (&iter);
}
else if (finalize)
{
/* this is debug code, a plug-in MUST NOT ref a proxy */
gint id;
g_object_get (object, "id", &id, NULL);
g_printerr ("%s: ERROR: %s proxy with ID %d was refed "
"by plug-in, it MUST NOT do that!\n",
G_STRFUNC, G_OBJECT_TYPE_NAME (object), id);
while (object->ref_count > 1)
g_object_unref (object);
g_hash_table_iter_remove (&iter);
}
}
}