From fe0d394435ad562176ae850175ebd5a083f0a6c2 Mon Sep 17 00:00:00 2001 From: Tomas Klaen <47283320+tomasklaen@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:48:51 +0200 Subject: [PATCH] feat: `set-button` API for scripts to add custom control bar buttons (#938) * feat: `set-button` API for scripts to add custom control bar buttons Adds `set-button ` message external scripts can send to add or update buttons whose state, look, and click action is defined by `data_json`. Users can then add this button into their controls bar with `button:` syntax. closes #935 --- src/uosc.conf | 1 + src/uosc/elements/Button.lua | 15 ++++--- src/uosc/elements/Controls.lua | 14 ++++++ src/uosc/elements/ManagedButton.lua | 29 ++++++++++++ src/uosc/lib/buttons.lua | 69 +++++++++++++++++++++++++++++ src/uosc/lib/utils.lua | 16 +++++++ src/uosc/main.lua | 1 + 7 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 src/uosc/elements/ManagedButton.lua create mode 100644 src/uosc/lib/buttons.lua diff --git a/src/uosc.conf b/src/uosc.conf index 8370f570..6762c33c 100644 --- a/src/uosc.conf +++ b/src/uosc.conf @@ -43,6 +43,7 @@ progress_line_width=20 # {scale} - factor of controls_size, default: 0.3 # `space` - fills all available space between previous and next item, useful to align items to the right # - multiple spaces divide the available space among themselves, which can be used for centering +# `button:{name}` - button whose state, look, and click action are managed by external script # Item visibility control: # `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility # - `{disposition}` can be one of: diff --git a/src/uosc/elements/Button.lua b/src/uosc/elements/Button.lua index b0d29f04..6ae84a36 100644 --- a/src/uosc/elements/Button.lua +++ b/src/uosc/elements/Button.lua @@ -1,6 +1,6 @@ local Element = require('elements/Element') ----@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string} +---@alias ButtonProps {icon: string; on_click?: function; is_clickable?: boolean; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string} ---@class Button : Element local Button = class(Element) @@ -17,13 +17,15 @@ function Button:init(id, props) self.badge = props.badge self.foreground = props.foreground or fg self.background = props.background or bg - ---@type fun() + self.is_clickable = true + ---@type fun()|nil self.on_click = props.on_click Element.init(self, id, props) end function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end function Button:handle_cursor_click() + if not self.on_click or not self.is_clickable then return end -- We delay the callback to next tick, otherwise we are risking race -- conditions as we are in the middle of event dispatching. -- For example, handler might add a menu to the end of the element stack, and that @@ -37,17 +39,20 @@ function Button:render() cursor:zone('primary_click', self, function() self:handle_cursor_click() end) local ass = assdraw.ass_new() + local is_clickable = self.is_clickable and self.on_click ~= nil local is_hover = self.proximity_raw == 0 - local is_hover_or_active = is_hover or self.active local foreground = self.active and self.background or self.foreground local background = self.active and self.foreground or self.background + local background_opacity = self.active and 1 or config.opacity.controls + + if is_hover and is_clickable and background_opacity < 0.3 then background_opacity = 0.3 end -- Background - if is_hover_or_active or config.opacity.controls > 0 then + if background_opacity > 0 then ass:rect(self.ax, self.ay, self.bx, self.by, { color = (self.active or not is_hover) and background or foreground, radius = state.radius, - opacity = visibility * (self.active and 1 or (is_hover and 0.3 or config.opacity.controls)), + opacity = visibility * background_opacity, }) end diff --git a/src/uosc/elements/Controls.lua b/src/uosc/elements/Controls.lua index 12c8e9c3..4a1c4029 100644 --- a/src/uosc/elements/Controls.lua +++ b/src/uosc/elements/Controls.lua @@ -1,6 +1,7 @@ local Element = require('elements/Element') local Button = require('elements/Button') local CycleButton = require('elements/CycleButton') +local ManagedButton = require('elements/ManagedButton') local Speed = require('elements/Speed') -- sizing: @@ -163,6 +164,19 @@ function Controls:init_options() table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1}) if badge then self:register_badge_updater(badge, element) end end + elseif kind == 'button' then + if #params ~= 1 then + mp.error(string.format( + 'managed button needs 1 parameter, %d received: %s', #params, table.concat(params, '/') + )) + else + local element = ManagedButton:new('control_' .. i, { + name = params[1], + render_order = self.render_order, + anchor_id = 'controls', + }) + table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1}) + end elseif kind == 'speed' then if not Elements.speed then local element = Speed:new({anchor_id = 'controls', render_order = self.render_order}) diff --git a/src/uosc/elements/ManagedButton.lua b/src/uosc/elements/ManagedButton.lua new file mode 100644 index 00000000..615cde19 --- /dev/null +++ b/src/uosc/elements/ManagedButton.lua @@ -0,0 +1,29 @@ +local Button = require('elements/Button') + +---@alias ManagedButtonProps {name: string; anchor_id?: string; render_order?: number} + +---@class ManagedButton : Button +local ManagedButton = class(Button) + +---@param id string +---@param props ManagedButtonProps +function ManagedButton:new(id, props) return Class.new(self, id, props) --[[@as ManagedButton]] end +---@param id string +---@param props ManagedButtonProps +function ManagedButton:init(id, props) + ---@type string | table | nil + self.command = nil + + Button.init(self, id, table_assign({}, props, {on_click = function() execute_command(self.command) end})) + + self:register_disposer(buttons:subscribe(props.name, function(data) self:update(data) end)) +end + +function ManagedButton:update(data) + for _, prop in ipairs({'icon', 'active', 'badge', 'command', 'tooltip'}) do + self[prop] = data[prop] + end + self.is_clickable = self.command ~= nil +end + +return ManagedButton diff --git a/src/uosc/lib/buttons.lua b/src/uosc/lib/buttons.lua new file mode 100644 index 00000000..b5c086a8 --- /dev/null +++ b/src/uosc/lib/buttons.lua @@ -0,0 +1,69 @@ +---@alias ButtonData {icon: string; active?: boolean; badge?: string; command?: string | string[]; tooltip?: string;} +---@alias ButtonSubscriber fun(data: ButtonData) + +local buttons = { + ---@type ButtonData[] + data = {}, + ---@type table + subscribers = {}, +} + +---@param name string +---@param callback fun(data: ButtonData) +function buttons:subscribe(name, callback) + local pool = self.subscribers[name] + if not pool then + pool = {} + self.subscribers[name] = pool + end + pool[#pool + 1] = callback + self:trigger(name) + return function() buttons:unsubscribe(name, callback) end +end + +---@param name string +---@param callback? ButtonSubscriber +function buttons:unsubscribe(name, callback) + if self.subscribers[name] then + if callback == nil then + self.subscribers[name] = {} + else + itable_delete_value(self.subscribers[name], callback) + end + end +end + +---@param name string +function buttons:trigger(name) + local pool = self.subscribers[name] + local data = self.data[name] or {icon = 'help_center', tooltip = 'Uninitialized button "' .. name .. '"'} + if pool then + for _, callback in ipairs(pool) do callback(data) end + end +end + +---@param name string +---@param data ButtonData +function buttons:set(name, data) + buttons.data[name] = data + buttons:trigger(name) + request_render() +end + +mp.register_script_message('set-button', function(name, data) + if type(name) ~= 'string' then + msg.error('Invalid set-button message parameter: 1st parameter (name) has to be a string.') + return + end + if type(data) ~= 'string' then + msg.error('Invalid set-button message parameter: 2nd parameter (data) has to be a string.') + return + end + + local data = utils.parse_json(data) + if type(data) == 'table' and type(data.icon) == 'string' then + buttons:set(name, data) + end +end) + +return buttons diff --git a/src/uosc/lib/utils.lua b/src/uosc/lib/utils.lua index 8166e590..fc0f6dcb 100644 --- a/src/uosc/lib/utils.lua +++ b/src/uosc/lib/utils.lua @@ -398,6 +398,22 @@ function has_any_extension(path, extensions) return false end +-- Executes mp command defined as a string or an itable, or does nothing if command is any other value. +-- Returns boolean specifying if command was executed or not. +---@param command string | string[] | nil | any +---@return boolean executed `true` if command was executed. +function execute_command(command) + local command_type = type(command) + if command_type == 'string' then + mp.command(command) + return true + elseif command_type == 'table' and #command > 0 then + mp.command_native(command) + return true + end + return false +end + ---@return string function get_default_directory() return mp.command_native({'expand-path', options.default_directory}) diff --git a/src/uosc/main.lua b/src/uosc/main.lua index 20dfafc8..b8af2259 100644 --- a/src/uosc/main.lua +++ b/src/uosc/main.lua @@ -395,6 +395,7 @@ state = { scale = 1, radius = 0, } +buttons = require('lib/buttons') thumbnail = {width = 0, height = 0, disabled = false} external = {} -- Properties set by external scripts key_binding_overwrites = {} -- Table of key_binding:mpv_command