feat: add tooltips

This commit is contained in:
tomasklaen
2022-09-07 14:02:54 +02:00
parent 29834b4d88
commit 03abd30f8c
2 changed files with 98 additions and 70 deletions

View File

@@ -32,8 +32,9 @@ timeline_chapters=dots
timeline_chapters_opacity=0.2 timeline_chapters_opacity=0.2
timeline_chapters_width=6 timeline_chapters_width=6
# A comma delimited list of elements to construct the controls bar above the timeline. Set to `never` to disable. # A comma delimited list of items to construct the controls bar above the timeline. Set to `never` to disable.
# Parameter spec: enclosed in `{}` means value, enclosed in `[]` means optional # Parameter spec: enclosed in `{}` means value, enclosed in `[]` means optional
# Full item syntax: `[<[!]{disposition1}[,[!]{dispositionN}]>]element[:paramN][?{tooltip}]`
# Common properties: # Common properties:
# `{icon}` - parameter used to specify an icon name (example: `face`) # `{icon}` - parameter used to specify an icon name (example: `face`)
# - you can pick one here: https://fonts.google.com/icons?selected=Material+Icons # - you can pick one here: https://fonts.google.com/icons?selected=Material+Icons
@@ -58,7 +59,7 @@ timeline_chapters_width=6
# - fullscreen: `cycle:fullscreen:fullscreen:no/yes=fullscreen_exit!` # - fullscreen: `cycle:fullscreen:fullscreen:no/yes=fullscreen_exit!`
# - loop-playlist: `cycle:repeat:loop-playlist:no/inf!` # - loop-playlist: `cycle:repeat:loop-playlist:no/inf!`
# - `toggle:{icon}:{prop}`: `cycle:{icon}:{prop}:no/yes!` # - `toggle:{icon}:{prop}`: `cycle:{icon}:{prop}:no/yes!`
# Element visibility control: # Item visibility control:
# `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility # `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility
# - `{disposition}` can be one of: # - `{disposition}` can be one of:
# - `image` - true if current file is a single image # - `image` - true if current file is a single image
@@ -72,6 +73,9 @@ timeline_chapters_width=6
# - `<stream>stream-quality` - show stream quality button only for streams # - `<stream>stream-quality` - show stream quality button only for streams
# - `<has_audio,!audio>audio` - show audio tracks button for all files that have # - `<has_audio,!audio>audio` - show audio tracks button for all files that have
# an audio track, but are not exclusively audio only files # an audio track, but are not exclusively audio only files
# Item tooltip:
# Place `?Tooltip text` after the element config to give it a tooltip.
# Example: `<stream>stream-quality?Stream quality`
controls=menu,gap,subtitles,<has_audio,!audio>audio,<stream>stream-quality,gap,loop-playlist,loop-file,space,speed,space,prev,items,next,shuffle,gap:1,fullscreen controls=menu,gap,subtitles,<has_audio,!audio>audio,<stream>stream-quality,gap,loop-playlist,loop-file,space,speed,space,prev,items,next,shuffle,gap:1,fullscreen
controls_size=32 controls_size=32
controls_size_fullscreen=40 controls_size_fullscreen=40

View File

@@ -374,11 +374,11 @@ end
function text_width_estimate(text, font_size) function text_width_estimate(text, font_size)
if not text or text == '' then return 0 end if not text or text == '' then return 0 end
local text_width = 0 local text_length = 0
for _, _, width in utf8_iter(text) do for _, _, length in utf8_iter(text) do
text_width = text_width + width text_length = text_length + length
end end
return text_width * font_size * options.font_height_to_letter_width_ratio return text_length * font_size * options.font_height_to_letter_width_ratio
end end
function utf8_iter(string) function utf8_iter(string)
@@ -405,18 +405,18 @@ function utf8_iter(string)
end end
end end
function wrap_text(text, line_width_requested) function wrap_text(text, target_line_length)
local line_width = 0 local line_length = 0
local wrap_at_chars = {' ', ' ', '-', ''} local wrap_at_chars = {' ', ' ', '-', ''}
local remove_when_wrap = {' ', ' '} local remove_when_wrap = {' ', ' '}
local lines = {} local lines = {}
local line_start = 1 local line_start = 1
local before_end = nil local before_end = nil
local before_width = 0 local before_length = 0
local before_line_start = 0 local before_line_start = 0
local before_removed_width = 0 local before_removed_length = 0
local max_width = 0 local max_length = 0
for char_start, count, char_width in utf8_iter(text) do for char_start, count, char_length in utf8_iter(text) do
local char_end = char_start + count - 1 local char_end = char_start + count - 1
local char = text.sub(text, char_start, char_end) local char = text.sub(text, char_start, char_end)
local can_wrap = false local can_wrap = false
@@ -426,7 +426,7 @@ function wrap_text(text, line_width_requested)
break break
end end
end end
line_width = line_width + char_width line_length = line_length + char_length
if can_wrap or (char_end == #text) then if can_wrap or (char_end == #text) then
local remove = false local remove = false
for _, c in ipairs(remove_when_wrap) do for _, c in ipairs(remove_when_wrap) do
@@ -435,36 +435,36 @@ function wrap_text(text, line_width_requested)
break break
end end
end end
local line_width_after_remove = line_width - (remove and char_width or 0) local line_length_after_remove = line_length - (remove and char_length or 0)
if line_width_after_remove < line_width_requested then if line_length_after_remove < target_line_length then
before_end = remove and char_start - 1 or char_end before_end = remove and char_start - 1 or char_end
before_width = line_width_after_remove before_length = line_length_after_remove
before_line_start = char_end + 1 before_line_start = char_end + 1
before_removed_width = remove and char_width or 0 before_removed_length = remove and char_length or 0
else else
if (line_width_requested - before_width) < if (target_line_length - before_length) <
(line_width_after_remove - line_width_requested) then (line_length_after_remove - target_line_length) then
lines[#lines + 1] = text.sub(text, line_start, before_end) lines[#lines + 1] = text.sub(text, line_start, before_end)
line_start = before_line_start line_start = before_line_start
line_width = line_width - before_width - before_removed_width line_length = line_length - before_length - before_removed_length
if before_width > max_width then max_width = before_width end if before_length > max_length then max_length = before_length end
else else
lines[#lines + 1] = text.sub(text, line_start, remove and char_start - 1 or char_end) lines[#lines + 1] = text.sub(text, line_start, remove and char_start - 1 or char_end)
line_start = char_end + 1 line_start = char_end + 1
line_width = remove and line_width - char_width or line_width line_length = remove and line_length - char_length or line_length
if line_width > max_width then max_width = line_width end if line_length > max_length then max_length = line_length end
line_width = 0 line_length = 0
end end
before_end = line_start before_end = line_start
before_width = 0 before_length = 0
end end
end end
end end
if #text >= line_start then if #text >= line_start then
lines[#lines + 1] = string.sub(text, line_start) lines[#lines + 1] = string.sub(text, line_start)
if line_width > max_width then max_width = line_width end if line_length > max_length then max_length = line_length end
end end
return table.concat(lines, '\n'), max_width return table.concat(lines, '\n'), max_length
end end
-- Escape a string for verbatim display on the OSD -- Escape a string for verbatim display on the OSD
@@ -723,6 +723,25 @@ function ass_mt:txt(x, y, align, value, opts)
self.text = self.text .. '{' .. tags .. '}' .. value self.text = self.text .. '{' .. tags .. '}' .. value
end end
-- Tooltip
-- Draws text at center coordinate that shifts its position to not overflow the edges.
---@param x number
---@param y number
---@param align number
---@param value string|number
---@param opts? {size?: number; bold?: boolean; italic?: boolean; text_length_override?: number}
function ass_mt:tooltip(x, y, align, value, opts)
opts = opts or {}
opts.size = opts.size or 16
opts.border = 1
opts.border_color = options.color_background
local text_width = opts.text_length_override
and opts.text_length_override * opts.size * options.font_height_to_letter_width_ratio
or text_width_estimate(value, opts.size)
local margin = text_width / 2
self:txt(math.max(margin, math.min(x, display.width - margin)), y, align, value, opts)
end
-- Rectangle -- Rectangle
---@param ax number ---@param ax number
---@param ay number ---@param ay number
@@ -1817,8 +1836,7 @@ function render_timeline(this)
if (this.proximity_raw == 0 or this.pressed) and not (Elements.speed and Elements.speed.dragging) then if (this.proximity_raw == 0 or this.pressed) and not (Elements.speed and Elements.speed.dragging) then
-- add 0.5 to be in the middle of the pixel -- add 0.5 to be in the middle of the pixel
local hovered_seconds = this:get_time_at_x(cursor.x + 0.5) local hovered_seconds = this:get_time_at_x(cursor.x + 0.5)
local chapter_title = '' local chapter_title, chapter_title_width = nil, nil
local chapter_title_width = 0
if (options.timeline_chapters ~= 'never' and state.chapters) then if (options.timeline_chapters ~= 'never' and state.chapters) then
for i = #state.chapters, 1, -1 do for i = #state.chapters, 1, -1 do
@@ -1833,23 +1851,15 @@ function render_timeline(this)
end end
end end
local time_formatted = format_time(hovered_seconds)
local margin_time = text_width_estimate(time_formatted, this.font_size) / 2
local margin_title = chapter_title_width * this.font_size * options.font_height_to_letter_width_ratio / 2
local opacity = math.min(options.timeline_opacity + 0.1, 1)
-- Chapter title -- Chapter title
ass:txt(math.min(math.max(cursor.x, margin_title), display.width - margin_title), fay - this.font_size * 1.3, if chapter_title then
2, chapter_title, { ass:tooltip(cursor.x, fay - this.font_size * 1.4, 2, chapter_title, {
size = this.font_size, color = options.color_background_text, bold = true, size = this.font_size, bold = true, text_length_override = chapter_title_width,
border = 1, border_color = options.color_background, opacity = opacity, })
}) end
-- Timestamp -- Timestamp
ass:txt(math.min(math.max(cursor.x, margin_time), display.width - margin_time), fay, ass:tooltip(cursor.x, fay - 2, 2, format_time(hovered_seconds), {size = this.font_size})
2, time_formatted, {
size = this.font_size, color = options.color_background_text,
border = 1, border_color = options.color_background, opacity = opacity,
})
-- Cursor line -- Cursor line
-- 0.5 to switch when the pixel is half filled in -- 0.5 to switch when the pixel is half filled in
@@ -1908,7 +1918,7 @@ function render_top_bar(this)
end end
-- Window title -- Window title
if options.top_bar_title and (state.media_title or state.playlist_count > 1 ) then if options.top_bar_title and (state.media_title or state.playlist_count > 1) then
local max_bx = this.title_bx - this.spacing local max_bx = this.title_bx - this.spacing
local text = state.media_title or 'n/a' local text = state.media_title or 'n/a'
if state.playlist_count > 1 then if state.playlist_count > 1 then
@@ -1921,7 +1931,7 @@ function render_top_bar(this)
local bg_ax = this.ax + bg_margin local bg_ax = this.ax + bg_margin
local bg_bx = math.min(max_bx, this.ax + text_width_estimate(text, this.font_size) + padding * 2) local bg_bx = math.min(max_bx, this.ax + text_width_estimate(text, this.font_size) + padding * 2)
ass:rect(bg_ax, this.ay + bg_margin, bg_bx, this.by - bg_margin, { ass:rect(bg_ax, this.ay + bg_margin, bg_bx, this.by - bg_margin, {
color = options.color_background, opacity = visibility * 0.8, radius = 2 color = options.color_background, opacity = visibility * 0.8, radius = 2,
}) })
-- Text -- Text
@@ -2433,13 +2443,14 @@ end
-- Button -- Button
---@param id string ---@param id string
---@param props {icon: string; on_click: function; anchor_id?: string; active?: boolean; foreground?: string; background?: string} ---@param props {icon: string; on_click: function; anchor_id?: string; active?: boolean; foreground?: string; background?: string; tooltip?: string}
function create_button(id, props) function create_button(id, props)
return Element.new(id, { return Element.new(id, {
enabled = true, enabled = true,
anchor_id = props.anchor_id, anchor_id = props.anchor_id,
icon = props.icon, icon = props.icon,
active = props.active, active = props.active,
tooltip = props.tooltip,
foreground = props.foreground or options.color_foreground, foreground = props.foreground or options.color_foreground,
background = props.background or options.color_background, background = props.background or options.color_background,
set_coordinates = function(this, ax, ay, bx, by) set_coordinates = function(this, ax, ay, bx, by)
@@ -2463,13 +2474,22 @@ function create_button(id, props)
local foreground = this.active and this.background or this.foreground local foreground = this.active and this.background or this.foreground
local background = this.active and this.foreground or this.background local background = this.active and this.foreground or this.background
-- Background on hover -- Background
if is_hover_or_active then if is_hover_or_active then
ass:rect(this.ax, this.ay, this.bx, this.by, { ass:rect(this.ax, this.ay, this.bx, this.by, {
color = this.active and background or foreground, opacity = visibility * (this.active and 0.8 or 0.2), radius = 2, color = this.active and background or foreground, radius = 2,
opacity = visibility * (this.active and 0.8 or 0.2),
}) })
end end
-- Tooltip on hover
if is_hover and this.tooltip then
ass:tooltip(
this.ax + (this.bx - this.ax) / 2, this.ay - this.font_size / 2, this.ay > 100 and 2 or 8,
this.tooltip
)
end
-- Icon -- Icon
local x, y = round(this.ax + (this.bx - this.ax) / 2), round(this.ay + (this.by - this.ay) / 2) local x, y = round(this.ax + (this.bx - this.ax) / 2), round(this.ay + (this.by - this.ay) / 2)
ass:icon(x, y, this.font_size, this.icon, { ass:icon(x, y, this.font_size, this.icon, {
@@ -2484,13 +2504,13 @@ end
-- Cycle prop button -- Cycle prop button
---@alias CycleState {value: any; icon: string; active?: boolean} ---@alias CycleState {value: any; icon: string; active?: boolean}
---@param id string ---@param id string
---@param props {prop: string; states: CycleState[]; anchor_id?: string;} ---@param props {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string}
function create_cycle_button(id, props) function create_cycle_button(id, props)
local prop = props.prop local prop = props.prop
local states = props.states local states = props.states
local current_state_index = 1 local current_state_index = 1
local button = create_button(id, { local button = create_button(id, {
anchor_id = props.anchor_id, icon = states[1].icon, active = states[1].active, anchor_id = props.anchor_id, icon = states[1].icon, active = states[1].active, tooltip = props.tooltip,
on_click = function() on_click = function()
local new_state = states[current_state_index + 1] or states[1] local new_state = states[current_state_index + 1] or states[1]
mp.set_property(prop, new_state.value) mp.set_property(prop, new_state.value)
@@ -2784,24 +2804,24 @@ if options.controls and options.controls ~= 'never' then
init = function(this) this:serialize() end, init = function(this) this:serialize() end,
serialize = function(this) serialize = function(this)
local shorthands = { local shorthands = {
menu = 'command:menu:script-binding uosc/menu', menu = 'command:menu:script-binding uosc/menu?Menu',
subtitles = 'command:subtitles:script-binding uosc/subtitles', subtitles = 'command:subtitles:script-binding uosc/subtitles?Subtitles',
audio = 'command:audiotrack:script-binding uosc/audio', audio = 'command:audiotrack:script-binding uosc/audio?Audio',
['audio-device'] = 'command:speaker:script-binding uosc/audio-device', ['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device',
video = 'command:theaters:script-binding uosc/video', video = 'command:theaters:script-binding uosc/video?Video',
playlist = 'command:list_alt:script-binding uosc/playlist', playlist = 'command:list_alt:script-binding uosc/playlist?Playlist',
chapters = 'command:bookmarks:script-binding uosc/chapters', chapters = 'command:bookmarks:script-binding uosc/chapters?Chapters',
['stream-quality'] = 'command:deblur:script-binding uosc/stream-quality', ['stream-quality'] = 'command:deblur:script-binding uosc/stream-quality?Stream quality',
['open-file'] = 'command:file_open:script-binding uosc/open-file', ['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file',
['items'] = 'command:list_alt:script-binding uosc/items', ['items'] = 'command:list_alt:script-binding uosc/items?Playlist/Files',
prev = 'command:arrow_back_ios:script-binding uosc/prev', prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous',
next = 'command:arrow_forward_ios:script-binding uosc/next', next = 'command:arrow_forward_ios:script-binding uosc/next?Next',
first = 'command:first_page:script-binding uosc/first', first = 'command:first_page:script-binding uosc/first?First',
last = 'command:last_page:script-binding uosc/last', last = 'command:last_page:script-binding uosc/last?Last',
['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!', ['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?Loop playlist',
['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!', ['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?Loop file',
shuffle = 'toggle:shuffle:shuffle', shuffle = 'toggle:shuffle:shuffle?Shuffle',
fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!', fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen',
} }
-- Parse configs -- Parse configs
@@ -2839,6 +2859,9 @@ if options.controls and options.controls ~= 'never' then
this.controls = {} this.controls = {}
for i, item in ipairs(items) do for i, item in ipairs(items) do
local config = shorthands[item.config] and shorthands[item.config] or item.config local config = shorthands[item.config] and shorthands[item.config] or item.config
local config_tooltip = split(config, ' *%? *')
config = config_tooltip[1]
local tooltip = config_tooltip[2]
local parts = split(config, ' *: *') local parts = split(config, ' *: *')
local kind, params = parts[1], itable_slice(parts, 2) local kind, params = parts[1], itable_slice(parts, 2)
@@ -2865,6 +2888,7 @@ if options.controls and options.controls ~= 'never' then
icon = params[1], icon = params[1],
anchor_id = 'controls', anchor_id = 'controls',
on_click = function() mp.command(params[2]) end, on_click = function() mp.command(params[2]) end,
tooltip = tooltip,
}) })
this.controls[#this.controls + 1] = { this.controls[#this.controls + 1] = {
kind = kind, element = element, sizing = 'static', scale = 1, ratio = 1, kind = kind, element = element, sizing = 'static', scale = 1, ratio = 1,
@@ -2893,7 +2917,7 @@ if options.controls and options.controls ~= 'never' then
end end
local element = create_cycle_button('control_' .. i, { local element = create_cycle_button('control_' .. i, {
prop = params[2], anchor_id = 'controls', states = states, prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip,
}) })
this.controls[#this.controls + 1] = { this.controls[#this.controls + 1] = {
kind = kind, element = element, sizing = 'static', scale = 1, ratio = 1, kind = kind, element = element, sizing = 'static', scale = 1, ratio = 1,