summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--catalogue.lua3
-rw-r--r--input.lua336
-rw-r--r--main.lua421
-rw-r--r--osd.lua8
-rw-r--r--rt.lua105
5 files changed, 455 insertions, 418 deletions
diff --git a/catalogue.lua b/catalogue.lua
index 5444d17..cf9d897 100644
--- a/catalogue.lua
+++ b/catalogue.lua
@@ -102,7 +102,8 @@ function mt:load_xc_section(section)
for _, v in ipairs(section.elements) do
local vv = {
section = section.id,
- parent_id = section.id .. ':category:' .. v.category_id,
+ parent_id = section.id .. ':category:' ..
+ v.category_id,
name = util.strip(v.name),
}
diff --git a/input.lua b/input.lua
new file mode 100644
index 0000000..7d4ea57
--- /dev/null
+++ b/input.lua
@@ -0,0 +1,336 @@
+-- Copyright 2025 David Vazgenovich Shakaryan
+
+local config = require('config')
+local rt = require('rt')
+
+local input = {}
+
+local mappings = {}
+local active_mapping = {}
+local mapping_bound = false
+
+local click_state = {}
+
+local state
+local osd
+
+function input.init(_state, _osd, _bs)
+ state = _state
+ osd = _osd
+end
+
+local function mouse_has_drifted(x1, y1, x2, y2)
+ return math.abs(x1 - x2) > config.click_max_drift or
+ math.abs(y1 - y2) > config.click_max_drift
+end
+
+-- when mpv registers a double-click, the second click triggers both
+-- double-click and click events. instead of identifying and ignoring this
+-- second click event, we can implement double-click detection ourselves for
+-- more control and to bypass some mpv inconsistencies.
+local function handle_mouse_click(ev, id)
+ if ev.canceled then
+ click_state = {}
+ return
+ end
+
+ local mpos = osd.mpos
+ if not mpos then
+ return
+ end
+
+ local time = mp.get_time()
+ local clk = click_state
+
+ if ev.event == 'down' then
+ clk.ct, clk.ck = time, ev.key_name
+ clk.cx, clk.cy, clk.ci = mpos.x, mpos.y, id
+ return
+ end
+
+ if ev.event ~= 'up' or not clk.ct then
+ return
+ end
+
+ if time - clk.ct > config.click_timeout or
+ clk.ck ~= ev.key_name or clk.ci ~= id or
+ mouse_has_drifted(mpos.x, mpos.y, clk.cx, clk.cy) then
+ click_state = {}
+ return
+ end
+
+ local dbl = clk.pt and clk.ct - clk.pt <= config.click_dbl_time and
+ clk.pk == ev.key_name and clk.pi == id and
+ not mouse_has_drifted(clk.cx, clk.cy, clk.px, clk.py)
+ click_state = dbl and {} or
+ {
+ pt = clk.ct, pk = ev.key_name,
+ px = clk.cx, py = clk.cy, pi = id,
+ }
+ return ev.key_name, dbl
+end
+
+local function process_mouse_click(ev)
+ local ms = osd.mstate
+ if not ms then
+ return
+ end
+
+ local area
+ local val
+ if ms.target == 'scrollbar' then
+ area = 'scrollbar'
+ val = ms.ratio
+ elseif ms.target == 'menu' then
+ area = 'menu'
+ val = ms.option_line
+ end
+
+ local key, dbl = handle_mouse_click(ev, ms.hitbox)
+ return key, dbl, area, val
+end
+
+local function mouse_click(ev)
+ local key, dbl, area, val = process_mouse_click(ev)
+ if not key then
+ return
+ end
+
+ local f
+ if key == 'MBTN_LEFT' then
+ f = rt.mouse_click_left
+ elseif key == 'MBTN_RIGHT' then
+ f = rt.mouse_click_right
+ end
+
+ f(dbl, area, val)
+end
+
+-- mpv does not process key-binding changes requested by script functions until
+-- those functions return. in the meantime, while the function is running, mpv
+-- discards any key presses for keys that were not already bound prior to the
+-- start of function execution. similarly, any pending key presses (for keys
+-- that were bound) are discarded if the binding changes during function
+-- execution.
+--
+-- in other words, we get an inconsistent result where pressing a key while a
+-- function is running triggers the bound function upon completion if that
+-- binding remains the same, but ignores such key presses when our function
+-- changes the corresponding binding or adds a new one. we can avoid this by
+-- leaving our keys bound to a common function and building our own logic to
+-- route keys per the current state.
+local function handle_key(ev)
+ local t = active_mapping[ev.key_name]
+ if not t and ev.key_text then
+ t = active_mapping['ANY_UNICODE']
+ end
+
+ -- ev.is_mouse is false for some mouse events
+ local k = ev.key_name
+ if k:find('MOUSE_') or k:find('MBTN_') or k:find('WHEEL_') then
+ if not osd.mactive then
+ osd:set_mactive(true)
+ end
+ elseif osd.mactive then
+ osd:set_mactive(false)
+ click_state = {}
+ end
+
+ local f = t and t[1]
+ if not f then
+ goto flush
+ end
+
+ if t[2] == 'complex' then
+ f(ev)
+ elseif ev.event == 'down' or
+ (ev.event == 'repeat' and t[2] == 'repeat') then
+ f()
+ end
+
+ ::flush::
+ osd:flush(state)
+end
+
+-- uses enable-section and disable-section to disable builtin key bindings
+-- while the OSD is visible. these commands are technically deprecated for
+-- non-internal use, but they still work, and there doesn't appear to be
+-- another way apart from setting an override for each individual key.
+--
+-- nonexistent `nodrag' section is enabled to disable VO dragging while the
+-- menu is open.
+function input.set_key_bindings()
+ if osd:is_hidden() then
+ if mapping_bound then
+ mp.remove_key_binding('mouse_move')
+ mp.remove_key_binding('unmapped')
+ mp.command_native({'disable-section', 'nodrag'})
+ mp.command_native({
+ 'enable-section', 'default',
+ 'allow-hide-cursor+allow-vo-dragging'})
+ mapping_bound = false
+ end
+ return
+ end
+
+ if not mapping_bound then
+ mp.command_native({'disable-section', 'default'})
+ mp.command_native({'enable-section', 'nodrag'})
+ mp.add_forced_key_binding('MOUSE_MOVE', 'mouse_move') -- noisy
+ mp.add_forced_key_binding(
+ 'UNMAPPED', 'unmapped', handle_key, {complex = true})
+ mapping_bound = true
+ end
+end
+
+local function bind_click(f)
+ mp.command_native({'enable-section', 'click-nodrag'})
+ mp.add_forced_key_binding(
+ 'MBTN_LEFT', 'click', function(ev)
+ if process_mouse_click(ev) then
+ f()
+ end
+ end, {complex = true})
+ mp.add_forced_key_binding('MBTN_LEFT_DBL', 'click-dbl')
+end
+
+local function unbind_click()
+ mp.remove_key_binding('click')
+ mp.remove_key_binding('click-dbl')
+ mp.command_native({'disable-section', 'click-nodrag'})
+end
+
+function input.set_key_mapping(m)
+ active_mapping = mappings[m]
+end
+
+local btn_timer; btn_timer = mp.add_periodic_timer(0.1, function()
+ if osd.mpos and mp.get_time() - osd.mpos_time > config.btn_timeout then
+ osd:show_menu_btn(false)
+ btn_timer:kill()
+ end
+end, true) -- disabled
+function input.update_mpos(mpos)
+ if not mpos then
+ return
+ end
+
+ local ps = osd.mstate or {}
+ local ms = osd:set_mpos(mpos)
+ if not ms then
+ return
+ end
+
+ -- mpv normally sends a cancel event when the mouse drifts before a
+ -- click is released, but only for a first click and not for the second
+ -- click of what mpv internally considers a double-click. this
+ -- replicates that drift detection behaviour, allowing for consistency
+ -- on double-clicks.
+ local clk = click_state
+ if clk.ct and mouse_has_drifted(mpos.x, mpos.y, clk.cx, clk.cy) then
+ click_state = {}
+ end
+
+ if not ms.over_btn_area ~= not ps.over_btn_area then
+ osd:show_menu_btn(ms.over_btn_area)
+ elseif ms.over_btn_area and osd.menu_btn.hidden then
+ osd:show_menu_btn(true)
+ end
+
+ if (ms.target == 'menu_btn') ~= (ps.target == 'menu_btn') then
+ if ms.target == 'menu_btn' then
+ bind_click(rt.toggle_menu)
+ else
+ unbind_click()
+ end
+ end
+
+ if ms.over_btn_area and ms.target ~= 'menu_btn' then
+ btn_timer:resume()
+ else
+ btn_timer:kill()
+ end
+
+ osd:flush(state)
+end
+
+-- after a resize, mpv does not update the mouse coordinates until the mouse is
+-- moved. if the mouse was previously over a clickable element and we do
+-- nothing, a click would trigger that element regardless of mouse position.
+-- since we cannot get the new position, we instead wipe mouse state on resize,
+-- clearing it until the next move.
+function input.on_resize()
+ osd:show_menu_btn(false)
+ osd:set_mpos(nil)
+ click_state = {}
+ unbind_click()
+ btn_timer:kill()
+end
+
+mappings.MENU = {
+ ['BS'] = {rt.prev_menu},
+ ['/'] = {rt.start_search},
+ ['Ctrl+s'] = {rt.toggle_menu_sort},
+ ['Ctrl+R'] = {rt.reload_data},
+
+ ['ENTER'] = {rt.select_option},
+ ['Ctrl+f'] = {rt.favourite_option},
+ ['g'] = {rt.goto_option},
+ ['i'] = {rt.open_option_info},
+ ['?'] = {rt.open_option_info},
+ ['Ctrl+p'] = {rt.goto_playing},
+
+ ['MBTN_LEFT'] = {mouse_click, 'complex'},
+ ['MBTN_RIGHT'] = {mouse_click, 'complex'},
+
+ ['k'] = {rt.cursor_up, 'repeat'},
+ ['j'] = {rt.cursor_down, 'repeat'},
+ ['K'] = {rt.cursor_page_up, 'repeat'},
+ ['J'] = {rt.cursor_page_down, 'repeat'},
+ ['UP'] = {rt.cursor_up, 'repeat'},
+ ['DOWN'] = {rt.cursor_down, 'repeat'},
+ ['Shift+UP'] = {rt.cursor_page_up, 'repeat'},
+ ['Shift+DOWN'] = {rt.cursor_page_down, 'repeat'},
+ ['PGUP'] = {rt.cursor_page_up, 'repeat'},
+ ['PGDWN'] = {rt.cursor_page_down, 'repeat'},
+ ['HOME'] = {rt.cursor_start},
+ ['END'] = {rt.cursor_end},
+ ['WHEEL_UP'] = {rt.cursor_wheel_up, 'repeat'},
+ ['WHEEL_DOWN'] = {rt.cursor_wheel_down, 'repeat'},
+ ['Shift+WHEEL_UP'] = {rt.cursor_wheel_page_up, 'repeat'},
+ ['Shift+WHEEL_DOWN'] = {rt.cursor_wheel_page_down, 'repeat'},
+
+ ['Alt+k'] = {rt.move_option_up, 'repeat'},
+ ['Alt+j'] = {rt.move_option_down, 'repeat'},
+ ['Alt+K'] = {rt.move_option_page_up, 'repeat'},
+ ['Alt+J'] = {rt.move_option_page_down, 'repeat'},
+ ['Alt+UP'] = {rt.move_option_up, 'repeat'},
+ ['Alt+DOWN'] = {rt.move_option_down, 'repeat'},
+ ['Shift+Alt+UP'] = {rt.move_option_page_up, 'repeat'},
+ ['Shift+Alt+DOWN'] = {rt.move_option_page_down, 'repeat'},
+ ['Alt+PGUP'] = {rt.move_option_page_up, 'repeat'},
+ ['Alt+PGDWN'] = {rt.move_option_page_down, 'repeat'},
+ ['Alt+HOME'] = {rt.move_option_start},
+ ['Alt+END'] = {rt.move_option_end},
+ ['Alt+WHEEL_UP'] = {rt.move_option_wheel_up, 'repeat'},
+ ['Alt+WHEEL_DOWN'] = {rt.move_option_wheel_down, 'repeat'},
+ ['Shift+Alt+WHEEL_UP'] = {rt.move_option_wheel_page_up, 'repeat'},
+ ['Shift+Alt+WHEEL_DOWN'] = {rt.move_option_wheel_page_down, 'repeat'},
+}
+
+mappings.SEARCH = {
+ ['ANY_UNICODE'] = {rt.search_input_char, 'complex'},
+ ['BS'] = {rt.search_input_bs, 'repeat'},
+ ['DEL'] = {rt.search_input_del, 'repeat'},
+
+ ['ENTER'] = {rt.end_search},
+ ['ESC'] = {rt.cancel_search},
+ ['Ctrl+c'] = {rt.cancel_search},
+
+ ['LEFT'] = {rt.search_cursor_left, 'repeat'},
+ ['RIGHT'] = {rt.search_cursor_right, 'repeat'},
+ ['Ctrl+a'] = {rt.search_cursor_start},
+ ['Ctrl+e'] = {rt.search_cursor_end},
+}
+
+return input
diff --git a/main.lua b/main.lua
index ea11ebf..f7ceace 100644
--- a/main.lua
+++ b/main.lua
@@ -2,6 +2,7 @@
local cacher = require('cacher')
local config = require('config')
+local input = require('input')
local rt = require('rt')
local util = require('util')
local _catalogue = require('catalogue')
@@ -16,9 +17,7 @@ local mp_utils = require('mp.utils')
local script_name = mp.get_script_name()
local state = _state.new()
-local binding_state = {mappings = {}, active = {}}
local click_state = {}
-local osc_visibility
local downloader = _downloader.new({limit = 5})
local xc = _xc.new({
@@ -46,13 +45,6 @@ xc = cacher.wrap(xc, {
local catalogue = _catalogue.new()
local epg = _epg.new()
-local ctx = {
- binding_state = binding_state,
- catalogue = catalogue,
- epg = epg,
- xc = xc,
-}
-
local osd
local function dl_img(url, path, cb)
downloader:schedule(url, path, function(success, _, path)
@@ -80,410 +72,27 @@ osd = _osd.new({
end
})
-rt.init(state, osd, ctx)
-
-local function mouse_has_drifted(x1, y1, x2, y2)
- return math.abs(x1 - x2) > config.click_max_drift or
- math.abs(y1 - y2) > config.click_max_drift
-end
-
--- when mpv registers a double-click, the second click triggers both
--- double-click and click events. instead of identifying and ignoring this
--- second click event, we can implement double-click detection ourselves for
--- more control and to bypass some mpv inconsistencies.
-local function handle_mouse_click(ev, id)
- if ev.canceled then
- click_state = {}
- return
- end
-
- local mpos = osd.mpos
- if not mpos then
- return
- end
-
- local time = mp.get_time()
- local clk = click_state
-
- if ev.event == 'down' then
- clk.ct, clk.ck = time, ev.key_name
- clk.cx, clk.cy, clk.ci = mpos.x, mpos.y, id
- return
- end
-
- if ev.event ~= 'up' or not clk.ct then
- return
- end
-
- if time - clk.ct > config.click_timeout or
- clk.ck ~= ev.key_name or clk.ci ~= id or
- mouse_has_drifted(mpos.x, mpos.y, clk.cx, clk.cy) then
- click_state = {}
- return
- end
-
- local dbl = clk.pt and clk.ct - clk.pt <= config.click_dbl_time and
- clk.pk == ev.key_name and clk.pi == id and
- not mouse_has_drifted(clk.cx, clk.cy, clk.px, clk.py)
- click_state = dbl and {} or
- {
- pt = clk.ct, pk = ev.key_name,
- px = clk.cx, py = clk.cy, pi = id,
- }
- return ev.key_name, dbl
-end
-
-local function process_mouse_click(ev)
- local ms = osd.mstate
- if not ms then
- return
- end
-
- local area
- local val
- if ms.target == 'scrollbar' then
- area = 'scrollbar'
- val = ms.ratio
- elseif ms.target == 'menu' then
- area = 'menu'
- val = ms.option_line
- end
-
- local key, dbl = handle_mouse_click(ev, area == 'menu' and val or nil)
- return key, dbl, area, val
-end
-
-local function mouse_click_left_menu(dbl, line)
- if line == 0 then
- return
- end
-
- -- title
- if line < 0 then
- if not dbl then
- return
- end
-
- if line == -1 then
- rt.set_cursor(1)
- else
- state.depth = state.depth + line + 1
- osd:dirty()
- end
- return
- end
-
- local menu = state:menu()
- local pos = menu.view_top + line - 1
- if dbl then
- if pos ~= menu.cursor then
- return
- end
-
- rt.select_option()
- else
- if pos > #menu.options then
- return
- end
-
- rt.set_cursor(pos)
- end
-end
-
-local function mouse_click_left_scrollbar(dbl, ratio)
- if not dbl then
- return
- end
-
- -- set_cursor handles out-of-bounds moves (when ratio == 1)
- rt.set_cursor(
- math.floor(ratio * #state:menu().options) + 1,
- {centre = true})
-end
-
-local function mouse_click_left(ev)
- local key, dbl, area, val = process_mouse_click(ev)
- if not key then
- return
- end
-
- if area == 'menu' then
- return mouse_click_left_menu(dbl, val)
- elseif area == 'scrollbar' then
- return mouse_click_left_scrollbar(dbl, val)
- end
-end
-
-local function mouse_click_right_menu(dbl, line)
- if not dbl or line < 1 then
- return
- end
-
- local menu = state:menu()
- local pos = menu.view_top + line - 1
- if pos > #menu.options then
- return
- end
-
- rt.open_option_info(menu.options[pos])
-end
-
-local function mouse_click_right(ev)
- local key, dbl, area, val = process_mouse_click(ev)
- if not key then
- return
- end
-
- if area == 'menu' then
- return mouse_click_right_menu(dbl, val)
- end
-end
-
-binding_state.mappings.MENU = {
- ['BS'] = {rt.prev_menu},
- ['/'] = {rt.start_search},
- ['Ctrl+s'] = {rt.toggle_menu_sort},
- ['Ctrl+R'] = {rt.reload_data},
-
- ['ENTER'] = {rt.select_option},
- ['Ctrl+f'] = {rt.favourite_option},
- ['g'] = {rt.goto_option},
- ['i'] = {rt.open_option_info},
- ['?'] = {rt.open_option_info},
- ['Ctrl+p'] = {rt.goto_playing},
-
- ['MBTN_LEFT'] = {rt.mouse_click_left, 'complex'},
- ['MBTN_RIGHT'] = {rt.mouse_click_right, 'complex'},
-
- ['k'] = {rt.cursor_up, 'repeat'},
- ['j'] = {rt.cursor_down, 'repeat'},
- ['K'] = {rt.cursor_page_up, 'repeat'},
- ['J'] = {rt.cursor_page_down, 'repeat'},
- ['UP'] = {rt.cursor_up, 'repeat'},
- ['DOWN'] = {rt.cursor_down, 'repeat'},
- ['Shift+UP'] = {rt.cursor_page_up, 'repeat'},
- ['Shift+DOWN'] = {rt.cursor_page_down, 'repeat'},
- ['PGUP'] = {rt.cursor_page_up, 'repeat'},
- ['PGDWN'] = {rt.cursor_page_down, 'repeat'},
- ['HOME'] = {rt.cursor_start},
- ['END'] = {rt.cursor_end},
- ['WHEEL_UP'] = {rt.cursor_wheel_up, 'repeat'},
- ['WHEEL_DOWN'] = {rt.cursor_wheel_down, 'repeat'},
- ['Shift+WHEEL_UP'] = {rt.cursor_wheel_page_up, 'repeat'},
- ['Shift+WHEEL_DOWN'] = {rt.cursor_wheel_page_down, 'repeat'},
-
- ['Alt+k'] = {rt.move_option_up, 'repeat'},
- ['Alt+j'] = {rt.move_option_down, 'repeat'},
- ['Alt+K'] = {rt.move_option_page_up, 'repeat'},
- ['Alt+J'] = {rt.move_option_page_down, 'repeat'},
- ['Alt+UP'] = {rt.move_option_up, 'repeat'},
- ['Alt+DOWN'] = {rt.move_option_down, 'repeat'},
- ['Shift+Alt+UP'] = {rt.move_option_page_up, 'repeat'},
- ['Shift+Alt+DOWN'] = {rt.move_option_page_down, 'repeat'},
- ['Alt+PGUP'] = {rt.move_option_page_up, 'repeat'},
- ['Alt+PGDWN'] = {rt.move_option_page_down, 'repeat'},
- ['Alt+HOME'] = {rt.move_option_start},
- ['Alt+END'] = {rt.move_option_end},
- ['Alt+WHEEL_UP'] = {rt.move_option_wheel_up, 'repeat'},
- ['Alt+WHEEL_DOWN'] = {rt.move_option_wheel_down, 'repeat'},
- ['Shift+Alt+WHEEL_UP'] = {rt.move_option_wheel_page_up, 'repeat'},
- ['Shift+Alt+WHEEL_DOWN'] = {rt.move_option_wheel_page_down, 'repeat'},
-}
-
-binding_state.mappings.SEARCH = {
- ['ANY_UNICODE'] = {rt.search_input_char, 'complex'},
- ['BS'] = {rt.search_input_bs, 'repeat'},
- ['DEL'] = {rt.search_input_del, 'repeat'},
-
- ['ENTER'] = {rt.end_search},
- ['ESC'] = {rt.cancel_search},
- ['Ctrl+c'] = {rt.cancel_search},
-
- ['LEFT'] = {rt.search_cursor_left, 'repeat'},
- ['RIGHT'] = {rt.search_cursor_right, 'repeat'},
- ['Ctrl+a'] = {rt.search_cursor_start},
- ['Ctrl+e'] = {rt.search_cursor_end},
+local ctx = {
+ catalogue = catalogue,
+ epg = epg,
+ xc = xc,
}
+rt.init(state, osd, input, ctx)
--- mpv does not process key-binding changes requested by script functions until
--- those functions return. in the meantime, while the function is running, mpv
--- discards any key presses for keys that were not already bound prior to the
--- start of function execution. similarly, any pending key presses (for keys
--- that were bound) are discarded if the binding changes during function
--- execution.
---
--- in other words, we get an inconsistent result where pressing a key while a
--- function is running triggers the bound function upon completion if that
--- binding remains the same, but ignores such key presses when our function
--- changes the corresponding binding or adds a new one. we can avoid this by
--- leaving our keys bound to a common function and building our own logic to
--- route keys per the current state.
-local function handle_key(ev)
- local t = binding_state.active[ev.key_name]
- if not t and ev.key_text then
- t = binding_state.active['ANY_UNICODE']
- end
-
- -- ev.is_mouse is false for some mouse events
- local k = ev.key_name
- if k:find('MOUSE_') or k:find('MBTN_') or k:find('WHEEL_') then
- if not osd.mactive then
- osd:set_mactive(true)
- end
- elseif osd.mactive then
- osd:set_mactive(false)
- click_state = {}
- end
-
- local f = t and t[1]
- if not f then
- goto flush
- end
-
- if t[2] == 'complex' then
- f(ev)
- elseif ev.event == 'down' or
- (ev.event == 'repeat' and t[2] == 'repeat') then
- f()
- end
-
- ::flush::
- osd:flush(state)
-end
-
--- uses enable-section and disable-section to disable builtin key bindings
--- while the OSD is visible. these commands are technically deprecated for
--- non-internal use, but they still work, and there doesn't appear to be
--- another way apart from setting an override for each individual key.
---
--- nonexistent `nodrag' section is enabled to disable VO dragging while the
--- menu is open.
-local function set_key_bindings()
- if osd:is_hidden() then
- if binding_state.bound then
- mp.remove_key_binding('mouse_move')
- mp.remove_key_binding('unmapped')
- mp.command_native({'disable-section', 'nodrag'})
- mp.command_native({
- 'enable-section', 'default',
- 'allow-hide-cursor+allow-vo-dragging'})
- binding_state.bound = false
- end
- return
- end
-
- if not binding_state.bound then
- mp.command_native({'disable-section', 'default'})
- mp.command_native({'enable-section', 'nodrag'})
- mp.add_forced_key_binding('MOUSE_MOVE', 'mouse_move') -- noisy
- mp.add_forced_key_binding(
- 'UNMAPPED', 'unmapped', handle_key, {complex = true})
- binding_state.bound = true
- end
-end
-
-local function set_osc_visibility()
- local v = osd:is_hidden() and osc_visibility or 'never'
- mp.command_native({'script-message', 'osc-visibility', v, ''})
-end
-
-local function toggle_menu()
- osd:toggle_hidden()
-
- if osc_visibility ~= 'never' then
- set_osc_visibility()
- end
-
- set_key_bindings()
-end
-
-local function bind_click(f)
- mp.command_native({'enable-section', 'click-nodrag'})
- mp.add_forced_key_binding(
- 'MBTN_LEFT', 'click', function(ev)
- if process_mouse_click(ev) then
- f()
- end
- end, {complex = true})
- mp.add_forced_key_binding('MBTN_LEFT_DBL', 'click-dbl')
-end
-
-local function unbind_click()
- mp.remove_key_binding('click')
- mp.remove_key_binding('click-dbl')
- mp.command_native({'disable-section', 'click-nodrag'})
-end
-
-local btn_timer; btn_timer = mp.add_periodic_timer(0.1, function()
- if osd.mpos and mp.get_time() - osd.mpos_time > config.btn_timeout then
- osd:show_menu_btn(false)
- btn_timer:kill()
- end
-end, true) -- disabled
mp.observe_property('mouse-pos', 'native', function(_, mpos)
- if not mpos then
- return
- end
-
- local ps = osd.mstate or {}
- local ms = osd:set_mpos(mpos)
- if not ms then
- return
- end
-
- -- mpv normally sends a cancel event when the mouse drifts before a
- -- click is released, but only for a first click and not for the second
- -- click of what mpv internally considers a double-click. this
- -- replicates that drift detection behaviour, allowing for consistency
- -- on double-clicks.
- local clk = click_state
- if clk.ct and mouse_has_drifted(mpos.x, mpos.y, clk.cx, clk.cy) then
- click_state = {}
- end
-
- if not ms.over_btn_area ~= not ps.over_btn_area then
- osd:show_menu_btn(ms.over_btn_area)
- elseif ms.over_btn_area and osd.menu_btn.hidden then
- osd:show_menu_btn(true)
- end
-
- if (ms.target == 'menu_btn') ~= (ps.target == 'menu_btn') then
- if ms.target == 'menu_btn' then
- bind_click(toggle_menu)
- else
- unbind_click()
- end
- end
-
- if ms.over_btn_area and ms.target ~= 'menu_btn' then
- btn_timer:resume()
- else
- btn_timer:kill()
- end
-
- osd:flush(state)
+ input.update_mpos(mpos)
end)
mp.observe_property('user-data/osc/visibility', 'native', function(_, val)
if val and (osd:is_hidden() or val ~= 'never') then
- osc_visibility = val
+ state.saved_osc_visibility = val
end
end)
mp.observe_property('osd-dimensions', 'native', function(_, val)
osd:resize(val.w, val.h)
osd:redraw(state)
-
- -- after a resize, mpv does not update the mouse coordinates until the
- -- mouse is moved. if the mouse was previously over a clickable element
- -- and we do nothing, a click would trigger that element regardless of
- -- mouse position. since we cannot get the new position, we instead
- -- wipe mouse state on resize, clearing it until the next move.
- osd:show_menu_btn(false)
- osd:set_mpos(nil)
- click_state = {}
- unbind_click()
- btn_timer:kill()
+ input.on_resize()
end)
mp.register_event('start-file', function()
@@ -502,12 +111,14 @@ end)
state:push_menu({title = 'mpv-iptv-menu'})
-osc_visibility = mp.get_property_native('user-data/osc/visibility', 'auto')
-set_osc_visibility()
+state.saved_osc_visibility = mp.get_property_native(
+ 'user-data/osc/visibility', 'auto')
+rt.set_osc_visibility()
-mp.add_forced_key_binding('TAB', 'toggle-menu', toggle_menu)
-binding_state.active = binding_state.mappings['MENU']
-set_key_bindings()
+mp.add_forced_key_binding('TAB', 'toggle-menu', rt.toggle_menu)
+input.init(state, osd)
+input.set_key_mapping('MENU')
+input.set_key_bindings()
mp.add_timeout(0, function()
rt.load_data()
diff --git a/osd.lua b/osd.lua
index eb7feda..6a9f0c2 100644
--- a/osd.lua
+++ b/osd.lua
@@ -712,6 +712,7 @@ function mt:update_mstate(mactive)
val = self:mouse_menu_line(self.mpos)
if val then
t.target = 'menu'
+ t.hitbox = 'menu:' .. val
t.line = val
t.option_line = val - #self.out.titles - 1
goto common
@@ -719,6 +720,7 @@ function mt:update_mstate(mactive)
::common::
+ t.hitbox = t.hitbox or t.target
t.over_btn_area = self:mouse_over_btn_area(self.mpos)
::update::
@@ -727,10 +729,8 @@ function mt:update_mstate(mactive)
local t_hov = (t.target == 'menu' or t.target == 'scrollbar')
local ms_hov = (ms.target == 'menu' or ms.target == 'scrollbar')
if (t_hov or ms_hov) and
- (t.target ~= ms.target or
- not mactive ~= not self.mactive or
- (t.target == 'menu' and
- t.line ~= ms.line)) then
+ (t.hitbox ~= ms.hitbox or
+ not mactive ~= not self.mactive) then
self.needs_render_bg = true
end
diff --git a/rt.lua b/rt.lua
index f8b73c1..785a5c7 100644
--- a/rt.lua
+++ b/rt.lua
@@ -7,11 +7,13 @@ local rt = {}
local state
local osd
+local input
local ctx
-function rt.init(_state, _osd, _ctx)
+function rt.init(_state, _osd, _input, _ctx)
state = _state
osd = _osd
+ input = _input
ctx = _ctx
end
@@ -28,10 +30,6 @@ local function cache_miss_status_msg(str)
}
end
-local function set_key_mapping(m)
- ctx.binding_state.active = ctx.binding_state.mappings[m]
-end
-
function rt.load_data(force)
local arr = {
{id = 'live', name = 'Live TV', type = 'live'},
@@ -913,7 +911,7 @@ function rt.start_search()
end
osd:dirty()
- set_key_mapping('SEARCH')
+ input.set_key_mapping('SEARCH')
end
function rt.end_search()
@@ -922,7 +920,7 @@ function rt.end_search()
menu.title = 'Search results: <text>' ..
' <colour.info>(<num_matches>/<num_total>)'
osd:dirty()
- set_key_mapping('MENU')
+ input.set_key_mapping('MENU')
end
function rt.cancel_search()
@@ -940,7 +938,7 @@ function rt.cancel_search()
menu.search_active = false
state.depth = state.depth - 1
osd:dirty()
- set_key_mapping('MENU')
+ input.set_key_mapping('MENU')
end
function rt.toggle_menu_sort()
@@ -953,6 +951,82 @@ function rt.toggle_menu_sort()
osd:dirty()
end
+local function mouse_click_left_menu(dbl, line)
+ if line == 0 then
+ return
+ end
+
+ -- title
+ if line < 0 then
+ if not dbl then
+ return
+ end
+
+ if line == -1 then
+ rt.set_cursor(1)
+ else
+ state.depth = state.depth + line + 1
+ osd:dirty()
+ end
+ return
+ end
+
+ local menu = state:menu()
+ local pos = menu.view_top + line - 1
+ if dbl then
+ if pos ~= menu.cursor then
+ return
+ end
+
+ rt.select_option()
+ else
+ if pos > #menu.options then
+ return
+ end
+
+ rt.set_cursor(pos)
+ end
+end
+
+local function mouse_click_left_scrollbar(dbl, ratio)
+ if not dbl then
+ return
+ end
+
+ -- set_cursor handles out-of-bounds moves (when ratio == 1)
+ rt.set_cursor(
+ math.floor(ratio * #state:menu().options) + 1,
+ {centre = true})
+end
+
+local function mouse_click_right_menu(dbl, line)
+ if not dbl or line < 1 then
+ return
+ end
+
+ local menu = state:menu()
+ local pos = menu.view_top + line - 1
+ if pos > #menu.options then
+ return
+ end
+
+ rt.open_option_info(menu.options[pos])
+end
+
+function rt.mouse_click_left(dbl, area, val)
+ if area == 'menu' then
+ return mouse_click_left_menu(dbl, val)
+ elseif area == 'scrollbar' then
+ return mouse_click_left_scrollbar(dbl, val)
+ end
+end
+
+function rt.mouse_click_right(dbl, area, val)
+ if area == 'menu' then
+ return mouse_click_right_menu(dbl, val)
+ end
+end
+
function rt.reload_data()
if state.depth > 1 then
osd:flash_error('Can only reload data from root menu')
@@ -965,4 +1039,19 @@ function rt.reload_data()
rt.push_group_menu(ctx.catalogue:get('root'))
end
+function rt.set_osc_visibility()
+ local v = osd:is_hidden() and state.saved_osc_visibility or 'never'
+ mp.command_native({'script-message', 'osc-visibility', v, ''})
+end
+
+function rt.toggle_menu()
+ osd:toggle_hidden()
+
+ if state.saved_osc_visibility ~= 'never' then
+ rt.set_osc_visibility()
+ end
+
+ input.set_key_bindings()
+end
+
return rt