Adobe UXP: Things you need to know! #11 Flyout Menus and Entrypoints

In this episode Iā€™ll show you how to setup Flyout Menus for UXP plugins, while introducing UXP Entrypoints. Feel free to watch the video or read the article, they cover the same ground.

Flyouts

Flyouts are the kind of popup menus that appear when you click the top-right corner of UXP pluginā€™s panels. Theyā€™re traditionally used to launch info/about dialogs, or directly set Preferences in a remarkably unobtrusive way: the Flyout is easily reachable (stuff in there is always just one click away) and it helps you tremendously in keeping the precious real estate of the UI as uncluttered as possible.

Flyouts can store either single items, or group them inside submenus: which in turn can also be nested in a sort-of Russian doll fashion multiple times. In addition, all Flyout items can be dynamically set as enabled or disabled (grayed out), and checked or unchecked (with or without a flag besides their name).

A UXP panel is always (when you instruct it to do so, which weā€™ll see in a moment) listening for Flyout user interactions, i.e. clicks. Thereā€™s just one callback in charge of dealing with those clicks, and within that function weā€™ll write the logic to handle the appropriate eventā€”in other words, to recognize which menu itemā€™s been clicked and then act accordingly.

The subject of Flyout menus is also a way for me to introduce another crucial aspect of UXP plugins programming.

Entrypoints

You use entrypoints to bootstrap the plugin and set lifecycles hooks, i.e. functions that are expected to run when a particular event happens in the pluginā€™s life. They operate on the level of the plugin itself, or the panels and the commands that the plugin can be made of1.

In addition, itā€™s used to setup the Flyout menu and its callback. Although at the time of this writing (uxp-4.4.2-PR-6039.17 in Photoshop 2021 v.22.3 release) many of the hooks arenā€™t functional yet, let me show you how to use it.

First you need to require uxp and use the entrypoints.setup() method, that accepts an object.

// index.js
const { entrypoints } = require('uxp');
entrypoints.setup({
  plugin: { /* ... */},
  panels: { /* ... */},
  commands: { /* ... */ },
})

Youā€™re allowed to call setup() only once in your code, otherwise an error will be thrown. The plugin property contains lifecycle hooks on the plugin level, create and destroy. Neither of them work so letā€™s skate over this; I wonā€™t talk about commands either (see episode #04 if you need to catch up), so letā€™s focus on the panels entry.

Panelā€™s lifecycle hooks

As Iā€™ve already mentioned in this series, a single UXP plugin can contain more than one panel. Each panel can have its own panel level entrypoint: in the panels object you should identify them through their id, that youā€™ve set in the manifest.json:

// manifest.json
{
  "id": "com.davidebarranca.flyout",
  "name": "UXP Flyout example",
  "version": "1.0.0",
  "main": "index.html",
  "host": [ { "app": "PS", "minVersion": "22.0.0" } ],
  "manifestVersion": 4,
  "entrypoints": [
    {
      "type": "panel",
      "id": "vanilla",
      /* ... */
    },
  ],
  /* ... */
}

Here we have only one panel, with id equal to vanilla, so the code in the entrypoints becomes:

// index.js
const { entrypoints } = require('uxp');
entrypoints.setup({
  plugin: { create(){}, destroy(){} }, // NOT WORKING
  panels: {
    vanilla: {
      // Lifecycle hooks
      create(){},       // NOT WORKING
      hide(){},         // NOT WORKING
      destroy(){},      // NOT WORKING
      show(event){},    // callback when panel is made visible
      // Flyout menu
      menuItems: [],    // Flyout menu's structure
      invokeMenu(id){}, // callback for Flyout menu click events
    }
  }
})

As you see, the create, hide, and destroy hooks (theoretically for when the panel is created the first time, hidden or destroyed) donā€™t fire yet. show runs fine instead, but only once (so itā€™s kind of a create I suppose).

Panelā€™s Flyout menu

The actual Flyout menu stuff weā€™re interested about is in the menuItems array, and the invokeMenu callback. Letā€™s start with the former: the code to produce the Flyout from the first screenshot in this article is as follows.

// index.js
const { entrypoints } = require('uxp');
entrypoints.setup({
  panels: {
    vanilla: {
      menuItems: [
        // by default all items are enabled and unchecked
        {
          label: "Preferences", submenu:
          [
            {id: "bell", label: "šŸ””  Notifications"},
            {id: "dynamite", label: "šŸ§Ø  Self-destruct", enabled: false },
            {id: "spacer", label: "-" }, // SPACER
            {id: "enabler", label: "Enable  šŸ§Ø" },
          ]
        },
        {id: "toggle", label: "Toggle me!", checked: true },
        {id: "about", label: "About" },
        {id: "reload", label: "Reload this panel" },
      ]
    }
  }
})

Let me walk you through the code. The structure is basically JSON: everything in the menuItems array will turn into a Flyout menu item, in the example weā€™ve got eight of them. Items are objects with a label property (please note that emoji do work there šŸ‹), and an id where it makes sense. Iā€™ve omitted the id in the first object labelled "Preferences", because this acts as a container: in fact it has a submenu property, which holds another array of objects, each one provided with both id and label.

Non-submenu items have the optional enabled and checked properties. By default, i.e. if you donā€™t explicitly set them, theyā€™re always enabled and unchecked. Spacers are available too, you create them setting the label to a single minus -, then they get expanded to a full line and disabled by default.

Let me paste again the menu here to show what Iā€™m after (keep in mind itā€™s just a dummy menu):

  • We have a ā€œPreferencesā€ menu, that contains two items. One of which is disabled (too dangerous!): clicking the ā€œEnable šŸ§Øā€ entry will re-enable itā€”actually itā€™s slightly more complex but weā€™ll see that in a minute.
  • The ā€œToggle me!ā€ item will check and uncheck itself.
  • ā€œAboutā€ is going to fire a popup dialog (Iā€™ll use a very lame alert instead), but itā€™s there as a placeholder for any kind of scripting code you may want to run
  • ā€œReload this panelā€ is a handy utility that will do what it suggests.

As is, the menu is shown when accessed from the UI, but it lacks any interactivity. This is why we also need the invokeMenu callback.

// index.js
const { entrypoints } = require('uxp');
entrypoints.setup({
  panels: {
    vanilla: {
      menuItems: [ /* ... */ ],
      invokeMenu(id) {
        console.log("Clicked menu with ID", id);
        // Storing the menu items array
        const { menuItems } = entrypoints.getPanel("vanilla");
        switch (id) {
          case "enabler":
            menuItems.getItem("dynamite").enabled =
              !menuItems.getItem("dynamite").enabled;
            menuItems.getItem(id).label =
              menuItems.getItem(id).label == "Enable  šŸ§Ø" ?
                                             "Disable  šŸ§Ø" :
                                             "Enable  šŸ§Ø";
            break;
          case "toggle":
            menuItems.getItem(id).checked = !menuItems.getItem(id).checked
            break;
          case "reload":
            window.location.reload()
            break;
          case "about":
            showAbout()
            break;
        }
      },
    }
  }
})

One thing to notice is that we have one callback, that receives the itemā€™s id as the only parameter: so it makes sense to use a switch statement with multiple cases.

Let me start with the "toggle", which is simpler: it toggles its own checked property. In order to reference itself, it uses the getItem() method of the menuItems array, which in turn you retrieve with the entrypoints.getPanel() function, passing the panel id (the one in the manifest.json, here vanilla). So in the end it should be like:

entrypoints.getPanel("vanilla").menuItems.getItem("toggle");

In my code Iā€™ve stored the menuItems in a constant in advance, for convenience: Iā€™m going to need it multiple times. Also, thereā€™s no need to explicitly write "toggle", as we already are in the case where this is the id.

To recap, you getPanel() from the entrypoints via the panel id, then you access the menuItems array from the panel, then you getItem() from the menuItems via the itemā€™s id. Finally you access the property that you need (here checked) and assign the new value.

A slightly more complex example is the "enabler", that toggles the boolean for the "dynamite"ā€™s enabled property. In doing so, it also change itā€™s own label (it toggles between "Enable šŸ§Ø" and "Disable šŸ§Ø"). Iā€™m not sure whether itā€™s really crucial from the UX point of view, but it was a nice way to show itā€™s possible to change labels too. Same menuItems.getItem() dance than before.

The remaining two cases are the simplest ones: "reload" runs window.location.reload() to refresh the panelā€™s view, while "about" calls the external function showAbout(), that is nothing but a bare wrapper for showAlert().

const photoshop = require('photoshop')
const showAbout = () => {
  photoshop.core.showAlert(
    "Hello everyone šŸ§¢\n\n" +
    "This could also be a dialog...\n" +
    "See Episode #10"
  )
}

In the real world, this could be a fully fledged dialog (see episode #10), or any command tapping directly into the host application Scripting API.

Recap

Flyout menus are a quite convenient way to group commands and information, and itā€™s not really difficult to build them. Items are stored in a JSON structure: theyā€™ve properties such as label, enabled, checked, and can be nested in submenus. A single handler function deals with user interaction via id of the clicked items. Weā€™ve seen how to check/uncheck items, as well as enable/disable and change the labels too.

Flyouts are set up in the UXP pluginā€™s Entrypoints, a special object that is used to define lifecycle hooks both on the pluginā€™s and panelā€™s level (most of which arenā€™t functional yet, but they will in the future), commands (in conjunction with the manifest.json), and the flyout itself.

Thanks for following along! If you find this content useful, please consider supporting me: you can either purchase my latest UXP Course with ReactJS, or donate what you can like the following fine people didā€”itā€™ll be much appreciated! šŸ™šŸ»

Thanks to: John Stevenson ā­ļø, Adam Plouff, Dana Frenklach, Dmitry Egorov, Roberto Sabatini, Carlo Diamanti, Wending Dai, Pedro Marques, Anthony Kuyper, Gabriel Correia, Ben Wright, CtrlSoftware, Maiane Araujo, MihĆ”ly DĆ”vid Paseczki, Caspar Shelley.

Stay safe, get the vaccine shot if/when you can ā€“ bye!

The whole series so far


  1. If you need a reminder, a Command is a GUI-less script that belongs to the pluginā€™s ā€œPluginsā€ menu and is set via the Manifest. Look back to Episode #04 - Commands vs. Panels and the manifest.json.Ā