C plug-ins - GUI

[Theory] Introduction

This tutorial is a continuation of the previous basic C plug-ins tutorial. When we look at our existing demo plug-in, we notice that there are definitely a few part of the procedure which could be parameterized, i.e. for which it seems like we could let people choose. In particular:

  • The text to display;
  • The font to use;
  • The font size.

Our next update will add “arguments” to our demo plug-in plug-in-zemarmot-c-demo-hello-world.

🛈 Important Note: even if you are not interested into making a graphical interface to your plug-in, you should still consider the various “arguments” you want your plug-in to have. This is useful for other plug-ins calling yours, as well as for storing last run values.

[Code] Adding Procedure Arguments

Update the create_procedure() method like this:

static GimpProcedure *
hello_world_create_procedure (GimpPlugIn  *plug_in,
                              const gchar *name)
{
  GimpProcedure *procedure = NULL;

  if (g_strcmp0 (name, PLUG_IN_PROC) == 0)
    {
      procedure = gimp_image_procedure_new (plug_in, name,
                                            GIMP_PDB_PROC_TYPE_PLUGIN,
                                            hello_world_run, NULL, NULL);

      gimp_procedure_set_sensitivity_mask (procedure, GIMP_PROCEDURE_SENSITIVE_ALWAYS);

      gimp_procedure_set_menu_label (procedure, "_C Hello World");
      gimp_procedure_add_menu_path (procedure, "<Image>/Hell_o Worlds/");

      gimp_procedure_set_documentation (procedure,
                                        "Official Hello World Tutorial in C",
                                        "Some longer text to explain about this procedure. "
                                        "This is mostly for other developers calling this procedure.",
                                        NULL);
      gimp_procedure_set_attribution (procedure, "Jehan",
                                      "Jehan, ZeMarmot project",
                                      "2025");

      gimp_procedure_add_font_argument   (procedure, "font", "Font", NULL,
                                          FALSE, NULL, TRUE,
                                          G_PARAM_READWRITE);
      gimp_procedure_add_int_argument    (procedure, "font-size", "Font Size", NULL,
                                          1, 1000, 20,
                                          G_PARAM_READWRITE);
      gimp_procedure_add_unit_argument   (procedure, "font-unit", "Font Unit", NULL,
                                          TRUE, FALSE, gimp_unit_pixel (),
                                          G_PARAM_READWRITE);
      gimp_procedure_add_string_argument (procedure, "text", "Text", NULL,
                                          "Hello World!",
                                          G_PARAM_READWRITE);

    }

  return procedure;
}

We are adding a font argument which will defaults to whatever is the current contextual font, an integer argument defaulting to 20, a unit argument defaulting to pixel unit, and a string argument with default value “Hello World!”.

I am not going to dive deeper in these functions and will let you read the reference documentation instead. In particular, you may be interested into the full list of gimp_procedure_add_*_argument() functions.

[Code] Using Procedure Arguments

Accessing the arguments from the run() function is pretty simple. You may have noticed a GimpProcedureConfig parameter earlier. This object is in reality a custom subclass of GimpProcedureConfig which is created on the fly and has all your procedure arguments as GObject properties.

For anyone used to GObject code, you know you would typically get an object property with g_object_get() in C code:

static GimpValueArray *
hello_world_run (GimpProcedure        *procedure,
                 GimpRunMode           run_mode,
                 GimpImage            *image,
                 GimpDrawable        **drawables,
                 GimpProcedureConfig  *config,
                 gpointer              run_data)
{
  GimpTextLayer *text_layer;
  GimpLayer     *parent   = NULL;
  gint           position = 0;
  gint           n_drawables;

  gchar         *text;
  GimpFont      *font;
  gint           size;
  GimpUnit      *unit;

  n_drawables = gimp_core_object_array_get_length ((GObject **) drawables);

  if (n_drawables > 1)
    {
      GError *error = NULL;

      g_set_error (&error, GIMP_PLUG_IN_ERROR, 0,
                   "Procedure '%s' works with zero or one layer.",
                   PLUG_IN_PROC);

      return gimp_procedure_new_return_values (procedure,
                                               GIMP_PDB_CALLING_ERROR,
                                               error);
    }
  else if (n_drawables == 1)
    {
      GimpDrawable *drawable = drawables[0];

      if (! GIMP_IS_LAYER (drawable))
        {
          GError *error = NULL;

          g_set_error (&error, GIMP_PLUG_IN_ERROR, 0,
                       "Procedure '%s' works with layers only.",
                       PLUG_IN_PROC);

          return gimp_procedure_new_return_values (procedure,
                                                   GIMP_PDB_CALLING_ERROR,
                                                   error);
        }

      parent   = GIMP_LAYER (gimp_item_get_parent (GIMP_ITEM (drawable)));
      position = gimp_image_get_item_position (image, GIMP_ITEM (drawable));
    }

  g_object_get (config,
                "text",      &text,
                "font",      &font,
                "font-size", &size,
                "font-unit", &unit,
                NULL);

  text_layer = gimp_text_layer_new (image, text, font, size, unit);
  gimp_image_insert_layer (image, GIMP_LAYER (text_layer), parent, position);

  g_clear_object (&font);
  g_clear_object (&unit);
  g_free (text);

  return gimp_procedure_new_return_values (procedure, GIMP_PDB_SUCCESS, NULL);
}

Now so far, we still don’t have any graphical interface so it may not seem that useful. Yet we already made a big step forward! While the plug-in cannot be tweaked in interactive mode (e.g. when called from the GUI), it can already be called by other plug-ins with customized arguments. For instance, another plug-in could call your procedure with a custom text, font or font size.

Now of course, this still doesn’t sound that useful because the plug-in is so basic. But imagine that you had a very incredible plug-in doing complicated computations or some awesome process (if you read this tutorial, I am guessing you already have some ideas in mind!). Any other plug-in could depend on yours so that they don’t have to reimplement everything and focus on their own awesome part.

[Code] Creating your First Dialog

To add our dialog, first include the libgimpui library at the top of your code:

#include <libgimp/gimpui.h>

Now update the run() function this way:

#define PLUG_IN_BINARY "c-hello-world"

static GimpValueArray *
hello_world_run (GimpProcedure        *procedure,
                 GimpRunMode           run_mode,
                 GimpImage            *image,
                 GimpDrawable        **drawables,
                 GimpProcedureConfig  *config,
                 gpointer              run_data)
{
  GimpTextLayer *text_layer;
  GimpLayer     *parent   = NULL;
  gint           position = 0;
  gint           n_drawables;

  gchar         *text;
  GimpFont      *font;
  gint           size;
  GimpUnit      *unit;

  n_drawables = gimp_core_object_array_get_length ((GObject **) drawables);

  if (n_drawables > 1)
    {
      GError *error = NULL;

      g_set_error (&error, GIMP_PLUG_IN_ERROR, 0,
                   "Procedure '%s' works with zero or one layer.",
                   PLUG_IN_PROC);

      return gimp_procedure_new_return_values (procedure,
                                               GIMP_PDB_CALLING_ERROR,
                                               error);
    }
  else if (n_drawables == 1)
    {
      GimpDrawable *drawable = drawables[0];

      if (! GIMP_IS_LAYER (drawable))
        {
          GError *error = NULL;

          g_set_error (&error, GIMP_PLUG_IN_ERROR, 0,
                       "Procedure '%s' works with layers only.",
                       PLUG_IN_PROC);

          return gimp_procedure_new_return_values (procedure,
                                                   GIMP_PDB_CALLING_ERROR,
                                                   error);
        }

      parent   = GIMP_LAYER (gimp_item_get_parent (GIMP_ITEM (drawable)));
      position = gimp_image_get_item_position (image, GIMP_ITEM (drawable));
    }

  if (run_mode == GIMP_RUN_INTERACTIVE)
    {
      GtkWidget *dialog;

      gimp_ui_init (PLUG_IN_BINARY);
      dialog = gimp_procedure_dialog_new (procedure,
                                          GIMP_PROCEDURE_CONFIG (config),
                                          "Hello World");
      gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), NULL);

      if (! gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog)))
        return gimp_procedure_new_return_values (procedure, GIMP_PDB_CANCEL, NULL);
    }

  g_object_get (config,
                "text",      &text,
                "font",      &font,
                "font-size", &size,
                "font-unit", &unit,
                NULL);

  text_layer = gimp_text_layer_new (image, text, font, size, unit);
  gimp_image_insert_layer (image, GIMP_LAYER (text_layer), parent, position);

  g_clear_object (&font);
  g_clear_object (&unit);
  g_free (text);

  return gimp_procedure_new_return_values (procedure, GIMP_PDB_SUCCESS, NULL);
}

Finally you will now compile your plug-in with libgimpui too:

gcc main.c `pkg-config --cflags --libs gimp-3.0 gimpui-3.0` -o c-hello-world

Running the plug-in, you now get greeted by this nice dialog:

Your first dialog for a C plug-in

[Theory] Studying the GUI Code

Run Mode

You may notice how very small is this code addition, even though it actually creates a fully functional dialog which allows you to actually edit all the procedure arguments we created!

The first test we do is checking the run_mode. There are currently 3 possible values for the GimpRunMode parameter which is given to the run() function:

  • The run may be interactive: this mostly means that if you procedure can be customized, then a graphical interface is expected. This is the run mode set when any procedure is started from menus.
  • The run mode may be non-interactive: this is when no questions should be asked and the values in the config object should be used. This is usually when a plug-in calls a procedure from another plug-in.
  • The run mode may be called with latest values: this happens for instance when one calls the filter from the Filters > Repeat menu item.

Note that in the non-interactive and last-vals cases, you should just use the config object as-is. In the former case, it is assumed that whatever called your procedure also sets the config properties as they want them; in the latter case, the whole libgimp infrastructure will have taken care to retrieve the last values (defaulting to default values if this plug-in is run for the first time).

GUI Initialization

The first thing you should do before running any function from libgimpui is calling gimp_ui_init(). As implied by the function name it will initialize various parts of the system, including GTK, GDK and more. It also sets up the same theme as core is using so that your plug-in dialog doesn’t stick out like a sore thumb.

The argument is the “program name”, which should be ideally the name of your executable (here c-hello-world).

Procedure Dialog

The GimpProcedureDialog is where the big UI-generation magic happens.

After creating your dialog, all you have to do is filling it with all the procedure arguments in the order you declared them. And that’s about it!

Now run your dialog. If it returns FALSE, it means that the user canceled the interactive dialog. This is not an error, just a normal exit where you are asked to do nothing (the user changed their mind). In such case, we return with GIMP_PDB_CANCEL and no GError.

On the other hand, if the dialog run returned TRUE, you go on with the processing, and this is where the second part of the magic happens: the config object has been automatically updated! It now contains the argument values per interactive choices in the just-run dialog.

[Theory] Tweaking the GUI

Maybe the dialog is not exactly what you want. In particular the default GUI generation will just create widgets for each argument, one under another.

The first thing to know is that gimp_procedure_dialog_fill() actually takes a NULL-terminated list of argument names). Your procedure has 4 arguments: “font”, “font-size”, “font-unit” and “text”. Setting an empty list is equivalent to set all this list in the same order. It also means you can reorder the widgets in the interface (e.g. if you want to show the “text” entry first).

It is also possible to add a bit of further widget organization. For instance here, you know that the font size unit argument is here to qualify the font size argument. Maybe you want these horizontally, one after the other?

The GimpProcedureDialog provides a few utility functions for this. And in particular several functions whose name is prefixed with gimp_procedure_dialog_fill_ will create GTK containers which can be filled by some arguments.

For instance gimp_procedure_dialog_fill_box() will create a GtkBox which you can further customize. The container_id parameter will be a new name which you can mix with the real arguments.

[Code] Customized GUI

Let’s show a proper example. We will now reorder the arguments by putting the “text” argument first, then reorganize the dialog by putting the unit next to the font size in a horizontal box.

We will only edit the piece of code within the if (run_mode == GIMP_RUN_INTERACTIVE) test block:

  if (run_mode == GIMP_RUN_INTERACTIVE)
    {
      GtkWidget *dialog;
      GtkWidget *box;

      gimp_ui_init (PLUG_IN_BINARY);
      dialog = gimp_procedure_dialog_new (procedure,
                                          GIMP_PROCEDURE_CONFIG (config),
                                          "Hello World");
      box = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog),
                                            "size-box", "font-size", "font-unit", NULL);
      gtk_orientable_set_orientation (GTK_ORIENTABLE (box), GTK_ORIENTATION_HORIZONTAL);
      gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog),
                                  "text", "font", "size-box", NULL);

      if (! gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog)))
        {
          gtk_widget_destroy (dialog);
          return gimp_procedure_new_return_values (procedure, GIMP_PDB_CANCEL, NULL);
        }
      gtk_widget_destroy (dialog);
    }

And here is what it looks like now that we reorganized the widgets:

Same plug-in dialog after reorganizing widgets

Conclusion

You now have seen how ligbimpui helps you to create dialogs for your plug-in in barely a few lines. The widgets will be generated according to your argument settings. For instance, the minimum and maximum values set to your integer arguments are reflected in the widget created by the dialog.

Or if you add a font argument, you also tell the system if you want a specific default font, or if you prefer to just default to whatever is the currently selected font (which will especially matter with the “Reset to Factory Defaults” feature).

If the next tutorial, we will even further complexify the graphical user interface while leaving the code very simple.