C plug-ins - Complex GUI

[Theory] Introduction

Of course, graphical interfaces are not always completely static widgets. In particular, sometimes you may want some options to only appear or be made sensitive depending on the state of other arguments.

Our GimpProcedureDialog allows such use cases.

[Code] Adding a Boolean Argument

As an example, inside hello_world_create_procedure(), we may now add a boolean argument called “compute-size”:

gimp_procedure_add_boolean_argument (procedure, "compute-size", "Compute Ideal Size",
                                     "This option will compute a font size "
                                     "so that the text optimally fills the whole canvas",
                                     FALSE, G_PARAM_READWRITE);

You may notice that I set a longer string in the fourth parameter for this argument, unlike all the previous arguments. This is because this argument may be a bit less obvious than the previous ones. While the third argument will be used as the checkbox title (which we usually want short), some longer explanation would be useful. This is what this fourth argument is for. In particular, in our generated dialogs, it will be used as tooltip text when hovering the widget.

Now you remember when I left as an exercise to compute a nice default font size in the first tutorial? Let’s do this now! Then you can compare with your solution.

In the end of hello_world_run(), the actual processing code could become:

  gboolean compute_size;

  []

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

  gimp_image_undo_group_start (image);
  text_layer = gimp_text_layer_new (image, text, font, size, unit);
  gimp_image_insert_layer (image, GIMP_LAYER (text_layer), parent, position);
  if (compute_size)
    {
      gint image_width;
      gint layer_width;

      image_width = gimp_image_get_width (image);
      layer_width = gimp_drawable_get_width (GIMP_DRAWABLE (text_layer));

      size = (gint) ((gdouble) size * (image_width - 1) / layer_width);
      gimp_text_layer_set_font_size (text_layer, size, gimp_unit_pixel ());

      /* The font size heuristic is not perfect. Verify the text layer is
       * smaller than the image.
       */
      while (size > 1)
        {
          layer_width = gimp_drawable_get_width (GIMP_DRAWABLE (text_layer));

          if (layer_width < image_width)
            break;

          size--;
          gimp_text_layer_set_font_size (text_layer, size, gimp_unit_pixel ());
        }
    }
  gimp_image_undo_group_end (image);

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

  return gimp_procedure_new_return_values (procedure, GIMP_PDB_SUCCESS, NULL);

I am not going to comment much the code logic. What I said in the first tutorial is still true: we are not going to explain the full extents of possibilities given by libgimp.

I would still just attract your attention to the gimp_image_undo_group_start() and gimp_image_undo_group_end() calls. These group various individual actions into a single undo item. If we weren’t doing this, since we may have changed the font size several times, the plug-in would require several undo steps to be reverted, which is usually not what we want (we are usually expecting the plug-in actions to look like a single action as a “black box”).

[Code] Further Tweak our Dialog: Conditional Sensitivity

But let’s get back to the graphical interface! Now the main problem is that this new boolean argument is kinda countering the “Font Size” and “Font Size Unit”. When “Compute Ideal Size” is checked ✅, then whatever we set as font size and font unit will not matter. Unfortunately the interface does not make this fact very clear.

Now the GtkFrame created by gimp_procedure_dialog_fill_frame() is actually able to do some magic here. This function allows to set a boolean argument as title_id, which will determine the sensitivity of the frame contents.

Concretely, here is our new GUI code:

  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_frame (GIMP_PROCEDURE_DIALOG (dialog),
                                        "size-frame", "compute-size",
                                        TRUE, "size-box");
      gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog),
                                  "text", "font", "size-frame", 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);
    }

Note that we set invert_title to TRUE since we want the size arguments to be disabled when the box is checked, not the other way around. This is how now the 2 font size (and font size unit) widgets are inside a frame whose title is the “compute-size” boolean argument. And these 2 size widgets are conditionally made insensitive when the “compute-size” option is checked:

Size arguments are insensitive

Note that when you want to handle more complicated cases, you may also be interested by gimp_procedure_dialog_set_sensitive() or gimp_procedure_dialog_set_sensitive_if_in().

Custom GTK

The various methods of GimpProcedureDialog actually gives you pointers to the widgets created automatically, which is already a good start if you wish to customize your GTK widgets.

Sometimes though, you may want to create fully custom widgets. This may happen for very complex GUI code, or simply for some types of arguments not supported yet by GimpProcedureDialog. In such cases, you may insert your custom widgets into the various generated containers and connect to signals directly, like any custom GTK code.

Now it is also possible that you want to make your completely home-made dialog, which is not something we encourage. The reason is that GimpProcedureDialog is not only about generating dialog. Firstly it also provides a whole infrastructure for remembering and resetting arguments across sessions. You may notice the “Reset” button in any such dialog, with 2 options:

  • Reset to Initial Values: this will set back to the values stored in the config at dialog creation.
  • Reset to Factory Defaults: this will set the default values as set in the argument’s code.

There are also the 2 “Load Saved Settings” and “Save Settings” buttons which have quite explicit titles and allow people to save common settings they often use for a given plug-in.

Basically this dialog class creates expectation that all plug-ins will provide such features and you may not want to lose this.

The second big point to take care of is that you always want to end your plug-in with an updated config object whose all properties must be set to the actual values you used. The reason for this is that GIMP will store all values of a successful plug-in run. This will be not only used when running a plug-in “with last values”, but also this will be the default settings in the next interactive mode.

Furthermore, going forward, in our plans of implementing macros, the last set properties are the way for GIMP to rerun a plug-in exactly the way it was run. If you don’t leave a properly set config object, then your plug-in will not work well with our upcoming macro infrastructure.

In other words, say that you decide not to use GimpProcedureDialog and make completely custom GUI code (again, we don’t recommend this). Then you want to end your plug-in with:

  g_object_set (config,
                "text",         text,
                "font",         font,
                "compute-size", compute_size,
                "font-size",    size,
                "font-unit",    unit,
                NULL);

This way, you set all the properties to the used values and make the core processus aware of how your plug-in was called and how to reproduce the same result. Doing this is unneeded when using our GimpProcedureDialog infrastructure.

Conclusion

Now you know a bit more about creating complex graphical interfaces to your plug-ins. From now on, the rest is mostly up to you to create awesome code! 🤗