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 submenu
s. 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
- #01 – Rundown on the UXP announcement @ the Adobe MAX 2020
- #02 - Documentation
- #03 - UXP Developer Tool
- #04 - Commands vs. Panels and the manifest.json
- #05 - Sync vs. Async code in Photoshop DOM Scripting
- #06 - BatchPlay (part 1): the ActionManager roots
- #07 - BatchPlay (part 2): Alchemist as a UXP Script Listener
- #08 - BatchPlay (part 3): Alchemist as a UXP Inspector
- #09 - Adobe Spectrum UXP
- #10 - Modal Dialogs
- #11 - Flyout Menus and Entrypoints
- #12 - React JS and the UXP plugins Course
- #13 - Manifest v5
- #special - The UXP Landscape Guide
-
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. ↩