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/.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 2febf35ef89e4..ef72e959f51c1 100644 --- a/DOCS/man/context_menu.rst +++ b/DOCS/man/context_menu.rst @@ -1,10 +1,48 @@ -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 +CONTEXT MENU +============ + +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`_), 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. + +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. + +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. +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..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. @@ -399,16 +402,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/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/input.conf b/etc/input.conf index 5c723a07d2a19..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 @@ -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 @@ -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/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, 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 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}, diff --git a/player/lua/select.lua b/player/lua/select.lua index 875a290658546..676eb1801897c 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,510 @@ 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.item.type = "submenu" + 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 })