Created
February 2, 2017 14:07
-
-
Save avih/bee746200b5712220b8bd2f230e535de to your computer and use it in GitHub Desktop.
flexible mpv context menu using Tcl/Tk, for Windows too!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ ************************************************************* | |
* Context menu for mpv using Tcl/Tk. Mostly proof of concept. | |
* Avi Halachmi (:avih) https://github.com/avih | |
* | |
* Features: | |
* - Simple construction: ["<some text>", "<mpv-command>"] is a complete menu item. | |
* - Possibly dynamic menu items and commands, disabled items, separators. | |
* - Possibly keeping the menu open after clicking an item (via re-launch). | |
* - Hacky pseudo sub menus. Really, this is an ugly hack. | |
* - Reasonably well behaved/integrated considering it's an external application. | |
* | |
* TODO-ish: | |
* - Proper sub menus (TBD protocol, tk relaunch), tooltips, other widgets (not). | |
* - Possibly different menus for different bindings or states. | |
* | |
* Setup: | |
* - Make sure Tcl/Tk is installed and `wish` is accessible and works. | |
* - Alternatively, configure `interpreter` below to `tclsh`, which may work smoother. | |
* - For windows, download a zip from http://www.tcl3d.org/html/appTclkits.html | |
* extract and then rename to wish.exe and put it at the path or at the mpv.exe dir. | |
* - Or, tclsh/wish from git/msys2(mingw) works too - set `interpreter` below. | |
* - Put context.lua (this file) and context.tcl at the mpv scripts dir. | |
* - Add a key/mouse binding at input.conf, e.g. "MOUSE_BTN2 script_message contextmenu" | |
* - Once it works, configure the context_menu items below to your liking. | |
* | |
* 2017-02-02 - Version 0.1 - initial version | |
* | |
*************************************************************** | |
--]] | |
--[[ ************ CONFIG: start ************ ]]-- | |
-- context_menu is an array of items, where each item is an array of: | |
-- - Display string or a function which returns such string, or "-" for separator. | |
-- - Command string or a function which is executed on click. Empty to disable/gray. | |
-- - Optional re-launch: a submenu array, or true to "keep" the same menu open. | |
function noop() end | |
local prop_native = mp.get_property_native | |
local context_menu = { | |
{function() return prop_native("mute") and "Un-mute" or "Mute" end, "cycle mute"}, | |
{"* Volume Up", "add volume 10", true}, | |
{"* Volume Down", "add volume -10", true}, | |
{function() return "[ Volume: " .. tostring(math.floor(prop_native("volume"))) .. " ]" end}, | |
{"-"}, | |
{"* Size: orig / 2", "set window-scale 0.5", true}, | |
{"* Size: orig 1:1", "set window-scale 1.0", true}, | |
{"* Size: orig x 2", "set window-scale 2.0", true}, | |
{"-"}, | |
{"Pseudo sub-menu -->", noop, { | |
{"* Press space with the mouse!", "keypress SPACE", true}, | |
{"GOTO 0", "set time-pos 0"}, | |
{"Another pseudo sub-menu -->", noop, { | |
{"Yay!", "show_text Yay!"}, | |
{"* Yay+!", function() mp.osd_message("Yay! " .. tostring(math.random())) end, true}, | |
}}, | |
}}, | |
{"-"}, | |
{"Quit watch-later", "quit-watch-later"}, | |
{"Quit", "quit"}, | |
{"-"}, | |
{"Dismiss", noop}, | |
} | |
local verbose = false -- true -> dump console messages also without -v | |
local interpreter = "wish"; -- tclsh/wish/full-path | |
local menuscript = mp.find_config_file("scripts/context.tcl") | |
--[[ ************ CONFIG: end ************ ]]-- | |
function info(x) mp.msg[verbose and "info" or "verbose"](x) end | |
local utils = require 'mp.utils' | |
local function do_menu(items, x, y) | |
local args = {interpreter, menuscript, tostring(x), tostring(y)} | |
for i = 1, #items do | |
local item = items[i] | |
args[#args+1] = (type(item[1]) == "string") and item[1] or item[1]() | |
args[#args+1] = item[2] and tostring(i) or "" | |
end | |
local ret = utils.subprocess({ | |
args = args, | |
cancellable = true | |
}) | |
if (ret.status ~= 0) then | |
mp.osd_message("Something happened ...") | |
return | |
end | |
info("ret: " .. ret.stdout) | |
local res = utils.parse_json(ret.stdout) | |
x = tonumber(res.x) | |
y = tonumber(res.y) | |
res.rv = tonumber(res.rv) | |
if (res.rv == -1) then | |
info("Context menu cancelled") | |
return | |
end | |
local item = items[res.rv] | |
if (not (item and item[2])) then | |
mp.msg.error("Unknown menu item index: " .. tostring(res.rv)) | |
return | |
end | |
-- run the command | |
if (type(item[2]) == "string") then | |
mp.command(item[2]) | |
else | |
item[2]() | |
end | |
-- re-launch | |
if (item[3]) then | |
if (type(item[3]) ~= "boolean") then | |
items = item[3] -- sub-menu, launch at mouse position | |
x = -1 | |
y = -1 | |
end | |
-- Break direct recursion with async, stack overflow can come quick. | |
-- Also allow to un-congest the events queue. | |
mp.add_timeout(0, function() do_menu(items, x, y) end) | |
end | |
end | |
mp.register_script_message("contextmenu", function() | |
do_menu(context_menu, -1, -1) | |
end) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ############################################################# | |
# Context menu constructed via CLI args. Mostly proof of concept. | |
# Avi Halachmi (:avih) https://github.com/avih | |
# | |
# Developed for and used in conjunction with context.lua - context-menu for mpv. | |
# See context.lua for more info. | |
# | |
# 2017-02-02 - Version 0.1 - initial version | |
# ############################################################# | |
# Required when launching via tclsh, no-op when launching via wish | |
package require Tk | |
# Remove the main window from the host window manager | |
wm withdraw . | |
if { $::argc < 4 } { | |
puts "Usage: context.tcl x y item1 rv1 [item2 rv2 ...]" | |
exit 1 | |
} | |
# construct the menu from argv: | |
# - First pair is absolute x, y menu position, or under the mouse if -1, -1 | |
# - The rest of the pairs are display-string, return-value-on-click. | |
# If the return value is empty then the display item is disabled, but if the | |
# display is "-" (and empty rv) then a separator is added instead of an item. | |
# - For now, return-value is expected to be a number, and -1 is reserved for cancel. | |
# | |
# On item-click/menu-dismissed, we print a json object to stdout with the | |
# keys x, y (menu absolute position) and rv (return value) - all numbers. | |
set RV_CANCEL -1 | |
set m [menu .popupMenu -tearoff 0] | |
set first 1 | |
foreach {disp rv} $::argv { | |
if {$first} { | |
set pos_x $disp | |
set pos_y $rv | |
set first 0 | |
continue | |
} | |
if {$rv == ""} { | |
if {$disp == "-"} { | |
$m add separator | |
} else { | |
$m add command -state disabled -label "$disp" | |
} | |
} else { | |
$m add command -label "$disp" -command "done $rv" | |
} | |
} | |
# Read the absolute mouse pointer position if we're not given a pos via argv | |
if {$pos_x == -1 && $pos_y == -1} { | |
set pos_x [winfo pointerx .] | |
set pos_y [winfo pointery .] | |
} | |
proc done {rv} { | |
puts -nonewline "{\"x\":\"$::pos_x\", \"y\":\"$::pos_y\", \"rv\":\"$rv\"}" | |
exit | |
} | |
# Seemingly, on both windows and linux, "cancelled" is reached after the click but | |
# before the menu command is executed and _a_sync to it. Therefore we wait a bit to | |
# allow the menu command to execute first (and exit), and if it didn't, we exit here. | |
proc cancelled {} { | |
after 100 {done $::RV_CANCEL} | |
} | |
# Calculate the menu position relative to the Tk window | |
set win_x [expr {$pos_x - [winfo rootx .]}] | |
set win_y [expr {$pos_y - [winfo rooty .]}] | |
# Launch the popup menu | |
tk_popup .popupMenu $win_x $win_y | |
# On Windows tk_popup is synchronous and so we exit when it closes, but on Linux | |
# it's async and so we need to bind to the <Unmap> event (<Destroyed> or | |
# <FocusOut> don't work as expected, e.g. when clicking elsewhere even if the | |
# popup disappears. <Leave> works but it's an unexpected behavior for a menu). | |
# Note: if we don't catch the right event, we'd have a zombie process since no | |
# window. Equally important - the script will not exit. | |
# Note: untested on macOS (macports' tk requires xorg. meh). | |
if {$tcl_platform(platform) == "windows"} { | |
cancelled | |
} else { | |
bind .popupMenu <Unmap> cancelled | |
} |
Also, is there a specific license for this code? I'd like to ensure my forking this doesn't run afoul of any relevant license.
Let's say MIT :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This was really useful as a basis for creating a right-click menu and was easier to figure out how to modify than the mpvmenu python script. I did end up forking this and heavily reworked it into mpvcontextmenu in case it helps you, particularly if you end up looking at adding cascading sub-menus rather than the pseudo ones.
Also, is there a specific license for this code? I'd like to ensure my forking this doesn't run afoul of any relevant license.