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.Ā ↩