Skip to content

Commit

Permalink
feat: added disable_elements option and disable-elements script m…
Browse files Browse the repository at this point in the history
…essage (#695)

* feat: added `disable_elements` option and `disable-elements` script message

Allows disabling elements and various indicators by adding their IDs to the list:

```conf
disable_elements=timeline,audio_indicator
```

Also includes a new script message listener `disable-elements`, that does the same thing:

```lua
local id = mp.get_script_name()
mp.commandv('script-message-to', 'uosc', 'disable-elements', id, 'timeline,audio_indicator')
```

It'll register what elements each script wants disabled. The element will be enabled only when it is not disabled by neither user nor any script.

To cancel or re-enable the elements, just pass an empty list:

```lua
mp.commandv('script-message-to', 'uosc', 'disable-elements', id, '')
```

ref #686, closes #592
  • Loading branch information
tomasklaen authored Oct 14, 2023
1 parent b7529ea commit 3af5ccf
Show file tree
Hide file tree
Showing 18 changed files with 303 additions and 123 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,16 @@ mp.commandv('script-message-to', 'uosc', 'overwrite-binding', 'stream-quality',
To cancel the overwrite and return to default behavior, just omit the `<command>` parameter.
### `disable-elements <script_id> <element_ids>`
Set what uosc elements your script wants to disable. To cancel or re-enable them, send the message again with an empty string in place of `element_ids`.
```lua
mp.commandv('script-message-to', 'uosc', 'disable-elements', mp.get_script_name(), 'timeline,volume')
```
Using `'user'` as `script_id` will overwrite user's `disable_elements` config. Elements will be enabled only when neither user, nor any script requested them to be disabled.
## Why _uosc_?
It stood for micro osc as it used to render just a couple rectangles before it grew to what it is today. And now it means a minimalist UI design direction where everything is out of your way until needed.
5 changes: 5 additions & 0 deletions script-opts/uosc.conf
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,8 @@ chapter_range_patterns=openings:オープニング;endings:エンディング
# `slang` is a keyword to inherit values from `--slang` mpv config.
# Supports paths to custom json files: `languages=~~/custom.json,slang,en`
languages=slang,en

# A comma separated list of element IDs to disable. Available IDs:
# window_border, top_bar, timeline, controls, volume,
# audio_indicator, buffering_indicator, pause_indicator
disable_elements=
3 changes: 2 additions & 1 deletion scripts/uosc/elements/BufferingIndicator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ local BufferingIndicator = class(Element)

function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end
function BufferingIndicator:init()
Element.init(self, 'buffer_indicator')
Element.init(self, 'buffer_indicator', {render_order = 2})
self.ignores_menu = true
self.enabled = false
self:decide_enabled()
end

function BufferingIndicator:decide_enabled()
Expand Down
35 changes: 24 additions & 11 deletions scripts/uosc/elements/Controls.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ local Controls = class(Element)

function Controls:new() return Class.new(self) --[[@as Controls]] end
function Controls:init()
Element.init(self, 'controls')
Element.init(self, 'controls', {render_order = 6})
---@type ControlItem[] All control elements serialized from `options.controls`.
self.controls = {}
---@type ControlItem[] Only controls that match current dispositions.
Expand All @@ -22,6 +22,11 @@ function Controls:init()
self:init_options()
end

function Controls:destroy()
self:destroy_elements()
Element.destroy(self)
end

function Controls:init_options()
-- Serialize control elements
local shorthands = {
Expand Down Expand Up @@ -109,6 +114,7 @@ function Controls:init_options()
))
else
local element = Button:new('control_' .. i, {
render_order = self.render_order,
icon = params[1],
anchor_id = 'controls',
on_click = function() mp.command(params[2]) end,
Expand Down Expand Up @@ -140,14 +146,18 @@ function Controls:init_options()
end

local element = CycleButton:new('control_' .. i, {
prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip,
render_order = self.render_order,
prop = params[2],
anchor_id = 'controls',
states = states,
tooltip = tooltip,
})
table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1})
if badge then self:register_badge_updater(badge, element) end
end
elseif kind == 'speed' then
if not Elements.speed then
local element = Speed:new({anchor_id = 'controls'})
local element = Speed:new({anchor_id = 'controls', render_order = self.render_order})
local scale = tonumber(params[1]) or 1.3
table_assign(control, {
element = element, sizing = 'dynamic', scale = scale, ratio = 3.5, ratio_min = 2,
Expand Down Expand Up @@ -215,24 +225,23 @@ function Controls:register_badge_updater(badge, element)
end

if is_external_prop then element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end
else mp.observe_property(observable_name, 'native', handler) end
else self:observe_mp_property(observable_name, handler) end
end

function Controls:get_visibility()
return (Elements.speed and Elements.speed.dragging) and 1 or Elements.timeline:get_is_hovered()
return Elements:v('speed', 'dragging') and 1 or Elements:maybe('timeline', 'get_is_hovered')
and -1 or Element.get_visibility(self)
end

function Controls:update_dimensions()
local window_border = Elements.window_border.size
local window_border = Elements:v('window_border', 'size', 0)
local size = round(options.controls_size * state.scale)
local spacing = round(options.controls_spacing * state.scale)
local margin = round(options.controls_margin * state.scale)

-- Disable when not enough space
local available_space = display.height - Elements.window_border.size * 2
if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end
if Elements.timeline.enabled then available_space = available_space - Elements.timeline.size end
local available_space = display.height - window_border * 2 - Elements:v('top_bar', 'size', 0)
- Elements:v('timeline', 'size', 0)
self.enabled = available_space > size + 10

-- Reset hide/enabled flags
Expand All @@ -245,7 +254,7 @@ function Controls:update_dimensions()

-- Container
self.bx = display.width - window_border - margin
self.by = (Elements.timeline.enabled and Elements.timeline.ay or display.height - window_border) - margin
self.by = Elements:v('timeline', 'ay', display.height - window_border) - margin
self.ax, self.ay = window_border + margin, self.by - size

-- Controls
Expand Down Expand Up @@ -332,10 +341,14 @@ function Controls:on_prop_title_bar() self:update_dimensions() end
function Controls:on_prop_fullormaxed() self:update_dimensions() end
function Controls:on_timeline_enabled() self:update_dimensions() end

function Controls:on_options()
function Controls:destroy_elements()
for _, control in ipairs(self.controls) do
if control.element then control.element:destroy() end
end
end

function Controls:on_options()
self:destroy_elements()
self:init_options()
end

Expand Down
2 changes: 1 addition & 1 deletion scripts/uosc/elements/Curtain.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ local Curtain = class(Element)

function Curtain:new() return Class.new(self) --[[@as Curtain]] end
function Curtain:init()
Element.init(self, 'curtain', {ignores_menu = true})
Element.init(self, 'curtain', {ignores_menu = true, render_order = 999})
self.opacity = 0
---@type string[]
self.dependents = {}
Expand Down
19 changes: 7 additions & 12 deletions scripts/uosc/elements/CycleButton.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ function CycleButton:init(id, props)
end
end

self.handle_change = function(name, value)
if is_state_prop and type(value) == 'boolean' then value = value and 'yes' or 'no' end
local function handle_change(name, value)
value = type(value) == 'boolean' and (value and 'yes' or 'no') or tostring(value or '')
local index = itable_find(self.states, function(state) return state.value == value end)
self.current_state_index = index or 1
self.icon = self.states[self.current_state_index].icon
Expand All @@ -46,19 +46,14 @@ function CycleButton:init(id, props)
local prop_parts = split(self.prop, '@')
if #prop_parts == 2 then -- External prop with a script owner
self.prop, self.owner = prop_parts[1], prop_parts[2]
self['on_external_prop_' .. self.prop] = function(_, value) self.handle_change(self.prop, value) end
self.handle_change(self.prop, external[self.prop])
self['on_external_prop_' .. self.prop] = function(_, value) handle_change(self.prop, value) end
handle_change(self.prop, external[self.prop])
elseif is_state_prop then -- uosc's state props
self['on_prop_' .. self.prop] = function(self, value) self.handle_change(self.prop, value) end
self.handle_change(self.prop, state[self.prop])
self['on_prop_' .. self.prop] = function(self, value) handle_change(self.prop, value) end
handle_change(self.prop, state[self.prop])
else
mp.observe_property(self.prop, 'string', self.handle_change)
self:observe_mp_property(self.prop, handle_change)
end
end

function CycleButton:destroy()
Button.destroy(self)
mp.unobserve_property(self.handle_change)
end

return CycleButton
35 changes: 33 additions & 2 deletions scripts/uosc/elements/Element.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;}
---@alias ElementProps {enabled?: boolean; render_order?: number; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;}

-- Base class all elements inherit from.
---@class Element : Class
Expand All @@ -8,6 +8,7 @@ local Element = class()
---@param props? ElementProps
function Element:init(id, props)
self.id = id
self.render_order = 1
-- `false` means element won't be rendered, or receive events
self.enabled = true
-- Element coordinates
Expand All @@ -24,6 +25,8 @@ function Element:init(id, props)
self.ignores_menu = false
---@type nil|string ID of an element from which this one should inherit visibility.
self.anchor_id = nil
---@type fun()[] Disposer functions called when element is destroyed.
self._disposers = {}

if props then table_assign(self, props) end

Expand All @@ -40,6 +43,7 @@ function Element:init(id, props)
end

function Element:destroy()
for _, disposer in ipairs(self._disposers) do disposer() end
self.destroyed = true
Elements:remove(self)
end
Expand Down Expand Up @@ -70,7 +74,10 @@ function Element:is_persistent()
local persist = config[self.id .. '_persistency']
return persist and (
(persist.audio and state.is_audio)
or (persist.paused and state.pause and (not Elements.timeline.pressed or Elements.timeline.pressed.pause))
or (
persist.paused and state.pause
and (not Elements.timeline or not Elements.timeline.pressed or Elements.timeline.pressed.pause)
)
or (persist.video and state.is_video)
or (persist.image and state.is_image)
or (persist.idle and state.is_idle)
Expand Down Expand Up @@ -154,4 +161,28 @@ function Element:flash()
end
end

-- Register disposer to be called when element is destroyed.
---@param disposer fun()
function Element:register_disposer(disposer)
if not itable_index_of(self._disposers, disposer) then
self._disposers[#self._disposers + 1] = disposer
end
end

-- Automatically registers disposer for the passed callback.
---@param event string
---@param callback fun()
function Element:register_mp_event(event, callback)
mp.register_event(event, callback)
self:register_disposer(function() mp.unregister_event(callback) end)
end

-- Automatically registers disposer for the observer.
---@param name string
---@param callback fun(name: string, value: any)
function Element:observe_mp_property(name, callback)
mp.observe_property(name, 'native', callback)
self:register_disposer(function() mp.unobserve_property(callback) end)
end

return Element
33 changes: 26 additions & 7 deletions scripts/uosc/elements/Elements.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
local Elements = {itable = {}}
local Elements = {_all = {}}

---@param element Element
function Elements:add(element)
Expand All @@ -9,9 +9,12 @@ function Elements:add(element)

if self:has(element.id) then Elements:remove(element.id) end

self.itable[#self.itable + 1] = element
self._all[#self._all + 1] = element
self[element.id] = element

-- Sort by render order
table.sort(self._all, function(a, b) return a.render_order < b.render_order end)

request_render()
end

Expand All @@ -22,7 +25,7 @@ function Elements:remove(idOrElement)
if element then
if not element.destroyed then element:destroy() end
element.enabled = false
self.itable = itable_delete_value(self.itable, self[id])
self._all = itable_delete_value(self._all, self[id])
self[id] = nil
request_render()
end
Expand Down Expand Up @@ -95,7 +98,7 @@ end
-- Flash passed elements.
---@param ids string[] IDs of elements to peek.
function Elements:flash(ids)
local elements = itable_filter(self.itable, function(element) return itable_index_of(ids, element.id) ~= nil end)
local elements = itable_filter(self._all, function(element) return itable_index_of(ids, element.id) ~= nil end)
for _, element in ipairs(elements) do element:flash() end
end

Expand All @@ -108,8 +111,8 @@ end
-- Disabled elements don't receive these events.
---@param name string Event name.
function Elements:proximity_trigger(name, ...)
for i = #self.itable, 1, -1 do
local element = self.itable[i]
for i = #self._all, 1, -1 do
local element = self._all[i]
if element.enabled then
if element.proximity_raw == 0 then
if element:trigger(name, ...) == 'stop_propagation' then break end
Expand All @@ -119,7 +122,23 @@ function Elements:proximity_trigger(name, ...)
end
end

-- Returns a property of an element with a passed `id` if it exists, with an optional fallback.
---@param id string
---@param prop string
---@param fallback any
function Elements:v(id, prop, fallback)
if self[id] and self[id].enabled and self[id][prop] ~= nil then return self[id][prop] end
return fallback
end

-- Calls a method on an element with passed `id` if it exists.
---@param id string
---@param method string
function Elements:maybe(id, method, ...)
if self[id] then return self[id]:maybe(method, ...) end
end

function Elements:has(id) return self[id] ~= nil end
function Elements:ipairs() return ipairs(self.itable) end
function Elements:ipairs() return ipairs(self._all) end

return Elements
11 changes: 7 additions & 4 deletions scripts/uosc/elements/Menu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function Menu:new(data, callback, opts) return Class.new(self, data, callback, o
---@param callback MenuCallback
---@param opts? MenuOptions
function Menu:init(data, callback, opts)
Element.init(self, 'menu', {ignores_menu = true})
Element.init(self, 'menu', {ignores_menu = true, render_order = 1000})

-----@type fun()
self.callback = callback
Expand Down Expand Up @@ -130,15 +130,15 @@ function Menu:init(data, callback, opts)

self:tween_property('opacity', 0, 1)
self:enable_key_bindings()
Elements.curtain:register('menu')
Elements:maybe('curtain', 'register', 'menu')
if self.opts.on_open then self.opts.on_open() end
end

function Menu:destroy()
Element.destroy(self)
self:disable_key_bindings()
self.is_closed = true
if not self.is_being_replaced then Elements.curtain:unregister('menu') end
if not self.is_being_replaced then Elements:maybe('curtain', 'unregister', 'menu') end
if utils.shared_script_property_set then
utils.shared_script_property_set('uosc-menu-type', nil)
end
Expand Down Expand Up @@ -969,11 +969,14 @@ function Menu:disable_key_bindings()
self.key_bindings = {}
end

-- Check if menu is not closed or closing.
function Menu:is_alive() return not self.is_closing and not self.is_closed end

-- Wraps a function so that it won't run if menu is closing or closed.
---@param fn function()
function Menu:create_action(fn)
return function(...)
if not self.is_closing and not self.is_closed then fn(...) end
if self:is_alive() then fn(...) end
end
end

Expand Down
Loading

0 comments on commit 3af5ccf

Please sign in to comment.