PluginAction sucks (and how to improve it)

PluginAction sucks (and how to improve it)

You’d assume that, as a plugin developer, I’ve used PluginAction a ton of times. However, they are missing a critical piece of functionality, rendering them nearly useless.

The key issue

It’s 2023 and you still can’t declare a default key bind for PluginAction.

The use cases for an action with no keyboard shortcut by default are extremely limited. That kind of action is saved for more niche operations which aren’t important enough to surface to the user directly, but might still be useful in, say, the command palette.

Key-bind-less actions are useless for any primary actions, though. Unfortunately, most actions in Roblox plugins tend to be primary, because Roblox plugins aren’t exactly massive software suites. No self-respecting developer will write a plugin where you have to spend five minutes manually assigning keyboard shortcuts for basic operations, and no self-respecting user will spend five minutes every time they install a new plugin. This is why plugin developers still use UserInputService for they keyboard bindings; it requires no setup, it ensures everyone has a consistently assigned key map to make communication easier (“press E to extrude” etc.), and it just works.

That is the bar that PluginAction must meet if it wants to be useful for more than two people. If it requires any setup, if it leads to inconsistencies between users, or if it fails sometimes, it’s not good enough and nobody will care.

Fixing a flawed suggestion

I’ve seen many suggestions along the lines of adding a KeyCode and/or ModifierKey to the constructor, as a ‘suggested default’ that’s applied so long as no other action is bound there.

Let’s check this API design against our criteria:

So this is no good either. It trades the setup stage for not knowing what’s bound on your keyboard, because all your plugin actions get spliced together silently into some abomination of random key binds that you aren’t told about.

Here’s my first fix: unless the user explicitly changes the key binding, the default is always adopted for the PluginAction, even if another action is bound there already. In addition, users should be allowed to specify overlapping key bindings in the ‘Customise Shortcuts’ dialogue without being prevented or having other key bindings reset. TL;DR - don’t consider whether key bindings collide ahead of time.

Of course, this immediately introduces ambiguity when a key binding is used more than once. If my plugin registers G for ‘grow grass’ and another registers G for ‘gradient tool’, which one should be activated when I press G?

Firstly, we must recognise that actions don’t always apply. For example, that ‘gradient tool’ might be part of a UI editing tool, and we only care about activating the gradient tool when it’s on screen and in focus. PluginAction currently doesn’t give you any way of communicating that.

That leads me to my second fix: plugins should be able to enable and disable PluginActions at any time, whenever they feel like their actions are relevant or not. This reduces the chance of ambiguity by reducing the number of active key bindings at any one time.

However, we can’t expect all plugins to behave, and moreover, we can’t expect only one plugin to be active at any one time. For example, there can be multiple widgets on screen, there can be plugins running in the background without visible UI, etc. The next step, then, is figuring out if one action should be prioritised over others competing for the same key binding.

I suggest sorting the competing actions into tiers. Here’s a suggestion for what these tiers could look like:

If actions exist in a tier, all lower tiers are ignored. The actions in the highest (non-empty) tier are left to compete among themselves.

Some examples:

This system should realistically resolve most conflicts and help plugins play nicely together the way the user would expect. It also prevents plugins from overriding Roblox Studio actions, which might stop users from accessing important functions easily such as saving and publishing.

However, as the last example demonstrates, this priority system won’t resolve all conflicts. Rather than arbitrarily trying to assign granular priorities per-action (which would be hard to learn), I instead propose that Studio gives up at this point. Instead of trying to resolve the error, a toast should be shown along the bottom of the screen, notifying the user of the conflict:

Toast saying 'G key binding failed; it is used by 5 actions (Foo, Bar, Baz, Frob, Garb)' with a link to 'Change keyboard shortcuts'.

This is better than silently failing and it’s better than choosing something arbitrary for the user. It makes sure they’re aware of the conflict and gives them control over how it is resolved.

Edit: an alternate solution

I mentioned this to my partner and they suggested the conflicting actions be presented in a menu instead of failing completely. Perhaps it could look like the Blender pie menu for best ergonomics:

Image of a pie menu in Blender.

This would be a nice Roblox-sanctioned way for users to construct their own menus! I actually quite like it.

Is it good enough?

Let’s check this new design against our criteria:

I think this would be good enough to ship.