diff options
| author | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-01-12 23:34:53 -0800 |
|---|---|---|
| committer | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-01-12 23:34:53 -0800 |
| commit | 3c84dd08f36c8368ea7371c1d3f152f1dda6cb71 (patch) | |
| tree | a6129ca7395f4c6f18f0a64da1f7d7b1fbf60ddc | |
| parent | 206f73ed62003dba5041348f17433b1b3fe70174 (diff) | |
| download | mpv-iptv-menu-3c84dd08f36c8368ea7371c1d3f152f1dda6cb71.tar.gz mpv-iptv-menu-3c84dd08f36c8368ea7371c1d3f152f1dda6cb71.tar.xz | |
add mouse support
| -rw-r--r-- | catalogue.lua | 1 | ||||
| -rw-r--r-- | config.lua | 4 | ||||
| -rw-r--r-- | main.lua | 207 | ||||
| -rw-r--r-- | osd.lua | 27 |
4 files changed, 204 insertions, 35 deletions
diff --git a/catalogue.lua b/catalogue.lua index d781cf4..1d54a17 100644 --- a/catalogue.lua +++ b/catalogue.lua @@ -12,6 +12,7 @@ function catalogue.new() t:add({ type = 'group', id = 'root', + name = '/', }) t:add({ type = 'group', @@ -9,6 +9,10 @@ config.font_size = 20 config.scroll_margin = 4 +config.click_timeout = 0.5 +config.click_dbl_time = 0.3 +config.click_max_drift = 2 + config.bg_alpha = '44' config.colours = { title = '999999', @@ -16,6 +16,7 @@ local script_name = mp.get_script_name() local script_dir = mp.get_script_directory() local state = _state.new() +local click_state = {} local downloader = _downloader.new({limit = 5}) local xc = _xc.new({ @@ -754,9 +755,9 @@ local function open_option_episode_info(opt) {info = opt.info_data}) end -local function open_option_info() +local function open_option_info(opt) local menu = state:menu() - local opt = menu.options[menu.cursor] + local opt = opt or menu.options[menu.cursor] if not opt then return end @@ -946,6 +947,139 @@ local function toggle_menu_sort() update_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 + +-- 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 function handle_mouse_drift(_, mpos) + local s = click_state + if not mpos or not s.ct then + return + end + + if mouse_has_drifted(mpos.x, mpos.y, s.cx, s.cy) then + click_state = {} + mp.unobserve_property(handle_mouse_drift) + end +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, mpos, id) + mp.unobserve_property(handle_mouse_drift) + + if ev.canceled then + click_state = {} + return + end + + local s = click_state + local time = mp.get_time() + + if ev.event == 'down' then + s.ct, s.ck = time, ev.key_name + s.cx, s.cy, s.ci = mpos.x, mpos.y, id + mp.observe_property('mouse-pos', 'native', handle_mouse_drift) + return + end + + if ev.event ~= 'up' or not s.ct then + return + end + + if time - s.ct > config.click_timeout or + s.ck ~= ev.key_name or s.ci ~= id or + mouse_has_drifted(mpos.x, mpos.y, s.cx, s.cy) then + click_state = {} + return + end + + local dbl = s.pt and s.ct - s.pt <= config.click_dbl_time and + s.pk == ev.key_name and s.pi == id and + not mouse_has_drifted(s.cx, s.cy, s.px, s.py) + click_state = dbl and {} or + { + pt = s.ct, pk = ev.key_name, + px = s.cx, py = s.cy, pi = id, + } + return ev.key_name, dbl +end + +local function process_mouse_click(ev) + local mpos = mp.get_property_native('mouse-pos') + if not mpos then + return + end + + local line = osd:get_mouse_line(mpos) + if line then + line = line - (osd.lines - osd_menu_lines()) + end + + local key, dbl = handle_mouse_click(ev, mpos, line) + return key, dbl, line +end + +local function mouse_click_left(ev) + local key, dbl, line = process_mouse_click(ev) + if not key or not line or line == 0 then + return + end + + -- title + if line < 0 then + if not dbl then + return + end + + if line == -1 then + set_cursor(1) + else + state.depth = state.depth + line + 1 + update_osd() + end + return + end + + local menu = state:menu() + local pos = menu.view_top + line - 1 + if dbl then + if pos ~= menu.cursor then + return + end + + select_option() + else + if pos > #menu.options then + return + end + + set_cursor(pos) + end +end + +local function mouse_click_right(ev) + local key, dbl, line = process_mouse_click(ev) + if not key or not dbl or not line or line < 1 then + return + end + + local menu = state:menu() + local pos = menu.view_top + line - 1 + if pos > #menu.options then + return + end + + open_option_info(menu.options[pos]) +end + local function bind_key(key, func, opts) -- unique name is needed for removal local i = #key_bindings+1 @@ -964,16 +1098,19 @@ end function bind_search_keys() unbind_keys() - bind_key('ANY_UNICODE', search_input_char, {complex = true}) - bind_key('BS', search_input_bs, {repeatable = true}) - bind_key('DEL', search_input_del, {repeatable = true}) + local c = {complex = true} + local r = {repeatable = true} + + bind_key('ANY_UNICODE', search_input_char, c) + bind_key('BS', search_input_bs, r) + bind_key('DEL', search_input_del, r) bind_key('ENTER', end_search) bind_key('ESC', cancel_search) bind_key('Ctrl+c', cancel_search) - bind_key('LEFT', search_cursor_left, {repeatable = true}) - bind_key('RIGHT', search_cursor_right, {repeatable = true}) + bind_key('LEFT', search_cursor_left, r) + bind_key('RIGHT', search_cursor_right, r) bind_key('Ctrl+a', search_cursor_start) bind_key('Ctrl+e', search_cursor_end) end @@ -981,6 +1118,9 @@ end function bind_menu_keys() unbind_keys() + local c = {complex = true} + local r = {repeatable = true} + bind_key('BS', prev_menu) bind_key('/', start_search) bind_key('Ctrl+s', toggle_menu_sort) @@ -992,23 +1132,44 @@ function bind_menu_keys() bind_key('?', open_option_info) bind_key('Ctrl+p', goto_playing) - bind_key('j', cursor_down, {repeatable = true}) - bind_key('k', cursor_up, {repeatable = true}) - bind_key('UP', cursor_up, {repeatable = true}) - bind_key('DOWN', cursor_down, {repeatable = true}) + bind_key('MBTN_LEFT', mouse_click_left, c) + bind_key('MBTN_RIGHT', mouse_click_right, c) + bind_key('MBTN_LEFT_DBL') + bind_key('MBTN_RIGHT_DBL') + + bind_key('k', cursor_up, r) + bind_key('j', cursor_down, r) + bind_key('K', cursor_page_up, r) + bind_key('J', cursor_page_down, r) + bind_key('UP', cursor_up, r) + bind_key('DOWN', cursor_down, r) + bind_key('Shift+UP', cursor_page_up, r) + bind_key('Shift+DOWN', cursor_page_down, r) + bind_key('PGUP', cursor_page_up, r) + bind_key('PGDWN', cursor_page_down, r) bind_key('HOME', cursor_start) bind_key('END', cursor_end) - bind_key('PGUP', cursor_page_up, {repeatable = true}) - bind_key('PGDWN', cursor_page_down, {repeatable = true}) - - bind_key('J', move_option_down, {repeatable = true}) - bind_key('K', move_option_up, {repeatable = true}) - bind_key('Shift+UP', move_option_up, {repeatable = true}) - bind_key('Shift+DOWN', move_option_down, {repeatable = true}) - bind_key('Shift+HOME', move_option_start) - bind_key('Shift+END', move_option_end) - bind_key('Shift+PGUP', move_option_page_up, {repeatable = true}) - bind_key('Shift+PGDWN', move_option_page_down, {repeatable = true}) + bind_key('WHEEL_UP', cursor_up, r) + bind_key('WHEEL_DOWN', cursor_down, r) + bind_key('Shift+WHEEL_UP', cursor_page_up, r) + bind_key('Shift+WHEEL_DOWN', cursor_page_down, r) + + bind_key('Alt+k', move_option_up, r) + bind_key('Alt+j', move_option_down, r) + bind_key('Alt+K', move_option_page_up, r) + bind_key('Alt+J', move_option_page_down, r) + bind_key('Alt+UP', move_option_up, r) + bind_key('Alt+DOWN', move_option_down, r) + bind_key('Alt+Shift+UP', move_option_page_up, r) + bind_key('Alt+Shift+DOWN', move_option_page_down, r) + bind_key('Alt+PGUP', move_option_page_up, r) + bind_key('Alt+PGDWN', move_option_page_down, r) + bind_key('Alt+HOME', move_option_start) + bind_key('Alt+END', move_option_end) + bind_key('Alt+WHEEL_UP', move_option_up, r) + bind_key('Alt+WHEEL_DOWN', move_option_down, r) + bind_key('Alt+Shift+WHEEL_UP', move_option_page_up, r) + bind_key('Alt+Shift+WHEEL_DOWN', move_option_page_down, r) end -- uses enable-section and disable-section to disable builtin key bindings @@ -1070,7 +1231,7 @@ end) load_data() push_group_menu(catalogue:get('root')) -osc_visibility = mp.get_property('user-data/osc/visibility', 'auto') +osc_visibility = mp.get_property_native('user-data/osc/visibility', 'auto') set_osc_visibility() -- keys added via bind_key() are unbound when the OSD is closed, but we want @@ -61,12 +61,8 @@ function mt:resize(w, h) end function mt:menu_lines(state) - if state.depth > 1 then - -- leaves an extra line for padding between titles and options - return self.lines - state.depth - else - return self.lines - end + -- leaves an extra line for padding between titles and options + return self.lines - state.depth - 1 end function mt:menu_title(menu) @@ -94,7 +90,7 @@ function mt:menu_title(menu) str = colours.icon_sorted .. '⇅ ' .. col .. str end - return col .. ' » ' .. str + return col .. '» ' .. str end function mt:option_icons(opt, info) @@ -364,12 +360,10 @@ function mt:redraw(state) -- navigation which moves the viewport. local out_max = {} - if state.depth > 1 then - for i = 2, state.depth do - out[#out+1] = self:menu_title(state.menus[i]) - end - out[#out+1] = ' ' -- space character for correct line height + for i = 1, state.depth do + out[#out+1] = self:menu_title(state.menus[i]) end + out[#out+1] = ' ' -- space character for correct line height local menu = state:menu() @@ -469,4 +463,13 @@ function mt:is_hidden() return self.fg.hidden end +function mt:get_mouse_line(mpos) + local y = mpos.y / self.scale + local line = math.floor((y - self.padding) / config.font_size) + 1 + if line < 1 or line > self.lines then + return + end + return line +end + return osd |
