From 36322b47cdef3fcab3774ea46205a461847a813e Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 25 Aug 2025 17:43:44 +0200 Subject: [PATCH 1/5] input.conf: format some commands more consistently The formatting of input.conf commands is totally inconsistent. At least for the commands that will be used in the context menu, make it consistent. The next commit will define the context menu commands, and the keys bound to these commands will be shown in the menu only when they match exactly, so this avoids copy pasting commands with inconsistent formatting just to make them match. --- etc/input.conf | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/etc/input.conf b/etc/input.conf index 5c723a07d2a19..19335107449a4 100644 --- a/etc/input.conf +++ b/etc/input.conf @@ -38,10 +38,10 @@ # Mouse wheels, touchpad or other input devices that have axes # if the input devices supports precise scrolling it will also scale the # numeric value accordingly -#WHEEL_UP add volume 2 +#WHEEL_UP add volume 2 #WHEEL_DOWN add volume -2 #WHEEL_LEFT seek -10 # seek 10 seconds backward -#WHEEL_RIGHT seek 10 # seek 10 seconds forward +#WHEEL_RIGHT seek 10 # seek 10 seconds forward ## Seek units are in seconds, but note that these are limited by keyframes #RIGHT seek 5 # seek 5 seconds forward @@ -74,13 +74,13 @@ #HOME seek 0 absolute # seek to the start #PGUP add chapter 1 # seek to the next chapter #PGDWN add chapter -1 # seek to the previous chapter -#Shift+PGUP seek 600 # seek 10 minutes forward +#Shift+PGUP seek 600 # seek 10 minutes forward #Shift+PGDWN seek -600 # seek 10 minutes backward #[ multiply speed 1/1.1 # decrease the playback speed #] multiply speed 1.1 # increase the playback speed #{ multiply speed 0.5 # halve the playback speed #} multiply speed 2.0 # double the playback speed -#BS set speed 1.0 # reset the speed to normal +#BS set speed 1 # reset the speed to normal #Shift+BS revert-seek # undo the previous (or marked) seek #Shift+Ctrl+BS revert-seek mark # mark the position for revert-seek #q quit @@ -105,19 +105,19 @@ #? script-binding stats/display-page-4-toggle # toggle displaying key bindings #` script-binding commands/open # open the console #z add sub-delay -0.1 # shift subtitles 100 ms earlier -#Z add sub-delay +0.1 # delay subtitles by 100 ms -#x add sub-delay +0.1 # delay subtitles by 100 ms -#ctrl++ add audio-delay 0.100 # change audio/video sync by delaying the audio -#ctrl+- add audio-delay -0.100 # change audio/video sync by shifting the audio earlier -#ctrl+KP_ADD add audio-delay 0.100 # change audio/video sync by delaying the audio -#ctrl+KP_SUBTRACT add audio-delay -0.100 # change audio/video sync by shifting the audio earlier -#G add sub-scale +0.1 # increase the subtitle font size +#Z add sub-delay 0.1 # delay subtitles by 100 ms +#x add sub-delay 0.1 # delay subtitles by 100 ms +#ctrl++ add audio-delay 0.1 # change audio/video sync by delaying the audio +#ctrl+- add audio-delay -0.1 # change audio/video sync by shifting the audio earlier +#ctrl+KP_ADD add audio-delay 0.1 # change audio/video sync by delaying the audio +#ctrl+KP_SUBTRACT add audio-delay -0.1 # change audio/video sync by shifting the audio earlier +#G add sub-scale 0.1 # increase the subtitle font size #F add sub-scale -0.1 # decrease the subtitle font size #9 add volume -2 #/ add volume -2 #KP_DIVIDE add volume -2 -#0 add volume 2 -#* add volume 2 +#0 add volume 2 +#* add volume 2 #KP_MULTIPLY add volume 2 #m cycle mute # toggle mute #1 add contrast -1 @@ -129,8 +129,8 @@ #7 add saturation -1 #8 add saturation 1 #Alt+0 set window-scale 0.5 # halve the window size -#Alt+1 set window-scale 1.0 # reset the window size -#Alt+2 set window-scale 2.0 # double the window size +#Alt+1 set window-scale 1 # reset the window size +#Alt+2 set window-scale 2 # double the window size #b cycle deband # toggle the debanding filter #d cycle deinterlace # cycle the deinterlacing filter #r add sub-pos -1 # move subtitles up @@ -173,11 +173,11 @@ #ctrl+w quit #E cycle edition # switch edition #l ab-loop # set/clear A-B loop points -#L cycle-values loop-file "inf" "no" # toggle infinite looping +#L cycle-values loop-file inf no # toggle infinite looping #ctrl+c quit 4 #Ctrl+v loadfile ${clipboard/text} append-play; show-text '+ ${clipboard/text}' # append the copied path #DEL script-binding osc/visibility # cycle OSC visibility between never, auto (mouse-move) and always -#ctrl+h cycle-values hwdec "no" "auto" # toggle hardware decoding +#ctrl+h cycle-values hwdec no auto # toggle hardware decoding #F8 show-text ${playlist} # show the playlist #F9 show-text ${track-list} # show the list of video, audio and sub tracks #g ignore From eee776597c7e67adaf4a8ec01316fcf579250c97 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 25 Aug 2025 19:54:17 +0200 Subject: [PATCH 2/5] menu.conf: add this file Add a default context menu definition in etc/menu.conf. It will be used to populate the context menu. The format is meant to be easily readable with just tabs as separators and submenus denoted by indentation. It avoids complicating the builtin input.conf like similar scripts do. By not defining the shortcut keys in the builtin input.conf, menu entries won't be associated to wrong shortcuts when the user's input.conf changes key bindings without redefining the menu. Note that external scripts don't have this issue. I used 3+ spaces as separators at first but then realized that since we have Windows users tabs will look better in editors with proportional fonts like notepad, though it still won't be perfectly aligned depending on the font. But I am still not sure what is better. On a curious note, etc/menu.conf already existed in the past as mplayer used to have a menu. git log -p etc/menu.conf will show commits from 2002 to 2010. --- .editorconfig | 3 + DOCS/man/context_menu.rst | 32 +++++++-- DOCS/tech-overview.txt | 5 +- etc/menu.conf | 137 ++++++++++++++++++++++++++++++++++++++ etc/meson.build | 2 +- 5 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 etc/menu.conf diff --git a/.editorconfig b/.editorconfig index a4baa4b4c80b0..7441ce27de6d5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,3 +21,6 @@ indent_style = unset indent_size = unset max_line_length = unset trim_trailing_whitespace = unset + +[menu.conf] +indent_style = tab diff --git a/DOCS/man/context_menu.rst b/DOCS/man/context_menu.rst index 2febf35ef89e4..227884f8f6f29 100644 --- a/DOCS/man/context_menu.rst +++ b/DOCS/man/context_menu.rst @@ -1,10 +1,34 @@ CONTEXT MENU SCRIPT =================== -This script provides a context menu for platforms without integration with a -native context menu. On these platforms, it can be disabled entirely using the -``--load-context-menu=no`` option. On platforms where the integration is -implemented, it is already disabled by default. +The context menu is a menu that pops up on the video window. By default, it is +bound to right click. + +menu.conf +--------- + +You can define your own menu in ``~~/menu.conf`` (see `FILES`_). It is +recommended to use the default ``menu.conf`` from +https://github.com/mpv-player/mpv/blob/master/etc/menu.conf as an example to get +started. + +Each line of ``menu.conf`` is a menu item with fields separated by 1 or more +tabs. The first field is the text shown in the menu. The second field is usually +the command that is run when that item is selected. Fields from the third +onwards can specify ``checked=``, ``disabled=`` and ``hidden=`` states in the +same way as `Conditional auto profiles`_. + +When there is no command, the item will open a submenu. Fields below indented +with leading whitespace are added to this submenu. Nested submenu items are +defined by adding more leading whitespace than the parent menu entry. + +Empty lines are interpreted as separators. + +The second field can also be one of the following tokens to make that entry a +submenu with the relative items: ``$playlist``, ``$video-tracks``, +``$audio-tracks``, ``$sub-tracks``, ``$secondary-sub-tracks``, ``$chapters``, +``$editions``, ``$audio-devices``. These menus are automatically disabled when +empty. Script messages --------------- diff --git a/DOCS/tech-overview.txt b/DOCS/tech-overview.txt index ca4c648004ef6..09bd98dca7e98 100644 --- a/DOCS/tech-overview.txt +++ b/DOCS/tech-overview.txt @@ -272,8 +272,9 @@ sub/: sd_ass.c's internal state. etc/: - The files input.conf and builtin.conf are actually integrated into the mpv - binary by the build system. They contain the default configs and keybindings. + The files input.conf, builtin.conf and menu.conf are actually integrated + into the mpv binary by the build system. They contain the default configs + and keybindings. Best practices and Concepts within mpv ====================================== diff --git a/etc/menu.conf b/etc/menu.conf new file mode 100644 index 0000000000000..ad5cbedd505d3 --- /dev/null +++ b/etc/menu.conf @@ -0,0 +1,137 @@ +Play cycle pause hidden=not pause and not idle_active disabled=idle_active +Pause cycle pause hidden=idle_active or pause +Stop stop hidden=idle ~= true disabled=idle_active + +Open + Clipboard loadfile ${clipboard/text} append-play; show-text '+ ${clipboard/text}' + History script-binding select/select-watch-history + Watch later script-binding select/select-watch-later +Playlist $playlist + +Video + Track $video-tracks + + Fill no-osd cycle-values panscan 0 1; no-osd set video-unscaled no; no-osd set video-zoom 0 checked=panscan == 1 + Unscaled no-osd cycle-values video-unscaled yes no; no-osd set video-zoom 0; no-osd set panscan 0 checked=video_unscaled + Zoom + 50% set video-zoom -1 checked=video_zoom == -1 + 100% set video-zoom 0 checked=video_zoom == 0 + 200% set video-zoom 1 checked=video_zoom == 1 + Aspect ratio + 16:9 set video-aspect-override 16:9 checked=math.abs(video_aspect_override - 1.7) < 0.1 + 4:3 set video-aspect-override 4:3 checked=math.abs(video_aspect_override - 1.3) < 0.1 + 2.35:1 set video-aspect-override 2.35:1 checked=video_aspect_override == 2.35 + Default set video-aspect-override no checked=video_aspect_override == -2 + Center no-osd set video-pan-x 0; no-osd set video-pan-y 0; no-osd set video-align-x 0; no-osd set video-align-y 0 disabled=video_pan_x == 0 and video_pan_y == 0 and video_align_x == 0 and video_align_y == 0 + + Rotate clockwise cycle-values video-rotate 90 180 270 0 + Rotate counterclockwise cycle-values video-rotate 270 180 90 0 + + Deband cycle deband checked=deband + Deinterlace cycle deinterlace checked=deinterlace_active + + Screenshot screenshot disabled=not p["current-tracks/video"] + Screenshot without subtitles screenshot video disabled=not p["current-tracks/video"] +Audio + Track $audio-tracks + Devices $audio-devices + Channels + Auto set audio-channels auto-safe checked=audio_channels == "auto-safe" + Stereo set audio-channels stereo checked=audio_channels == "stereo" + Mono set audio-channels mono checked=audio_channels == "mono" + + Increase volume add volume 2 + Decrease volume add volume -2 + Mute cycle mute checked=mute + + Increase delay add audio-delay 0.1 + Decrease delay add audio-delay -0.1 +Subtitle + Track $sub-tracks + Visible cycle sub-visibility checked=sub_visibility + + Increase delay add sub-delay 0.1 + Decrease delay add sub-delay -0.1 + + Scale up add sub-scale 0.1 + Scale down add sub-scale -0.1 + + Lines script-binding select/select-subtitle-line disabled=not sid or p["current-tracks/sub/codec"] == "dvd_subtitle" or p["current-tracks/sub/codec"] == "hdmv_pgs_subtitle" + Secondary subtitle + Track $secondary-sub-tracks + Visible cycle secondary-sub-visibility checked=secondary_sub_visibility + + Increase delay add secondary-sub-delay 0.1 + Decrease delay add secondary-sub-delay -0.1 + +Playback + Display duration hidden=not p["current-tracks/video/image"] or p["current-tracks/audio"] + 1 second set image-display-duration 1 checked=image_display_duration == 1 + 2 seconds set image-display-duration 2 checked=image_display_duration == 2 + 5 seconds set image-display-duration 5 checked=image_display_duration == 5 + 10 seconds set image-display-duration 10 checked=image_display_duration == 10 + Infinite set image-display-duration inf checked=image_display_duration == math.huge + Speed hidden=p["current-tracks/video/image"] and not p["current-tracks/audio"] + 25% set speed 0.25 checked=speed == 0.25 + 50% set speed 0.50 checked=speed == 0.50 + 75% set speed 0.75 checked=speed == 0.75 + 100% set speed 1 checked=speed == 1 + 125% set speed 1.25 checked=speed == 1.25 + 150% set speed 1.50 checked=speed == 1.50 + 175% set speed 1.75 checked=speed == 1.75 + 200% set speed 2 checked=speed == 2 + 400% set speed 4 checked=speed == 4 + 800% set speed 8 checked=speed == 8 + + Seek 10 seconds forward seek 10 hidden=p["current-tracks/video/image"] and not p["current-tracks/audio"] + Seek 10 seconds backward seek -10 hidden=p["current-tracks/video/image"] and not p["current-tracks/audio"] + Seek 10 minutes forward seek 600 hidden=p["current-tracks/video/image"] and not p["current-tracks/audio"] + Seek 10 minutes backward seek -600 hidden=p["current-tracks/video/image"] and not p["current-tracks/audio"] + + Next file playlist-next disabled=playlist_count < 2 + Previous file playlist-prev disabled=playlist_count < 2 + + Next sub-playlist playlist-next-playlist disabled=playlist_count < 2 + Previous sub-playlist playlist-prev-playlist disabled=playlist_count < 2 +Chapters $chapters +Editions/Titles $editions + +Window + Fullscreen cycle fullscreen checked=fullscreen + Border cycle border checked=border + Always on top cycle ontop checked=ontop + Window scale + 50% set window-scale 0.5 checked=math.abs(get("current-window-scale", 0) - 0.5) < 0.1 + 100% set window-scale 1 checked=math.abs(get("current-window-scale", 0) - 1) < 0.1 + 200% set window-scale 2 checked=math.abs(get("current-window-scale", 0) - 2) < 0.1 + 300% set window-scale 3 checked=math.abs(get("current-window-scale", 0) - 3) < 0.1 + Screenshot window screenshot window +View + Playback statistics script-binding stats/display-page-1-toggle + File information script-binding stats/display-page-5-toggle + Key bindings script-binding stats/display-page-4-toggle + Time OSD no-osd cycle-values osd-level 3 1 checked=osd_level == 3 + Cycle OSC visibility script-binding osc/visibility +Tools + Set/clear A-B loop points ab-loop + Loop file cycle-values loop-file inf no checked=loop_file == "inf" + Loop playlist cycle-values loop-playlist inf no checked=loop_playlist == "inf" + + Copy path set clipboard/text ${path} disabled=idle_active + Copy subtitle set clipboard/text ${sub-text} disabled=not sid or p["current-tracks/sub/codec"] == "dvd_subtitle" or p["current-tracks/sub/codec"] == "hdmv_pgs_subtitle" + Copy title set clipboard/text ${media-title} disabled=idle_active + + Shuffle playlist-shuffle + Unshuffle playlist-unshuffle + + Hardware decoding cycle-values hwdec no auto checked=hwdec_current ~= "no" disabled=p["current-tracks/video/image"] ~= false + Key bindings script-binding select/select-binding + Properties script-binding select/show-properties + Console script-binding commands/open + + Edit config file script-binding select/edit-config-file + Edit key bindings script-binding select/edit-input-conf + Online documentation script-binding select/open-docs + +Quit quit +Quit watch later quit-watch-later hidden=save_position_on_quit diff --git a/etc/meson.build b/etc/meson.build index ac9eec0e6f831..36251b08ca135 100644 --- a/etc/meson.build +++ b/etc/meson.build @@ -9,7 +9,7 @@ foreach size: icons sources += icon endforeach -etc_files = ['input.conf', 'builtin.conf'] +etc_files = ['input.conf', 'builtin.conf', 'menu.conf'] foreach file: etc_files etc_file = custom_target(file, input: file, From 52edbc4672c2fba1b866d2c41a255147ad6eea3e Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 25 Aug 2025 20:12:29 +0200 Subject: [PATCH 3/5] command: add the default-menu property This property contains the builtin etc/menu.conf, and will be used by select.lua. It is undocumented because it is for internal usage. --- player/command.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/player/command.c b/player/command.c index 73cf519df0cd9..aa75bf967a923 100644 --- a/player/command.c +++ b/player/command.c @@ -4039,6 +4039,22 @@ static int mp_property_mdata(void *ctx, struct m_property *prop, return M_PROPERTY_NOT_IMPLEMENTED; } +static int mp_property_default_menu(void *ctx, struct m_property *prop, + int action, void *arg) +{ + switch (action) { + case M_PROPERTY_GET: + *(char **)arg = talloc_strdup(NULL, +#include "etc/menu.conf.inc" + ); + return M_PROPERTY_OK; + case M_PROPERTY_GET_TYPE: + *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING}; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + static int do_list_udata(int item, int action, void *arg, void *ctx); struct udata_ctx { @@ -4542,6 +4558,7 @@ static const struct m_property mp_properties_base[] = { {"input-bindings", mp_property_bindings}, {"menu-data", mp_property_mdata}, + {"default-menu", mp_property_default_menu}, {"user-data", mp_property_udata}, {"term-size", mp_property_term_size}, From a44744d891b4640f9b7cf1a8a1c19b055dc05d24 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Fri, 26 Sep 2025 20:01:39 +0200 Subject: [PATCH 4/5] select.lua: populate the context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make select.lua parse menu.conf, fill menu-data and open the context menu. This is done from select.lua to reuse the code to format the data and retrieve key bindings. The first time the context menu is opened, menu-data is filled initially, and the referenced properties are observed so that menu-data is already updated by the next time you open the context menu. So if you never use the context menu there is no extra overhead. While the context menu is scrollable, for the playlist it is better to start from around the current entry rather than from the beginning, and since menu-data has no way to specify where to start, when there are more than 25 items playlist items this adds … entries that open the scrollable console menu when clicked. input-bindings is parsed to show the shortcuts bound to commands. Since it is not observable, it just uses the bindings from the first time the context menu is opened. console and stats key bindings are skipped in case the context menu is opened together with those scripts. Multimedia and numpad keys are not shown to reduce clutter. The old Context Menu section of the docs is merged in the more detailed context_menu.rst to not repeat the same information. --- .luacheckrc | 1 + DOCS/man/context_menu.rst | 20 +- DOCS/man/mpv.rst | 10 - DOCS/man/select.rst | 28 ++ player/lua/select.lua | 580 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 607 insertions(+), 32 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 84ae84e9bbd8a..dda48b031d23b 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -106,4 +106,5 @@ stds = { mp = { read_globals = mp_globals } } -- mp_internal seems to be merged with mp for other files too... files["player/lua/defaults.lua"] = { globals = mp_internal } files["player/lua/auto_profiles.lua"] = { globals = { "p", "get" } } +files["player/lua/select.lua"] = { globals = { "p", "get" } } max_line_length = 100 diff --git a/DOCS/man/context_menu.rst b/DOCS/man/context_menu.rst index 227884f8f6f29..ef72e959f51c1 100644 --- a/DOCS/man/context_menu.rst +++ b/DOCS/man/context_menu.rst @@ -1,5 +1,5 @@ -CONTEXT MENU SCRIPT -=================== +CONTEXT MENU +============ The context menu is a menu that pops up on the video window. By default, it is bound to right click. @@ -7,7 +7,8 @@ bound to right click. menu.conf --------- -You can define your own menu in ``~~/menu.conf`` (see `FILES`_). It is +You can define your own menu in ``~~/menu.conf`` (see `FILES`_), or an +alternative path specified with ``--script-opt=select-menu_conf_path``. It is recommended to use the default ``menu.conf`` from https://github.com/mpv-player/mpv/blob/master/etc/menu.conf as an example to get started. @@ -30,6 +31,19 @@ submenu with the relative items: ``$playlist``, ``$video-tracks``, ``$editions``, ``$audio-devices``. These menus are automatically disabled when empty. +To use the native context menu, you need to fill the ``menu-data`` property with +menu definition data, and call the ``context-menu`` command. In builtin scripts, +this is done by ``select.lua``, which parses ``menu.conf`` to populate +``menu-data``. It then calls the ``context-menu`` command on platforms where +integration with the native context menu is implemented, while on platforms +where it is not, it opens ``context_menu.lua``. + +On platforms without integration with the native context menu, +``context_menu.lua`` can be disabled entirely using the +``--load-context-menu=no`` option. On platforms where the integration is +implemented, it is already disabled by default, and ``--load-context-menu=yes`` +will make ``select.lua`` use it. + Script messages --------------- diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index 895572da2f0f3..23b43069bfddd 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -399,16 +399,6 @@ Ctrl+Wheel up/down Change video zoom keeping the part of the video hovered by the cursor under it. -Context Menu -------------- - -Context Menu is a menu that pops up on the video window on user interaction -(mouse right click, etc.). - -To use this feature, you need to fill the ``menu-data`` property with menu -definition data, and add a keybinding to run the ``context-menu`` command, -which can be done with a user script. - USAGE ===== diff --git a/DOCS/man/select.rst b/DOCS/man/select.rst index 5d9f93d34757e..a6e0f7f09ea06 100644 --- a/DOCS/man/select.rst +++ b/DOCS/man/select.rst @@ -7,6 +7,8 @@ providing script bindings that gather and format the data to be selected in the console and do operations on the selected item. It can be disabled using the ``--load-select=no`` option. +This script is also used to populate the context menu. + Key bindings ------------ @@ -141,6 +143,9 @@ Available script bindings are: ``menu`` Show a menu with miscellaneous entries. +``context-menu`` + Show the context menu. + Configuration ------------- @@ -161,3 +166,26 @@ Configurable options Default: yes Whether to show only the last of history entries with the same path. + +``menu_conf_path`` + Default: ~~/menu.conf (see `FILES`_). + + The path from which to read the custom context menu definition (see `CONTEXT + MENU`_). + +``max_playlist_items`` + Default: 25 + + The maximum number of playlist entries in the context menu. + +``use_context_menu_script`` + Default: auto + + Whether to use the native context menu or ``context_menu.lua``. + + ``auto`` means ``context_menu.lua`` is used with + ``--load-context-menu=yes``, and the native context menu is attempted to be + used with ``--load-context-menu=no``. + + ``yes`` allows using a fork of ``context_menu.lua`` with + ``--load-context-menu=no``. diff --git a/player/lua/select.lua b/player/lua/select.lua index 875a290658546..bd0a1a68bc168 100644 --- a/player/lua/select.lua +++ b/player/lua/select.lua @@ -21,10 +21,16 @@ local input = require "mp.input" local options = { history_date_format = "%Y-%m-%d %H:%M:%S", hide_history_duplicates = true, + menu_conf_path = "~~/menu.conf", + max_playlist_items = 25, + use_context_menu_script = "auto", } require "mp.options".read_options(options, nil, function () end) +local trailing_slash_pattern = mp.get_property("platform") == "windows" + and "[/\\]+$" or "/+$" + local function show_warning(message) mp.msg.warn(message) if mp.get_property_native("vo-configured") then @@ -39,25 +45,42 @@ local function show_error(message) end end +local function to_map(t) + local map = {} + + for _, value in pairs(t) do + map[value] = true + end + + return map +end + +local function format_playlist_entry(entry, show) + local item = entry.title + + if not item or show ~= "title" then + item = entry.filename + + if not item:find("://") then + item = select(2, utils.split_path( + item:gsub(trailing_slash_pattern, ""))) + end + + if entry.title and show == "both" then + item = entry.title .. " (" .. item .. ")" + end + end + + return item +end + mp.add_key_binding(nil, "select-playlist", function () local playlist = {} local default_item local show = mp.get_property_native("osd-playlist-entry") - local trailing_slash_pattern = mp.get_property("platform") == "windows" - and "[/\\]+$" or "/+$" for i, entry in ipairs(mp.get_property_native("playlist")) do - playlist[i] = entry.title - if not playlist[i] or show ~= "title" then - playlist[i] = entry.filename - if not playlist[i]:find("://") then - playlist[i] = select(2, utils.split_path( - playlist[i]:gsub(trailing_slash_pattern, ""))) - end - end - if entry.title and show == "both" then - playlist[i] = string.format("%s (%s)", entry.title, playlist[i]) - end + playlist[i] = format_playlist_entry(entry, show) if entry.playing then default_item = i @@ -229,6 +252,10 @@ mp.add_key_binding(nil, "select-chapter", function () }) end) +local function format_edition(edition) + return edition.title or ("Edition " .. edition.id + 1) +end + mp.add_key_binding(nil, "select-edition", function () local edition_list = mp.get_property_native("edition-list") @@ -241,7 +268,7 @@ mp.add_key_binding(nil, "select-edition", function () local default_item = mp.get_property_native("current-edition") for i, edition in ipairs(edition_list) do - editions[i] = edition.title or ("Edition " .. edition.id + 1) + editions[i] = format_edition(edition) end input.select({ @@ -381,6 +408,10 @@ mp.add_key_binding(nil, "select-subtitle-line", function () }) end) +local function format_audio_device(device) + return device.name .. " (" .. device.description .. ")" +end + mp.add_key_binding(nil, "select-audio-device", function () local devices = mp.get_property_native("audio-device-list") local items = {} @@ -396,7 +427,7 @@ mp.add_key_binding(nil, "select-audio-device", function () end for i, device in ipairs(devices) do - items[i] = device.name .. " (" .. device.description .. ")" + items[i] = format_audio_device(device) if device.name == selected_device then default_item = i @@ -550,7 +581,7 @@ mp.add_key_binding(nil, "select-watch-later", function () }) end) -mp.add_key_binding(nil, "select-binding", function () +local function get_active_bindings() local bindings = {} for _, binding in pairs(mp.get_property_native("input-bindings")) do @@ -559,13 +590,18 @@ mp.add_key_binding(nil, "select-binding", function () (bindings[binding.key].is_weak and not binding.is_weak) or (binding.is_weak == bindings[binding.key].is_weak and binding.priority > bindings[binding.key].priority) - ) then + ) and binding.section ~= "input_forced_console" + and binding.section ~= "input_forced_stats" then bindings[binding.key] = binding end end + return bindings +end + +mp.add_key_binding(nil, "select-binding", function () local items = {} - for _, binding in pairs(bindings) do + for _, binding in pairs(get_active_bindings()) do if binding.cmd ~= "ignore" then items[#items + 1] = binding.key .. " " .. binding.cmd end @@ -689,7 +725,7 @@ mp.add_key_binding(nil, "menu", function () local text_sub_selected = false local is_disc = mp.get_property("current-demuxer") == "disc" - local image_sub_codecs = {["dvd_subtitle"] = true, ["hdmv_pgs_subtitle"] = true} + local image_sub_codecs = to_map({"dvd_subtitle", "hdmv_pgs_subtitle"}) for _, track in pairs(mp.get_property_native("track-list")) do if track.type == "sub" then @@ -751,3 +787,509 @@ mp.add_key_binding(nil, "menu", function () end, }) end) + + +local menu = {} -- contains wrappers of menu_data's items +local menu_data = {} +local observed_properties = {} +local property_cache = {} +local active_bindings = {} +local property_set = {} +local property_items = {} +local have_dirty_items = false +local current_item + +local function on_property_change(name, value) + property_cache[name] = value + + if property_items[name] then + for item, _ in pairs(property_items[name]) do + item.dirty = true + end + have_dirty_items = true + end +end + +function _G.get(name, default) + if not observed_properties[name] then + local result, err = mp.get_property_native(name) + + if err == "property not found" and not property_set(name:match("^([^/]+)")) then + mp.msg.error("Property '" .. name .. "' was not found.") + return default + end + + observed_properties[name] = true + property_cache[name] = result + mp.observe_property(name, "native", on_property_change) + end + + if current_item then + if not property_items[name] then + property_items[name] = {} + end + + property_items[name][current_item] = true + end + + if property_cache[name] == nil then + return default + end + + return property_cache[name] +end + +local function magic_get(name) + return get(name:gsub("_", "-"), nil) +end + +local evil_magic = {} +setmetatable(evil_magic, { + __index = function(_, key) + if _G[key] ~= nil then + return _G[key] + end + + return magic_get(key) + end, +}) + +_G.p = {} +setmetatable(p, { + __index = function(_, key) + return magic_get(key) + end, +}) + +local function compile_condition(chunk, chunkname) + chunk = "return " .. chunk + chunkname = 'Menu entry "' .. chunkname .. '"' + + local compiled_chunk, err + + -- luacheck: push + -- luacheck: ignore setfenv loadstring + if setfenv then -- lua 5.1 + compiled_chunk, err = loadstring(chunk, chunkname) + if compiled_chunk then + setfenv(compiled_chunk, evil_magic) + end + else -- lua 5.2 + compiled_chunk, err = load(chunk, chunkname, "t", evil_magic) + end + -- luacheck: pop + + if not compiled_chunk then + mp.msg.error(chunkname .. " : " .. err) + compiled_chunk = function() return false end + end + + return compiled_chunk +end + +local function evaluate_condition(chunk, chunkname) + local status, result + status, result = pcall(chunk) + + if not status then + mp.msg.verbose(chunkname .. " error on evaluating: " .. result) + return false + end + + return not not result +end + +local function toggle_state(states, state, add) + for i, existing_state in ipairs(states) do + if existing_state == state then + if add then + return + end + + table.remove(states, i) + end + end + + if add then + states[#states + 1] = state + end +end + +local function on_idle() + if not have_dirty_items then + return + end + + have_dirty_items = false + + for _, item in pairs(menu) do + if item.dirty then + item:update() + item.dirty = false + end + end + + mp.set_property_native("menu-data", menu_data) +end + +local function clamp_submenu(submenu, max, cmd) + if #submenu <= max then + return submenu + end + + local mid = 1 + for i, item in pairs(submenu) do + if item.state then + mid = i + break + end + end + + local offset = math.floor(max / 2) + local first = mid + 1 - offset + local last = mid + offset + + if first < 1 then + first = 1 + last = max + end + + if last > #submenu then + first = math.max(#submenu - max + 1, 1) + last = #submenu + end + + local clamped = {} + + if first > 1 then + clamped[1] = { + title = "…", + cmd = cmd, + shortcut = first - 1 .. " more", + } + end + + for i = first, last do + clamped[#clamped + 1] = submenu[i] + end + + if last < #submenu then + clamped[#clamped + 1] = { + title = "…", + cmd = cmd, + shortcut = #submenu - last .. " more", + } + end + + return clamped +end + +local function playlist() + local items = {} + local show = get("osd-playlist-entry") + + for i, entry in ipairs(get("playlist")) do + items[i] = { + title = format_playlist_entry(entry, show), + cmd = "playlist-play-index " .. (i - 1) + } + + if entry.playing then + items[i].state = {"checked"} + end + end + + return clamp_submenu(items, options.max_playlist_items, + "script-binding select/select-playlist") +end + +local function tracks(property, type) + local items = {} + + for _, track in ipairs(get("track-list")) do + if track.type == type then + items[#items + 1] = { + -- Remove the circles since checkmarks are already added. + title = format_track(track):sub(5), + cmd = "set " .. property .. " " .. track.id, + } + + if track.selected then + items[#items].cmd = "set " .. property .. " no" + items[#items].state = {"checked"} + end + end + end + + return items +end + +local function chapters() + local items = {} + local current_chapter = get("chapter", -1) + local duration = mp.get_property_native("duration", math.huge) + + for i, chapter in ipairs(get("chapter-list")) do + items[i] = { + title = chapter.title, + cmd = "set chapter " .. (i - 1), + shortcut = format_time(chapter.time, duration), + } + + if i == current_chapter + 1 then + items[i].state = {"checked"} + end + end + + return items +end + +local function editions() + local items = {} + local current_edition = get("current-edition", -1) + + for i, edition in ipairs(get("edition-list", {})) do + items[i] = { + title = format_edition(edition), + cmd = "set edition " .. (i - 1), + } + + if i == current_edition + 1 then + items[i].state = {"checked"} + end + end + + return items +end + +local function audio_devices() + local items = {} + local selected_device = get("audio-device") + + for i, device in ipairs(get("audio-device-list")) do + items[i] = { + title = format_audio_device(device), + cmd = "set audio-device " .. device.name, + } + + if device.name == selected_device then + items[i].state = {"checked"} + end + end + + return items +end + +local builtin_submenus = { + ["$playlist"] = playlist, + ["$video-tracks"] = function () return tracks("video", "video") end, + ["$audio-tracks"] = function () return tracks("audio", "audio") end, + ["$sub-tracks"] = function () return tracks("sub", "sub") end, + ["$secondary-sub-tracks"] = function () return tracks("secondary-sid", "sub") end, + ["$chapters"] = chapters, + ["$editions"] = editions, + ["$audio-devices"] = audio_devices, +} + +local submenu_commands = { + ["$playlist"] = "script-binding select/select-playlist", + ["$video-tracks"] = "script-binding select/select-vid", + ["$audio-tracks"] = "script-binding select/select-aid", + ["$sub-tracks"] = "script-binding select/select-sid", + ["$secondary-sub-tracks"] = "script-binding select/select-secondary-sid", + ["$chapters"] = "script-binding select/select-chapter", + ["$editions"] = "script-binding select/select-edition", + ["$audio-devices"] = "script-binding select/select-audio-device", +} + +local function get_shortcut(cmd) + local shortcuts = {} + local uncommon_keys = to_map({ + "MBTN_BACK", "MBTN_FORWARD", "POWER", "PLAY", "PAUSE", "PLAYPAUSE", + "PLAYONLY", "PAUSEONLY", "STOP", "FORWARD", "REWIND", "NEXT", "PREV", + "VOLUME_UP", "VOLUME_DOWN", "MUTE", "CLOSE_WIN", + }) + + for _, binding in pairs(active_bindings) do + if binding.cmd == cmd and not uncommon_keys[binding.key] + and not binding.key:find("KP_") then + shortcuts[#shortcuts + 1] = binding.key + end + end + + return table.concat(shortcuts, ",") +end + +local function update_builtin_submenu(item) + item.item.submenu = builtin_submenus[item.builtin_submenu]() + + local min = item.builtin_submenu == "$editions" and 2 or 1 + item.item.state = #item.item.submenu < min and {"disabled"} or {} +end + +local function update_state(item) + for state, compiled_condition in pairs(item.compiled_conditions) do + toggle_state(item.item.state, state, + evaluate_condition(compiled_condition, item.item.title)) + end +end + +local function parse_menu_item(line) + local tokens = {} + local separator = "\t+" + for token in line:gmatch("(.-)" .. separator) do + tokens[#tokens + 1] = token + end + tokens[#tokens + 1] = line:gsub(".*" .. separator, "") + + if tokens[1] == "" then + return { type = "separator" } + end + + local item = { + item = { + title = tokens[1], + state = {}, + }, + compiled_conditions = {}, + } + + current_item = item + + if builtin_submenus[tokens[2]] then + item.builtin_submenu = tokens[2] + item.item.shortcut = get_shortcut(submenu_commands[item.builtin_submenu]) + + -- Observing large playlist slows down changing file. + if item.builtin_submenu == "$playlist" and + mp.get_property_native("playlist-count") > 999 then + item.item.cmd = submenu_commands[item.builtin_submenu] + return item + end + + item.update = update_builtin_submenu + item:update() + return item + end + + local state_start = 3 + for _, state in pairs({"checked", "disabled", "hidden"}) do + if not tokens[2] or tokens[2]:find("^" .. state .. "=") then + state_start = 2 + break + end + end + + if state_start == 2 then + item.item.type = "submenu" + item.item.submenu = {} + else + item.item.cmd = tokens[2] + item.item.shortcut = get_shortcut(tokens[2]) + end + + for i = state_start, #tokens do + local state, condition = tokens[i]:match("(.-)=(.*)") + item.compiled_conditions[state] = compile_condition(condition, tokens[1]) + if evaluate_condition(item.compiled_conditions[state], tokens[1]) then + table.insert(item.item.state, state) + end + end + + item.update = update_state + item:update() + + return item +end + +local function get_menu_conf() + local menu_conf + local file_handle = io.open(mp.command_native({"expand-path", options.menu_conf_path})) + if file_handle then + menu_conf = file_handle:read("*a") + file_handle:close() + else + menu_conf = mp.get_property("default-menu") + end + + local lines = {} + for line in menu_conf:gmatch("(.-)\n") do + lines[#lines + 1] = line + end + + return lines +end + +local function parse_menu_conf() + property_set = to_map(mp.get_property_native("property-list")) + active_bindings = get_active_bindings() + + local lines = get_menu_conf() + local last_leading_whitespace = "" + local menus_by_depth = { [""] = menu_data } + + for i, line in ipairs(lines) do + local leading_whitespace = line:match("^%s*") + local item = parse_menu_item(line:gsub("^%s*", "")) + + if item.item then + menu[#menu + 1] = item + item = item.item + end + + if #leading_whitespace > #last_leading_whitespace then + local last_menu = menus_by_depth[last_leading_whitespace] + + if not last_menu[#last_menu].submenu then + show_error("menu.conf is malformed: " .. line .. + " has leading whitespace but no parent menu was defined") + return + end + + menus_by_depth[leading_whitespace] = last_menu[#last_menu].submenu + end + + if line == "" then + -- Determine the depth of the separator from the next line. + table.insert(menus_by_depth[lines[i + 1]:match("%s*")], item) + else + table.insert(menus_by_depth[leading_whitespace], item) + last_leading_whitespace = leading_whitespace + end + end + + property_set = nil + active_bindings = nil + current_item = nil + + mp.set_property_native("menu-data", menu_data) + + mp.register_idle(on_idle) +end + +mp.add_key_binding(nil, "context-menu", function (info) + if info.event == "repeat" then + return + end + + if not menu_data[1] then + parse_menu_conf() + end + + local use_context_menu_script = options.use_context_menu_script == "yes" + or mp.get_property_native("load-context-menu") + + if info.event == "up" then + if use_context_menu_script and info.is_mouse then + mp.commandv("script-message-to", "context_menu", "select") + end + + return + end + + mp.command( + use_context_menu_script + and "script-message-to context_menu open" + or "context-menu" + ) +end, { complex = true }) From e8dabce800c4993c8e522391da3840900a84a992 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Fri, 26 Sep 2025 20:21:37 +0200 Subject: [PATCH 5/5] input.conf: add context menu bindings Rebind right click and MENU to show the context menu. Shift+F10 is also bound because it is a standard context menu binding. MENU is not added to restore-old-bindings.conf because it is uncommon. --- DOCS/man/mpv.rst | 7 +++++-- etc/input.conf | 5 +++-- etc/restore-old-bindings.conf | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index 23b43069bfddd..86009e75091e4 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -351,11 +351,14 @@ g-b g-r Show the values of all properties. -g-m, MENU, Ctrl+p +g-m, Ctrl+p Show a menu with miscellaneous entries. See `SELECT`_ for more information. +MENU, Shift+F10 + Show the context menu (see `CONTEXT MENU`_). + (The following keys are valid if you have a keyboard with multimedia keys.) PAUSE @@ -384,7 +387,7 @@ Left double click Toggle fullscreen on/off. Right click - Toggle pause on/off. + Show the context menu (see `CONTEXT MENU`_). Forward/Back button Skip to next/previous entry in playlist. diff --git a/etc/input.conf b/etc/input.conf index 19335107449a4..82c12f517f131 100644 --- a/etc/input.conf +++ b/etc/input.conf @@ -30,7 +30,7 @@ #MBTN_LEFT ignore # don't do anything #MBTN_LEFT_DBL cycle fullscreen # toggle fullscreen -#MBTN_RIGHT cycle pause # toggle pause/playback mode +#MBTN_RIGHT script-binding select/context-menu # show the context menu #MBTN_BACK playlist-prev # skip to the previous file #MBTN_FORWARD playlist-next # skip to the next file #Ctrl+MBTN_LEFT script-binding positioning/drag-to-pan # pan around the clicked point @@ -196,8 +196,9 @@ #g-b script-binding select/select-binding #g-r script-binding select/show-properties #g-m script-binding select/menu -#MENU script-binding select/menu #ctrl+p script-binding select/menu +#MENU script-binding select/context-menu +#Shift+F10 script-binding select/context-menu #Alt+KP1 add video-rotate -1 # rotate video counterclockwise by 1 degree #Alt+KP5 set video-rotate 0 # reset rotation diff --git a/etc/restore-old-bindings.conf b/etc/restore-old-bindings.conf index d07b37e02b9e3..3339e525278aa 100644 --- a/etc/restore-old-bindings.conf +++ b/etc/restore-old-bindings.conf @@ -9,6 +9,10 @@ # # Older installations use ~/.mpv/input.conf instead. +# changed in mpv 0.41.0 + +MBTN_RIGHT cycle pause + # changed in mpv 0.37.0 WHEEL_UP seek 10 # seek 10 seconds forward