Support item (PF_DRAWABLE etc) chooser widget for plugin dialogs; for PDB procedure arguments
This proposes development of the API for plugins in 2.99. This is a design document.
Background
This is about letting a user choose GimpItems (layers etc.) in plugin dialogs.
This is about PF_IMAGE, PF_DRAWABLE, PF_LAYER, PF_CHANNEL, PF_VECTORS in Gimp v2. Those let a plugin declare an auxiliary argument which would engender a combobox widget in the plugin's dialog.
It has been the plan that plugins (or language bindings such as PyGimp and ScriptFu) would not implement GUI and settings for plugins, but that those features would be migrated to libgimp. In the form of GimpProcedureDialog, GimpProcedureConfig, and GimpPropWidget, which are well started.
So this is about replacing a feature of v2. It migrates the feature from PyGimp and ScriptFu to libgimp, so that plugins written in any bound language can use the feature.
Note that plugins that are GimpImageProcedures or that are file load/save plugins receive the current i.e. active image and selected drawables, without needing to explicitly declare them as arguments. This is about the case where a plugin needs auxiliary arguments that are images or drawables.
This is a rare use case. It seems that few plugins require auxiliary drawables.
In other words, this discusses how to implement PropWidgetItem (Item is the superclass of Drawable and its subclasses) i.e. an "item chooser widget" in libgimp and app.
See also
This continues the discussion in #8543. Which is item 3 of #8495.
See also #9133, which discusses a similar replacement of a feature of v2.
#8543 has a rejected MR !724 replaces the PF_DRAWABLE feature using a simple int combo box widget, without a sophisticated new widget proposed here.
Brief summary
A plugin can declare an auxiliary argument of one of the Gimp types: GimpImage, GimpItem, GimpDrawable, GimpLayer, GimpChannel, or GimpVectors (these are all components of an image.) An auxiliary argument is other than the image and drawables arguments of a GimpImageProcedure. A plugin does not declare those arguments and those arguments pass the currently selected image and drawables, a choice the user has already made. An auxiliary argument asks the user to make another choice, for say a second operand.
Then the generated GUI for the plugin displays a widget for choosing the component. The widget shows the whole structure of open images, but only lets the user choose a component of the desired type.
In Gimp v2, the widget only showed a list of the names of the components of the desired type, the components from all open images catted together. In v3, the widget will show preview thumbnails of the components, and show relationships i.e. which components belong to which images.
Architecture
Use the same architecture used for Resource choosers for plugins. The widgets are remote in app. The owning widget is a button in libgimp, similar to FontSelectButton. The button shows a preview thumbnail of the selected component. When clicked, the button opens a chooser dialog that is remote, a GimpPdbDialog in app.
They communicate over the wire. A remote chooser calls back, returning an ID on each choice (and whether closing.) The plugin receives an object. whose class in libgimp is not quite the same as the class in app, but more or less a proxy class.
This architecture leaves the model in app, only communicating the ID of the chosen component. Libgimp does not have the model. An alternative design would query the model from libgimp and implement the widgets in libgimp, not remotely.
Look-and-feel of the widget on the app side
This specifies how a user interacts with the widget on the app side.
This is one of two major design decisions:
- look-and-feel
- architecture.
Look-and-feel is less important than the architecture since the look and feel can be changed later without changing the fundamental operation: returning a choice to the plugin.
However, these topcis are relevent to the API:
- initial selection,
- multi-selection
- nullable choice,
- cancelling
since they affect the API for plugins.
Brief summary
The widget has similar look and feel to the widget which pops up when you click the button: Filters>Map>BumpMap>AuxInput. What pops up is app/GimpPickablePopup.
(That seems to be the only use of the PickablePopup widget. That is, used for Gegl plugin dialogs but not for the Gimp app itself? )
The feel is different. PickablePopup has popup behavior while the proposed ItemChooser will have dialog behavior, with buttons. Similar to the way that ResourceSelect widget has dialog behavior.
It shows thumbnail images of images and their components. It displays a "forest of trees" structure of the set of open images. More or less trees, but in a notebook widget with tabs. (An alternate design might be more like a file browser, showing the entire forest in one scrollable pane, and having expander, arrow icons to collapse/expand.)
The widget lets a user browse the structure of images. The user can choose certain components of an image.
The owning widget is usually a button. Here we are discussing the widget that appears when you click the owning widget.
Note that the owning widget may allow a choice of "None" or "Unknown" (the AuxInput widget displays "?") which isn't equivalent since knowing something is null is more than not knowing anything at all. It is the responsibility of the plugin to ensure that a user makes a choice when the plugin requires one, for example by providing an initial selection (in the owning widget and in this widget) or by disabling the plugin's OK button when the selection is None or Unknown.
Similar GUI function in the Gimp app
In the Gimp app, a user makes similars selections in the image icon bar and in the layer and channel dockables. Also the the Gimp app passes selected components to a GimpImageProcedure when the user invokes a plugin. The plugin executes independently, and the user could select different images while the plugin is still operating.
A choice made in this widget has no effect on the selections in the Gimp app.
In other words, this widget shows a condensed view of images and their components. The widget is for one-time choices for the sake of the plugin. (Except that any choice is kept in the settings of the plugin and might be used again.)
The widget for now will not be used elsewhere in the Gimp app, but might be in the future.
Generic on type of item chosen
The same look and feel is used for all types of components (e.g. layer versus channel.) The look of the widget does not change according to type. It always displays the Channel and Vectors tabs even when the user can only choose a Layer and even when the tabs are empty. The widget only enables elements (for choosing) of the type the user can and should choose.
When the widget is for choosing Image type, the enabled elements are root elements. When the widget is for choosing Drawable, Layer, Channel, Vectors types, the enabled elements are leaf elements of the tree. (An alternate design might use a distinct look and feel widget for choosing Image.)
(Its not clear whether the widget will show layer groups, i.e. show many levels.)
Clicking, selecting, and choosing
Clicking is not always selecting. Selecting is different from choosing.
Selecting means an element becomes highlighted. In the widget, clicking may navigate without selecting.
Choosing is the user selecting (clicking an item) then clicking the Close ( sometimes called OK) button, OR the user just double-clicking.
The user can only select/highlight elements of the desired type. The user can only double click (select/choose in one action) elements of the desired type.
Selecting alone does not update the owning widget (a thumbnail button) only choosing does.
The widget sounds an audible alert (the so-called "system alert" meaning the click isn't allowed and has no effect) when:
-
the user attempts to select (highlight) an element that is not chooseable but is in the correct level of the tree. For example, clicking on a Channel when a Layer is desired. Clicking on an Image when a Channel is desired will navigate, without an alert. Clicking on a Channel when an Image is desired will sound an alert.
-
the user attempts to double-click (select and choose) an element that is not chooseable.
The widget does not sound an audible alert when the user Closes without a selection, i.e. Cancels. That is normal, and the widget does not callback the plugin with a new choice. See "Initial Selection" and "Cancel"
The title of the widget says which type of component the user can choose, e.g. "Channel Chooser".
This design makes the same common widget familiar to a user. Only the content of the widget changes, not the number of tabs in the widget. (This is a similar design issue as for file choosers: do you show files of the wrong suffix/format?)
Similarly, the user sees the same look and feel widget to choose auxiliary images, and then only an image is enabled (double-clickable) but the widget still shows the structure of the image, because it may help the user choose the image they want. (Images are not Items. Both have an integer ID.)
Navigation
A user can click on any element to navigate, but only enabled (chooseable) elements will highlight when clicked. When the user clicks up tree (say an image), the contents of the down tree tabs changes. The up tree element (the image) is not highlighted (selected), unless the purpose of the widget is to choose Image type, but the name of the image is fed back i.e. shown above the tabs. (That's how the existing widget works.)
On a click, any selection (a highlighted element) remains in force until the user clicks on a different chooseable element, even though the user might not be able to see the highlighted element. (Similar to some choosers where user can select an item then scroll past it.)
Choosing None
When the widget was given an initial selection (for example when the user changes their mind and reopens the widget) or when the user has selected something, the user can unselect (to None selection) only by clicking in an empty space of the scrollable tab for the desired type (where the current selection is shown.) An alternate design has a "None" element in the model/store that a user can select.
Clicking the "Cancel" button does not return a new choice of "None" and may leave the owning widget showing a choice of "Unknown."
It seems like unselecting might be obscure to most users. There doesn't seem to be a discussion of this in the Gnome user interface guidelines.
Close i.e. confirm
The widget will be like other plugin choosers, having a "Close" button as does GimpPdbDialog. The Close button closes the dialog and sends the choice back to the plugin. This is unlike the GimpPickablePopup widget, which is a pure popup and has no "Close" button but can be canceled by keystroke or by clicking outside it.
The "Close" button is always enabled. When the user has not selected anything, or unselected everything, the "Close" button is still enabled and sends back a "None" choice (ID zero). When the user doesn't want that, they can choose the "Cancel" button. The widget cannot be parameterized as "is not nullable", that is, guaranteed to not return None, it is the responsibility of a plugin to handle None.
In other GUI frameworks, the "Close" button might be labeled "OK". Here the action is to confirm i.e. propagate the choice to the owning widget of the plugin dialog.
The widget will also let a user double-click on an item to choose and close (like the widget for AuxInput.) Thus the user will have two methods to close i.e. confirm.
Cancel
The widget will have a Cancel button. When the user clicks it, the widget closes without sending a choice to the plugin.
Unlike the Resource choosers, selecting an item does NOT immediately send a choice to the owning widget in the plugin dialog (so it can update it's preview.) Only double-click or Close button does that.
This is unlike the GimpPickablePopup which has no buttons and is only double-clickable for "confirm", and Escape key or click outside the widget for "cancel".
This is unlike the chooser dialogs for Resource, which have no Cancel button. The Resource choosers and their owning widgets always show a choice, often from the context, (which also always has a choice since Gimp does not allow a user to delete all the resources.)
(Note that the callback will need an additional parameter, "is-canceled" in addition to the "is-closing" parameter of the callbacks.)
Initial selection
The owning widget may pass an initial selection or not, when opening the widget. Thus the widget may or may not show an initial selection.
Unlike other chooser widgets, the initial selection will NOT come from a context or from the image that is active. A plugin already knows the active image and selected layers. This widget will NOT by default initially select the same (or some subset, say the first.)
An initial selection might be passed to the plugin when the plugin is "Reshow(n)" or "Repeat(ed)", that is, from the settings or config of the plugin, usually but not always from the same session of the Gimp app.
Passing None as the initial selection causes the element "None" to be initially selected/highlighted.
Invalid initial selection
It is the responsibility of the plugin to ensure that a non-null initial selection is valid i.e. still exists. Remember that the choices in the settings are serialized as integer ID's, but the actual object may no longer exist.
Note that the same problem is in the owning widget: it cannot show the preview of an object that no longer exists. Preferably the owning widget will substitute "None" for the previous setting (and give a warning.)
When the plugin DOES pass an initial selection that does not exist, the widget will show no selection, quietly. Then, if closed/confirmed, the widget will return a None choice to the owning widget. The widget will NOT attempt to show a reasonable selection, say the first component that exists, because that is an arbitrary choice rather than a user choice.
Multi selection
The widget does not allow multi-selection (e.g. Ctl-click to select and then choose two items.) Few plugins want a set of items (and it was not supported in Gimp 2.)
Plugin sensitivity i.e. preflight
For a plugin taking a parameter that is chosen by this widget (say "an auxiliary image") the plugin's menu item will not be disabled ahead of time, when no suitable choice exists. This is unlike the mandatory Image and Drawables parameters of many plugins (of type GimpImageProcedure.)
In other words, there is no set_sensitive method of GimpPlugin that traffics in the parameters chosen by this widget.
In other words, the mechanism for enabling plugins by state of user interaction i.e. the gimp_plugin_set_sensitive() functions, will not be extended to guarantee that a user cannot launch a plugin when the plugin requires an auxiliary item yet no items exist for a user to choose. For example, an image might not have "other" channels, and libgimp will not prevent a plugin from showing this widget to choose a channel in that case. In other words, this is not as user-friendly as other parts of Gimp, and plugins themselves must allow for that, handling a none return and possibly giving an error message (instead of more friendly tooltips explaining why a plugin's menu item is disabled.)
Channels
The widget will only show "other" channels, not the RGB components. (Re same confusion with the Channels dockable.)
Text layers
The widget will not distinguish text layers. Formerly, there is no PF_TEXTLAYER in v2 PyGimp. A plugin that requires a user to choose an auxiliary text layer is responsible for ensuring that a user's choice of layer is indeed a text layer. This widget will let a user choose any layer.
Vectors
Formerly, there is PF_VECTORS in v2 PyGimp.
This widget will show a tab for Vectors and let a user only choose an element of that tab.
Special cases
One case is choosing type GimpDrawableType, i.e. either a Channel or a Layer. Corresponding to PF_DRAWABLE. The widget will enable (for choosing) both Channels and Layer leaf elements.
Pragmatics, the order of development
This is a rough idea of how development might proceed.
The development can proceed from the app side towards libgimp side. First, develop a widget on the app side that pops up when you call the PDB procedure gimp_items_popup. Second, develop the mechanisms on the libgimp side. Finally, the ultimate goal, is that a python plugin declaring an arg using a GParamSpec for type say GimpChannelType will show a button/chooser widget in its generated dialog.
Start by cloning the PDB procedures for font widgets: pdb/groups/font_select.pdb => item_select.pdb
Then you can start testing in ScriptFu console, in Scheme:
>(gimp-items-popup)
Then implement the GUI/widget on the app side. That entails cloning and modifying app/gimpPickablePopup and app/GimpPdbDialog.
Then implement on the libgimp side. That is relatively straightforward. Additions to GimpProcedureDialog, GimpPropWidget, etc. Implementing GimpItemSelectButton i.e. libgimp/gimpitemselectbutton.[c,h] The widgets on the libgimp side show a preview of the selected component. They have code to draw a preview of the component (similar to libgimp/GimpResourceSelect and GimpResourceSelectButton and its subclasses.)
Implementation pragmatics on the app side
The same generic class/widget can be used for choosers for all Item subclasses, parameterized by a passed type e.g. passing GimpChannelType would make the widget let the user choose only a Channel.
Disable leaf elements by type using TreeSelect.set_select_function.
The widget will be cloned from the existing widget, app/gimppickablepopup. You might first refactor to extract just the frame object from the existing class. But it might be less breakage if you just copy the existing code for the frame i.e. the interior of the widget (whose exterior is a GimpPopup, not a GimpPdbDialog.) Also, the GUI behaviors are not quite the same, it is not clear that the extracted frame would have the desired behavior.
Existing app/GimpPdbDialog may also be cloned rather than modified. Currently, it is specific to choosing data/resources. (Data/resources have string ID's while image components have integer ID's.) You could refactor to generalize, i.e. to extract common code, but again there might be less breakage by simply copying: GimpPdbDialog => GimpPdbItemDialog.