Custom Action

Context

You are developing an explorable domain model of your application and find yourself repeatedly evaluating the same code snippets to perform a certain action or navigate to another object.

Problem

How can you streamline execution of repeated actions?

Forces

-— Repeated tasks are annoying and time-consuming.

— Remembering how to perform common tasks increases cognitive overload.

— Storing code to perform common actions as methods or as snippets in class comments doesn't guarantee that the code will be easily found when you need it.

Solution

Add a custom action button to the moldable tool for the object involved in a repeated task, encapsulating the boilerplate code to perform it.

Steps

Be sure to pick an evocative button icon and tooltip text to make the intent of the button clear. Only add buttons for the most important actions to avoid cluttering the interface of a moldable tool.

Examples

As an example, consider the inspector view below of this notebook page in the GT Book, the live documentation system of GT. Common actions (buttons at the top right) are to navigate to the notebook database, to view the file in which the state of the notebook page is stored, or to export an HTML version of the page. Each of these actions can easily be packaged as an inspector button, so that the action can be performed with a single click, opening a new inspector view of the result. In the example we navigate to the database holding all related notebook pages for the given project.

Consequences

Custom actions appear as buttons in moldable tools only in the context of the objects to which they can be applied.

You need a Moldable Tool into which you can dynamically plug custom actions.

Known Uses

In the GT image of August, 2024, there are over 200 classes that define a total of over 300 actions, ranging from checking HTTP links within web pages to pushing commits to a remote repository.

Related patterns

A Custom View has a similar intent, but is more suitable when the goal is to quickly access information in an Inspector view that would otherwise be found only be one or more navigation steps.

GT implementation notes

Consider GT slideshows. These are classes containing methods that use the <gtSlide> pragma. Here is a list of all such classes in this image:

(#gtSlide gtPragmas contents collect: #methodClass) asSet
  

The way to play a slideshow in a new window, for example, the GtTour Object subclass: #GtTour uses: TGtSlideShow instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Demo-MoldableDevelopment-Slideshows' slideshow, is to evaluate a snippet like this:

GtPresenterSlideShow openInSpace: GtTour
  

Sometime this information is stored in a class comment, as is the case for GtTour Object subclass: #GtTour uses: TGtSlideShow instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Demo-MoldableDevelopment-Slideshows' , or in cases where the slideshows inherit from a common class, it can be encapsulated in a class-side method, such as PrioritySlideshow>>#show show "NB: this is a class-side method." GtPresenterSlideShow openInSpace: self , but in any case, extra steps have to be performed to play the slideshow.

A solution is to add a custom <gtAction> button which will be displayed when you inspect the class. Here we see custom gtAction buttons to Inspect and to Play the slideshow class GtPharo101 Object subclass: #GtPharo101 uses: TGtSlideShow instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Demo-Pharo101'

Since most slideshow classes typically do share a common slideshow hierarchy, these actions are implemented in a trait TGtSlideShow Trait named: #TGtSlideShow instanceVariableNames: '' package: 'GToolkit-Presenter-Models - Support' , which can then used by all slideshow classes.

The implementation is straightforward in this case GtPharo101>>#gtOpenSlideshowActionFor: gtOpenSlideshowActionFor: anAction <gtAction> <gtClassAction> (self methods select: [ :m | m hasPragmaNamed: #gtSlide ]) isEmpty ifTrue: [ ^ anAction noAction ]. ^ anAction button priority: 11; tooltip: 'Play slideshow in new window'; icon: BrGlamorousVectorIcons play; action: [ :aButton | GtPresenterSlideShow openInSpace: self ]

In addition to the <gtAction> pragmas, there is also a <gtClassAction> pragma, so the button will also appear in a Coder.

The first line of the method checks that the class actually has a <gtSlide> method, otherwise the button will be suppressed (noAction). Then a button is displayed with a priority (the order in which buttons appear), a tooltip to display, an dicon and the action to perform.

Another typical example is the Open page in a new world tab button at the top of this page. You can Secondary-click on the button to see its implementation in LePage>>#gtOpenInNewTabActionFor: gtOpenInNewTabActionFor: anAction <lePageAction> ^ anAction button id: LeOpenPageInNewWorldTabElementId; tooltip: 'Open page in a new world tab'; icon: BrGlamorousVectorIcons spawn; action: [ :aButton | GtWorldUtility showSpaceWithTitle: self title inPagerWith: [ self asLepiterPagePhlowTool asElementDo: [ :e | e ] ] asStencil from: aButton ] . It streamlines the task of opening a new window for the current Lepiter notebook page, for example, by copying the page title, pasting it into Spotter, and then opening the found page.