refactor!: active
and selected_index
menu API
Allows multiple items in a menu to be `active`. `select` flag is no longer on the item, but moved to the menu properties as `selected_index`.
This commit is contained in:
10
README.md
10
README.md
@@ -378,6 +378,7 @@ Menu {
|
|||||||
type?: string;
|
type?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
items: Item[];
|
items: Item[];
|
||||||
|
selected_index?: integer;
|
||||||
keep_open?: boolean;
|
keep_open?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,8 +399,7 @@ Command {
|
|||||||
bold?: boolean;
|
bold?: boolean;
|
||||||
italic?: boolean;
|
italic?: boolean;
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
selected?: number;
|
active?: integer;
|
||||||
active?: number;
|
|
||||||
keep_open?: boolean;
|
keep_open?: boolean;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -408,11 +408,11 @@ When command value is a string, it'll be passed to `mp.command(value)`. If it's
|
|||||||
|
|
||||||
Menu `type` controls what happens when opening a menu when some other menu is already open. When the new menu type is different, it'll replace the currently opened menu. When it's the same, the currently open menu will simply be closed. This is used to implement toggling of menus with the same type.
|
Menu `type` controls what happens when opening a menu when some other menu is already open. When the new menu type is different, it'll replace the currently opened menu. When it's the same, the currently open menu will simply be closed. This is used to implement toggling of menus with the same type.
|
||||||
|
|
||||||
`item.icon` property accepts icon names you can pick from here: https://fonts.google.com/icons?selected=Material+Icons
|
`item.icon` property accepts icon names. You can pick one from here: [Google Material Icons](https://fonts.google.com/icons?selected=Material+Icons)
|
||||||
|
|
||||||
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. Set to `false` to override the parent.
|
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.
|
||||||
|
|
||||||
It's usually not necessary to define `selected` as it'll default to `active` item, or 1st item in the list.
|
It's usually not necessary to define `selected_index` as it'll default to the first `active` item, or 1st item in the list.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
122
scripts/uosc.lua
122
scripts/uosc.lua
@@ -1486,15 +1486,15 @@ menu.close()
|
|||||||
]]
|
]]
|
||||||
|
|
||||||
-- Menu data structure accepted by `Menu:open(menu)`.
|
-- Menu data structure accepted by `Menu:open(menu)`.
|
||||||
---@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected?: boolean;}
|
---@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer;}
|
||||||
---@alias MenuDataItem MenuDataValue|MenuData
|
---@alias MenuDataItem MenuDataValue|MenuData
|
||||||
---@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; selected?: boolean; keep_open?: boolean; separator?: boolean;}
|
---@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean;}
|
||||||
---@alias MenuOptions {on_open?: fun(), on_close?: fun()}
|
---@alias MenuOptions {on_open?: fun(), on_close?: fun()}
|
||||||
|
|
||||||
-- Internal data structure created from `Menu`.
|
-- Internal data structure created from `Menu`.
|
||||||
---@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; active_index?: number; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; width: number; height: number; top: number; scroll_y: number; scroll_height: number; selected?: boolean; title_length: number; title_width: number; hint_length: number; hint_width: number; max_width: number; is_root?: boolean;}
|
---@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_length: number; title_width: number; hint_length: number; hint_width: number; max_width: number; is_root?: boolean;}
|
||||||
---@alias MenuStackItem MenuStackValue|MenuStack
|
---@alias MenuStackItem MenuStackValue|MenuStack
|
||||||
---@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; title_length: number; title_width: number; hint_length: number; hint_width: number}
|
---@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; title_length: number; title_width: number; hint_length: number; hint_width: number}
|
||||||
|
|
||||||
---@class Menu : Element
|
---@class Menu : Element
|
||||||
local Menu = class(Element)
|
local Menu = class(Element)
|
||||||
@@ -1578,7 +1578,7 @@ function Menu:init(data, callback, opts)
|
|||||||
self:update(data)
|
self:update(data)
|
||||||
|
|
||||||
for _, menu in ipairs(self.all) do
|
for _, menu in ipairs(self.all) do
|
||||||
self:scroll_to_index(menu.selected_index or menu.active_index, menu)
|
self:scroll_to_index(menu.selected_index, menu)
|
||||||
end
|
end
|
||||||
|
|
||||||
self:tween_property('opacity', 0, 1)
|
self:tween_property('opacity', 0, 1)
|
||||||
@@ -1612,17 +1612,15 @@ function Menu:update(data)
|
|||||||
menu.hint_length = text_length(menu.title)
|
menu.hint_length = text_length(menu.title)
|
||||||
|
|
||||||
-- Update items
|
-- Update items
|
||||||
local selected_index = nil
|
local first_active_index = nil
|
||||||
local active_index = nil
|
|
||||||
menu.items = {}
|
menu.items = {}
|
||||||
|
|
||||||
for i, item_data in ipairs(menu_data.items or {}) do
|
for i, item_data in ipairs(menu_data.items or {}) do
|
||||||
if item_data.selected then selected_index = i end
|
if item_data.active and not first_active_index then first_active_index = i end
|
||||||
if item_data.active then active_index = i end
|
|
||||||
|
|
||||||
local item = {}
|
local item = {}
|
||||||
table_assign(item, item_data, {
|
table_assign(item, item_data, {
|
||||||
'title', 'icon', 'hint', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator',
|
'title', 'icon', 'hint', 'active', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator',
|
||||||
})
|
})
|
||||||
if item.keep_open == nil then item.keep_open = menu.keep_open end
|
if item.keep_open == nil then item.keep_open = menu.keep_open end
|
||||||
item.title_length = text_length(item.title)
|
item.title_length = text_length(item.title)
|
||||||
@@ -1638,9 +1636,8 @@ function Menu:update(data)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if menu.is_root then
|
if menu.is_root then
|
||||||
menu.selected_index = selected_index or active_index or (#menu.items > 0 and 1 or nil)
|
menu.selected_index = menu_data.selected_index or first_active_index or (#menu.items > 0 and 1 or nil)
|
||||||
end
|
end
|
||||||
menu.active_index = active_index
|
|
||||||
|
|
||||||
-- Retain old state
|
-- Retain old state
|
||||||
local old_menu = self.by_id[menu.is_root and '__root__' or menu.id]
|
local old_menu = self.by_id[menu.is_root and '__root__' or menu.id]
|
||||||
@@ -1724,8 +1721,7 @@ function Menu:reset_navigation()
|
|||||||
local menu = self.current
|
local menu = self.current
|
||||||
|
|
||||||
-- Reset indexes and scroll
|
-- Reset indexes and scroll
|
||||||
self:select_index(menu.selected_index or menu.active_index or (menu.items and #menu.items > 0 and 1 or nil))
|
self:select_index(menu.selected_index or (menu.items and #menu.items > 0 and 1 or nil))
|
||||||
self:activate_index(menu.active_index)
|
|
||||||
self:scroll_to(menu.scroll_y)
|
self:scroll_to(menu.scroll_y)
|
||||||
|
|
||||||
-- Walk up the parent menu chain and activate items that lead to current menu
|
-- Walk up the parent menu chain and activate items that lead to current menu
|
||||||
@@ -1752,6 +1748,13 @@ function Menu:get_item_index_below_cursor()
|
|||||||
return math.max(1, math.min(math.ceil((cursor.y - self.ay + menu.scroll_y) / self.scroll_step), #menu.items))
|
return math.max(1, math.min(math.ceil((cursor.y - self.ay + menu.scroll_y) / self.scroll_step), #menu.items))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Menu:get_first_active_index(menu)
|
||||||
|
menu = menu or self.current
|
||||||
|
for index, item in ipairs(self.current.items) do
|
||||||
|
if item.active then return index end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@param pos? number
|
---@param pos? number
|
||||||
---@param menu? MenuStack
|
---@param menu? MenuStack
|
||||||
function Menu:scroll_to(pos, menu)
|
function Menu:scroll_to(pos, menu)
|
||||||
@@ -1785,20 +1788,40 @@ function Menu:select_value(value, menu)
|
|||||||
self:select_index(index, 5)
|
self:select_index(index, 5)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param menu? MenuStack
|
||||||
|
function Menu:deactivate_items(menu)
|
||||||
|
menu = menu or self.current
|
||||||
|
for _, item in ipairs(menu.items) do item.active = false end
|
||||||
|
request_render()
|
||||||
|
end
|
||||||
|
|
||||||
---@param index? integer
|
---@param index? integer
|
||||||
---@param menu? MenuStack
|
---@param menu? MenuStack
|
||||||
function Menu:activate_index(index, menu)
|
function Menu:activate_index(index, menu)
|
||||||
menu = menu or self.current
|
menu = menu or self.current
|
||||||
menu.active_index = (index and index >= 1 and index <= #menu.items) and index or nil
|
if index and index >= 1 and index <= #menu.items then menu.items[index] = true end
|
||||||
request_render()
|
request_render()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param index? integer
|
||||||
|
---@param menu? MenuStack
|
||||||
|
function Menu:activate_unique_index(index, menu)
|
||||||
|
self:deactivate_items(menu)
|
||||||
|
self:activate_index(index, menu)
|
||||||
|
end
|
||||||
|
|
||||||
---@param value? any
|
---@param value? any
|
||||||
---@param menu? MenuStack
|
---@param menu? MenuStack
|
||||||
function Menu:activate_value(value, menu)
|
function Menu:activate_value(value, menu)
|
||||||
menu = menu or self.current
|
menu = menu or self.current
|
||||||
local index = itable_find(menu.items, function(_, item) return item.value == value end)
|
self:activate_index(itable_find(menu.items, function(_, item) return item.value == value end), menu)
|
||||||
self:activate_index(index, menu)
|
end
|
||||||
|
|
||||||
|
---@param value? any
|
||||||
|
---@param menu? MenuStack
|
||||||
|
function Menu:activate_unique_value(value, menu)
|
||||||
|
menu = menu or self.current
|
||||||
|
self:activate_unique_index(itable_find(menu.items, function(_, item) return item.value == value end), menu)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param id string
|
---@param id string
|
||||||
@@ -1818,10 +1841,8 @@ end
|
|||||||
function Menu:delete_index(index, menu)
|
function Menu:delete_index(index, menu)
|
||||||
menu = menu or self.current
|
menu = menu or self.current
|
||||||
if (index and index >= 1 and index <= #menu.items) then
|
if (index and index >= 1 and index <= #menu.items) then
|
||||||
local previous_active_value = menu.active_index and menu.items[menu.active_index].value or nil
|
|
||||||
table.remove(menu.items, index)
|
table.remove(menu.items, index)
|
||||||
self:update_content_dimensions()
|
self:update_content_dimensions()
|
||||||
if previous_active_value then self:activate_value(previous_active_value, menu) end
|
|
||||||
self:scroll_to_index(menu.selected_index, menu)
|
self:scroll_to_index(menu.selected_index, menu)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2014,15 +2035,19 @@ function Menu:render()
|
|||||||
local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')'
|
local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')'
|
||||||
local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1
|
local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1
|
||||||
local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step)
|
local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step)
|
||||||
local selected_index, active_index = menu.selected_index or -1, menu.active_index or -1
|
local selected_index = menu.selected_index or -1
|
||||||
|
|
||||||
-- Background
|
-- Background
|
||||||
ass:rect(ax, ay - (draw_title and self.item_height or 0) - 3, bx, by + 3, {
|
ass:rect(ax, ay - (draw_title and self.item_height or 0) - 2, bx, by + 2, {
|
||||||
color = options.color_background, opacity = opacity, radius = 4,
|
color = options.color_background, opacity = opacity, radius = 4,
|
||||||
})
|
})
|
||||||
|
|
||||||
for index = start_index, end_index, 1 do
|
for index = start_index, end_index, 1 do
|
||||||
local item = menu.items[index]
|
local item = menu.items[index]
|
||||||
|
local next_item = menu.items[index + 1]
|
||||||
|
local is_highlighted = selected_index == index or item.active
|
||||||
|
local next_is_active = next_item and next_item.active
|
||||||
|
local next_is_highlighted = selected_index == index + 1 or next_is_active
|
||||||
|
|
||||||
if not item then break end
|
if not item then break end
|
||||||
|
|
||||||
@@ -2033,28 +2058,24 @@ function Menu:render()
|
|||||||
-- controls title & hint clipping proportional to the ratio of their widths
|
-- controls title & hint clipping proportional to the ratio of their widths
|
||||||
local title_hint_ratio = item.hint and item.title_width / (item.title_width + item.hint_width) or 1
|
local title_hint_ratio = item.hint and item.title_width / (item.title_width + item.hint_width) or 1
|
||||||
local content_ax, content_bx = ax + spacing, bx - spacing
|
local content_ax, content_bx = ax + spacing, bx - spacing
|
||||||
local font_color = active_index == index and options.color_foreground_text or options.color_background_text
|
local font_color = item.active and options.color_foreground_text or options.color_background_text
|
||||||
local shadow_color = active_index == index and options.color_foreground or options.color_background
|
local shadow_color = item.active and options.color_foreground or options.color_background
|
||||||
|
|
||||||
-- Separator
|
-- Separator
|
||||||
local separator_size = (item.separator and 3 or 1)
|
local separator_ay = item.separator and item_by - 1 or item_by
|
||||||
local hide_separator = separator_size == 1 and (
|
local separator_by = item_by + (item.separator and 2 or 1)
|
||||||
active_index == index or selected_index == index or
|
if is_highlighted then separator_ay = item_by + 1 end
|
||||||
active_index - 1 == index or selected_index - 1 == index
|
if next_is_highlighted then separator_by = item_by end
|
||||||
)
|
if separator_by - separator_ay > 0 and item_by < by then
|
||||||
if not hide_separator and item_by < by then
|
ass:rect(ax + spacing / 2, separator_ay, bx - spacing / 2, separator_by, {
|
||||||
local sep_ay = item_by - math.floor(separator_size / 2)
|
|
||||||
ass:rect(ax + 3, sep_ay, bx - 3, sep_ay + separator_size, {
|
|
||||||
color = options.color_foreground, opacity = opacity * 0.13,
|
color = options.color_foreground, opacity = opacity * 0.13,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Highlight
|
-- Highlight
|
||||||
local highlight_opacity = 0
|
local highlight_opacity = 0 + (item.active and 0.8 or 0) + (selected_index == index and 0.15 or 0)
|
||||||
+ (active_index == index and 0.8 or 0)
|
|
||||||
+ (selected_index == index and 0.15 or 0)
|
|
||||||
if highlight_opacity > 0 then
|
if highlight_opacity > 0 then
|
||||||
ass:rect(ax + 2, item_ay - 1, bx - 2, item_by + 1, {
|
ass:rect(ax + 2, item_ay, bx - 2, item_by, {
|
||||||
radius = 2, color = options.color_foreground, opacity = highlight_opacity * self.opacity,
|
radius = 2, color = options.color_foreground, opacity = highlight_opacity * self.opacity,
|
||||||
clip = item_clip,
|
clip = item_clip,
|
||||||
})
|
})
|
||||||
@@ -2097,16 +2118,17 @@ function Menu:render()
|
|||||||
|
|
||||||
-- Menu title
|
-- Menu title
|
||||||
if draw_title then
|
if draw_title then
|
||||||
local title_ay = ay - self.item_height - 1
|
local title_ay = ay - self.item_height
|
||||||
|
local title_height = self.item_height - 3
|
||||||
menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title)
|
menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title)
|
||||||
|
|
||||||
-- Background
|
-- Background
|
||||||
ass:rect(ax + 2, title_ay, bx - 2, ay - 3, {
|
ass:rect(ax + 2, title_ay, bx - 2, title_ay + title_height, {
|
||||||
color = options.color_foreground, opacity = opacity * 0.55, radius = 2,
|
color = options.color_foreground, opacity = opacity * 0.55, radius = 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Title
|
-- Title
|
||||||
ass:txt(ax + menu.width / 2, title_ay + (self.item_height * 0.5), 5, menu.title, {
|
ass:txt(ax + menu.width / 2, title_ay + (title_height / 2), 5, menu.title, {
|
||||||
size = self.font_size, bold = true, color = options.color_background, wrap = 2, opacity = opacity,
|
size = self.font_size, bold = true, color = options.color_background, wrap = 2, opacity = opacity,
|
||||||
clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')',
|
clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')',
|
||||||
})
|
})
|
||||||
@@ -3724,14 +3746,19 @@ function create_self_updating_menu_opener(options)
|
|||||||
local ignore_initial_active = true
|
local ignore_initial_active = true
|
||||||
local function handle_active_prop_change(name, value)
|
local function handle_active_prop_change(name, value)
|
||||||
if ignore_initial_active then ignore_initial = false
|
if ignore_initial_active then ignore_initial = false
|
||||||
else menu:activate_index(options.active_index_serializer(name, value)) end
|
else menu:activate_unique_index(options.active_index_serializer(name, value)) end
|
||||||
end
|
end
|
||||||
|
|
||||||
local initial_items = options.list_serializer(options.list_prop, mp.get_property_native(options.list_prop))
|
local initial_items, selected_index = options.list_serializer(
|
||||||
|
options.list_prop,
|
||||||
|
mp.get_property_native(options.list_prop)
|
||||||
|
)
|
||||||
|
|
||||||
-- Items and active_index are set in the handle_prop_change callback, since adding
|
-- Items and active_index are set in the handle_prop_change callback, since adding
|
||||||
-- a property observer triggers its handler immediately, we just let that initialize the items.
|
-- a property observer triggers its handler immediately, we just let that initialize the items.
|
||||||
menu = Menu:open({type = options.type, title = options.title, items = initial_items}, options.on_select, {
|
menu = Menu:open(
|
||||||
|
{type = options.type, title = options.title, items = initial_items, selected_index = selected_index},
|
||||||
|
options.on_select, {
|
||||||
on_open = function()
|
on_open = function()
|
||||||
mp.observe_property(options.list_prop, 'native', handle_list_prop_change)
|
mp.observe_property(options.list_prop, 'native', handle_list_prop_change)
|
||||||
if options.active_prop then
|
if options.active_prop then
|
||||||
@@ -3757,6 +3784,8 @@ function create_select_tracklist_type_menu_opener(menu_title, track_type, track_
|
|||||||
end
|
end
|
||||||
|
|
||||||
local first_item_index = #items + 1
|
local first_item_index = #items + 1
|
||||||
|
local active_index = nil
|
||||||
|
local disabled_item = nil
|
||||||
|
|
||||||
-- Add option to disable a subtitle track. This works for all tracks,
|
-- Add option to disable a subtitle track. This works for all tracks,
|
||||||
-- but why would anyone want to disable audio or video? Better to not
|
-- but why would anyone want to disable audio or video? Better to not
|
||||||
@@ -3764,7 +3793,8 @@ function create_select_tracklist_type_menu_opener(menu_title, track_type, track_
|
|||||||
-- If I'm mistaken and there is an active need for this, feel free to
|
-- If I'm mistaken and there is an active need for this, feel free to
|
||||||
-- open an issue.
|
-- open an issue.
|
||||||
if track_type == 'sub' then
|
if track_type == 'sub' then
|
||||||
items[#items + 1] = {title = 'Disabled', italic = true, muted = true, hint = '—', value = nil, active = true}
|
disabled_item = {title = 'Disabled', italic = true, muted = true, hint = '—', value = nil, active = true}
|
||||||
|
items[#items + 1] = disabled_item
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, track in ipairs(tracklist) do
|
for _, track in ipairs(tracklist) do
|
||||||
@@ -3791,13 +3821,17 @@ function create_select_tracklist_type_menu_opener(menu_title, track_type, track_
|
|||||||
title = (track.title and track.title or 'Track ' .. track.id),
|
title = (track.title and track.title or 'Track ' .. track.id),
|
||||||
hint = table.concat(hint_vals_filtered, ', '),
|
hint = table.concat(hint_vals_filtered, ', '),
|
||||||
value = track.id,
|
value = track.id,
|
||||||
selected = first_item_index == #items + 1 or track.selected,
|
|
||||||
active = track.selected,
|
active = track.selected,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if track.selected then
|
||||||
|
if disabled_item then disabled_item.active = false end
|
||||||
|
active_index = #items
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return items
|
return items, active_index or first_item_index
|
||||||
end
|
end
|
||||||
|
|
||||||
local function selection_handler(value)
|
local function selection_handler(value)
|
||||||
|
Reference in New Issue
Block a user