-
Notifications
You must be signed in to change notification settings - Fork 68
Menu API
A message other scripts can send to open a uosc menu serialized as JSON. You can optionally pass a submenu_id
to pre-open a submenu. The ID is the submenu title chain leading to the submenu concatenated with >
, for example Tools > Aspect ratio
.
Menu data structure (pseudo types):
MenuBase {
title?: string;
items: Child[];
selected_index?: integer;
keep_open?: boolean;
footnote?: string; // Short message below the menu.
id?: string; // Default IDs look like `{root} > Submenu title`. You can overwrite it with this.
on_search?: 'callback' | string | string[]; // If command, query & menu_id added as last params.
on_paste?: 'callback' | string | string[]; // If command, value & menu_id added as last params.
on_move?: 'callback' | string | string[]; // If command, from_index, to_index, and menu_id added as last params.
search_style?: 'on_demand' | 'palette' | 'disabled'; // default: on_demand
search_debounce?: 'submit' | number; // default: 0
search_suggestion?: string;
search_submenus?: boolean;
item_actions?: Action[];
item_actions_place?: 'inside'|'outside'; // Preferred buttons place. Default: 'inside'
}
Menu extends MenuBase {
type?: string;
on_close?: 'callback' | string | string[];
callback?: string[];
}
Submenu extends MenuBase {
hint?: string;
bold?: boolean;
italic?: boolean;
align?: 'left'|'center'|'right';
muted?: boolean;
separator?: boolean;
}
Child = Item | Submenu;
Item {
title?: string;
hint?: string;
icon?: string;
value: string | string[];
active?: integer;
selectable?: boolean;
bold?: boolean;
italic?: boolean;
align?: 'left'|'center'|'right';
muted?: boolean;
separator?: boolean;
keep_open?: boolean;
actions?: Action[];
actions_place?: 'inside'|'outside'; // Preferred buttons place. Default: 'inside'
}
Action {
name: string;
icon: string;
label?: string;
}
It's not necessary to define selected_index
as it'll default to the first active
item, or 1st item in the list.
When Item.value
is a string, it'll be passed to mp.command(value)
. If it's a table (array) of strings, it'll be used as mp.commandv(table.unpack(value))
. The same goes for on_close
and on_search
. on_search
additionally appends the current search string as the last parameter.
on_close
is only sent if uosc/user is closing the menu. It is NOT send after you called close-menu
, as that might lead to circular loops.
Menu.type
is used to refer to this menu in update-menu
and close-menu
.
While the menu is open this value will be available in user-data/uosc/menu/type
and the shared-script-properties
entry uosc-menu-type
. If no type was provided, those will be set to 'undefined'
.
search_style
can be:
-
on_demand
(default) - Search input pops up when user starts typing, or presses/
orctrl+f
, depending on user configuration. It disappears onshift+backspace
, or when input text is cleared. -
palette
- Search input is always visible and can't be disabled. In this mode, menutitle
is used as input placeholder when no text has been entered yet. -
disabled
- Menu can't be searched.
search_debounce
controls how soon the search happens after the last character was entered in milliseconds. Entering new character resets the timer. Defaults to 300
. It can also have a special value 'submit'
, which triggers a search only after ctrl+enter
was pressed.
search_submenus
makes uosc's internal search handler (when no on_search
callback is defined) look into submenus as well, effectively flattening the menu for the duration of the search. This property is inherited by all submenus.
search_suggestion
fills menu search with initial query string. Useful for example when you want to implement something like subtitle downloader, you'd set it to current file name.
item.icon
property accepts icon names. You can pick one from here: Google Material Icons
There is also a special icon name spinner
which will display a rotating spinner. Along with a no-op command on an item and keep_open=true
, this can be used to display placeholder menus/items that are still loading.
on_paste
is triggered when user pastes a string while menu is opened. Works the same as on_search
.
When keep_open
is true
, activating the item will not close the menu. This property can be defined on both menus and items, and is inherited from parent to child if child doesn't overwrite it.
MenuBase.item_actions
& Item.actions
adds buttons to items in the menu. You can use Item.actions
to add different buttons to each individual item, or MenuBase.item_actions
to add same actions to all items of that menu. The information about what action was pressed is only available via Callback mode documented below. If user presses the button instead of the item, its name will be available on callback's event.action
property, otherwise it's nil
.
MenuBase.item_actions_place
& Item.actions_place
control whether the preferred place for action buttons is inside
or outside
the menu. Default is inside
. You should use outside
when your menu has hints/icons that have a high informational value and shouldn't be covered by buttons unless necessary. Note that uosc will still place buttons inside the menu if there's not enough space for them outside.
callback
enables a more advanced API interfacing. See Callback mode below for more.
Example:
local utils = require('mp.utils')
local menu = {
type = 'menu_type',
title = 'Custom menu',
items = {
{title = 'Foo', hint = 'foo', value = 'quit'},
{title = 'Bar', hint = 'bar', value = 'quit', active = true},
}
}
mp.commandv('script-message-to', 'uosc', 'open-menu', utils.format_json(menu))
Updates currently opened menu with the same type
.
The difference between this and open-menu
is that if the same type menu is already open, open-menu
will reset the menu as if it was newly opened, while update-menu
will update it's data.
update-menu
, along with {menu/item}.keep_open
property and item command that sends a message back can be used to create a self updating menu with some limited UI. Example:
local utils = require('mp.utils')
local script_name = mp.get_script_name()
local state = {
checkbox = 'no',
radio = 'bar'
}
function command(str)
return string.format('script-message-to %s %s', script_name, str)
end
function create_menu_data()
return {
type = 'test_menu',
title = 'Test menu',
keep_open = true,
items = {
{
title = 'Checkbox',
icon = state.checkbox == 'yes' and 'check_box' or 'check_box_outline_blank',
value = command('set-state checkbox ' .. (state.checkbox == 'yes' and 'no' or 'yes'))
},
{
title = 'Radio',
hint = state.radio,
items = {
{
title = 'Foo',
icon = state.radio == 'foo' and 'radio_button_checked' or 'radio_button_unchecked',
value = command('set-state radio foo')
},
{
title = 'Bar',
icon = state.radio == 'bar' and 'radio_button_checked' or 'radio_button_unchecked',
value = command('set-state radio bar')
},
{
title = 'Baz',
icon = state.radio == 'baz' and 'radio_button_checked' or 'radio_button_unchecked',
value = command('set-state radio baz')
},
},
},
{
title = 'Submit',
icon = 'check',
value = command('submit'),
keep_open = false
},
}
}
end
mp.add_forced_key_binding('t', 'test_menu', function()
local json = utils.format_json(create_menu_data())
mp.commandv('script-message-to', 'uosc', 'open-menu', json)
end)
mp.register_script_message('set-state', function(prop, value)
state[prop] = value
-- Update currently opened menu
local json = utils.format_json(create_menu_data())
mp.commandv('script-message-to', 'uosc', 'update-menu', json)
end)
mp.register_script_message('submit', function(prop, value)
-- Do something with state
end)
Selects an item in menu and immediately scrolls to it. Ignored if user is currently navigating with a pointer and not a keyboard.
-
<menu_type>
required - Your menu type. Ensures you won't be messing with other menus. -
<item_index>
required - Index of an item to select. -
[submenu_id]
optional - ID of (sub)menu in nested menus. When empty selects item in currently active (sub)menu.
Closes the menu. If the optional parameter type
is provided, then the menu only
closes if it matches Menu.type
of the currently open menu.
Provides more control over menu lifecycle via sending detailed events to callback, and allowing it to decide what happens afterwards. It's also the only way to access what action button was pressed when activating an item.
Menu.callback
property value are params to be used in script-message-to
command before event data to reach your message handler:
-- Assuming `callback = {'foo', 'bar'}`, each event will be triggered with:
mp.commandv('script-message-to', 'foo', 'bar', event_data)
Not all events are redirected to callback automatically, some need to be specifically configured to do so by setting their on_{event}
config to 'callback'
. Here's a table:
Event | Condition | Note |
---|---|---|
activate |
always | Any enter or primary click with any modifier combination is turned into activate event.You need to send close-menu message to close the menu. |
move |
on_move='callback' |
Condition informs us that you're handling this event, so we can adjust selected item index. (ctrl+up/down/pgup/pgdwn/home/end) |
search |
on_search='callback' |
Condition informs us that you're handling this event, so we redirect search handling to callback. (ctrl+enter) |
key |
always | Only keys that don't already have a function are sent. You'll get enter (and its modifier combinations) only if no item is selected and user presses enter key, otherwise it turns into activate event.Due to environment limitations, we don't listen to every shortcut possible. You can see which ones are bound by reading the Menu:enable_key_bindings() function source code in src/uosc/elements/Menu.lua . |
paste |
on_paste='callback' |
Condition informs us that you're handling this event, so we don't start search on paste, but redirect it to callback. (ctrl+v) |
back |
always | Fired when user tries to navigate back in submenu structure when there's no parent menu. (backspace) |
close |
on_close='callback' |
When handled by callback and user tries to close the menu, it'll instead send a close event, and you have to close by sending close-menu message. (esc, or primary mouse button on background) |
event_data
is a json encoded string with one of these interfaces (pseudo types):
EventActivate {
type: 'activate';
index: number;
value: any;
action?: string;
keep_open?: boolean; // Inherited from item or its menu prop.
modifiers?: string; // Combined modifiers ID string, e.g.: `alt+ctrl` - lowercase & in alphabetical order.
alt: boolean;
ctrl: boolean;
shift: boolean;
is_pointer: boolean; // Whether the event was triggered by a pointer click/tap.
menu_id: string;
}
EventMove {type: 'move'; from_index: number; to_index: number; menu_id: string;}
EventSearch {type: 'search'; query: string; menu_id: string;}
EventKey {
type: 'key';
id: string; // e.g.: `alt+ctrl+enter` always lowercase, modifiers in alphabetical order, key last.
key: string; // e.g.: `enter`. Note: mouse primary is normalized to `enter`.
modifiers?: string; // e.g.: `alt+ctrl` always lowercase & in alphabetical order.
alt: boolean;
ctrl: boolean;
shift: boolean;
menu_id: string;
selected_item?: {index: number; value: any; action?: string;}
}
EventPaste {
type: 'paste';
value: string;
menu_id: string;
selected_item?: {index: number; value: any; action?: string;}
}
EventBack {type: 'back';}
EventClose {type: 'close';}
Example:
local utils = require('mp.utils')
local menu = {
type = 'menu_type',
title = 'Custom menu',
callback = {mp.get_script_name(), 'menu-event'},
actions = {
{name = 'thumb_up', icon = 'thumb_up', label = 'Thumbs up'},
},
items = {
{title = 'Foo', hint = 'foo', value = 'foo'},
{title = 'Bar', hint = 'bar', value = 'bar', active = true},
}
}
-- Open menu
mp.commandv('script-message-to', 'uosc', 'open-menu', utils.format_json(menu))
-- Handle events
mp.register_script_message('menu-event', function(json)
local event = utils.parse_json(json)
if event.type == 'activate' then
print(event.value) -- 'foo' | 'bar'
print(event.action) -- nil | 'thumb_up'
mp.commandv('script-message-to', 'uosc', 'close-menu', 'menu_type')
end
end)