Skip to content

Commit

Permalink
feat: set-button API for scripts to add custom control bar buttons (#…
Browse files Browse the repository at this point in the history
…938)

* feat: `set-button` API for scripts to add custom control bar buttons

Adds `set-button <name> <data_json>` 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:<name>` syntax.

closes #935
  • Loading branch information
tomasklaen authored Jul 29, 2024
1 parent 9fa7220 commit fe0d394
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 5 deletions.
1 change: 1 addition & 0 deletions src/uosc.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 10 additions & 5 deletions src/uosc/elements/Button.lua
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
Expand All @@ -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

Expand Down
14 changes: 14 additions & 0 deletions src/uosc/elements/Controls.lua
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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})
Expand Down
29 changes: 29 additions & 0 deletions src/uosc/elements/ManagedButton.lua
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions src/uosc/lib/buttons.lua
Original file line number Diff line number Diff line change
@@ -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<string, ButtonSubscriber[]>
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
16 changes: 16 additions & 0 deletions src/uosc/lib/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
1 change: 1 addition & 0 deletions src/uosc/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit fe0d394

Please sign in to comment.