diff --git a/DOCS/interface-changes/add-win32-context-menu.txt b/DOCS/interface-changes/add-win32-context-menu.txt new file mode 100644 index 0000000000..10cc63b0ef --- /dev/null +++ b/DOCS/interface-changes/add-win32-context-menu.txt @@ -0,0 +1,2 @@ +add `context-menu` command +add `menu-data` property diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 341d20c926..ff68d7d146 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -927,6 +927,9 @@ Remember to quote string arguments in input.conf (see `Flat command syntax`_). Do not change current track selections. +``context-menu`` + Show context menu on the video window. See `Context Menu`_ section for details. + Input Commands that are Possibly Subject to Change -------------------------------------------------- @@ -3454,6 +3457,48 @@ Property list and not using raw mode, the underlying content will be given (e.g. strings will be printed directly, rather than quoted and JSON-escaped). +``menu-data`` (RW) + This property stores the raw menu definition. See `Context Menu`_ section for details. + + ``type`` + Menu item type. Can be: ``separator``, ``submenu``, or empty. + + ``title`` + Menu item title. Required if type is not ``separator``. + + ``cmd`` + Command to execute when the menu item is clicked. + + ``shortcut`` + Menu item shortcut key which appears to the right of the menu item. + A shortcut key does not have to be functional; it's just a visual hint. + + ``state`` + Menu item state. Can be: ``checked``, ``disabled``, ``hidden``, or empty. + + ``submenu`` + Submenu items, which is required if type is ``submenu``. + + When querying the property with the client API using ``MPV_FORMAT_NODE``, or with + Lua ``mp.get_property_native``, this will return a mpv_node with the following + contents: + + :: + + MPV_FORMAT_NODE_ARRAY + MPV_FORMAT_NODE_MAP (menu item) + "type" MPV_FORMAT_STRING + "title" MPV_FORMAT_STRING + "cmd" MPV_FORMAT_STRING + "shortcut" MPV_FORMAT_STRING + "state" MPV_FORMAT_NODE_ARRAY[MPV_FORMAT_STRING] + "submenu" MPV_FORMAT_NODE_ARRAY[menu item] + + Writing to this property with the client API using ``MPV_FORMAT_NODE`` or with + Lua ``mp.set_property_native`` will trigger an immediate update of the menu if + mpv video output is currently active. You may observe the ``current-vo`` + property to check if this is the case. + ``working-directory`` The working directory of the mpv process. Can be useful for JSON IPC users, because the command line player usually works with relative paths. diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index 93e9207061..d1711fcb22 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -310,6 +310,21 @@ Wheel left/right Ctrl+Wheel up/down Change video zoom. +Context Menu +------------- + +.. warning:: + + This feature is experimental. It may not work with all VOs. A libass based + fallback may be implemented in the future. + +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/meson.build b/meson.build index c59efc0864..6094f17a35 100644 --- a/meson.build +++ b/meson.build @@ -503,7 +503,8 @@ if features['win32-desktop'] 'osdep/terminal-win.c', 'video/out/w32_common.c', 'video/out/win32/displayconfig.c', - 'video/out/win32/droptarget.c') + 'video/out/win32/droptarget.c', + 'video/out/win32/menu.c') main_fn_source = files('osdep/main-fn-win.c') endif diff --git a/player/command.c b/player/command.c index bcac0828e0..9f0af44663 100644 --- a/player/command.c +++ b/player/command.c @@ -114,6 +114,7 @@ struct command_ctx { char **script_props; mpv_node udata; + mpv_node mdata; double cached_window_scale; }; @@ -126,6 +127,10 @@ static const struct m_option udata_type = { .type = CONF_TYPE_NODE }; +static const struct m_option mdata_type = { + .type = CONF_TYPE_NODE +}; + struct overlay { struct mp_image *source; int x, y; @@ -3730,6 +3735,35 @@ static int mp_property_bindings(void *ctx, struct m_property *prop, return M_PROPERTY_NOT_IMPLEMENTED; } +static int mp_property_mdata(void *ctx, struct m_property *prop, + int action, void *arg) +{ + MPContext *mpctx = ctx; + mpv_node *node = &mpctx->command_ctx->mdata; + + switch (action) { + case M_PROPERTY_GET_TYPE: + *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE}; + return M_PROPERTY_OK; + case M_PROPERTY_GET: + case M_PROPERTY_GET_NODE: + m_option_copy(&mdata_type, arg, node); + return M_PROPERTY_OK; + case M_PROPERTY_SET: + case M_PROPERTY_SET_NODE: { + m_option_copy(&mdata_type, node, arg); + talloc_steal(mpctx->command_ctx, node_get_alloc(node)); + mp_notify_property(mpctx, prop->name); + + struct vo *vo = mpctx->video_out; + if (vo) + vo_control(vo, VOCTRL_UPDATE_MENU, arg); + 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 { @@ -4109,6 +4143,8 @@ static const struct m_property mp_properties_base[] = { {"command-list", mp_property_commands}, {"input-bindings", mp_property_bindings}, + {"menu-data", mp_property_mdata}, + {"user-data", mp_property_udata}, {"term-size", mp_property_term_size}, @@ -6567,6 +6603,16 @@ static void cmd_begin_vo_dragging(void *p) vo_control(vo, VOCTRL_BEGIN_DRAGGING, NULL); } +static void cmd_context_menu(void *p) +{ + struct mp_cmd_ctx *cmd = p; + struct MPContext *mpctx = cmd->mpctx; + struct vo *vo = mpctx->video_out; + + if (vo) + vo_control(vo, VOCTRL_SHOW_MENU, NULL); +} + /* This array defines all known commands. * The first field the command name used in libmpv and input.conf. * The second field is the handler function (see mp_cmd_def.handler and @@ -7039,6 +7085,8 @@ const struct mp_cmd_def mp_cmds[] = { { "begin-vo-dragging", cmd_begin_vo_dragging }, + { "context-menu", cmd_context_menu }, + {0} }; @@ -7123,6 +7171,9 @@ void command_init(struct MPContext *mpctx) ctx->properties[count++] = prop; } + node_init(&ctx->mdata, MPV_FORMAT_NODE_ARRAY, NULL); + talloc_steal(ctx, ctx->mdata.u.list); + node_init(&ctx->udata, MPV_FORMAT_NODE_MAP, NULL); talloc_steal(ctx, ctx->udata.u.list); talloc_free(prop_names); diff --git a/video/out/vo.h b/video/out/vo.h index eb92d4fc5b..313c08aafc 100644 --- a/video/out/vo.h +++ b/video/out/vo.h @@ -125,6 +125,10 @@ enum mp_voctrl { // Begin VO dragging. VOCTRL_BEGIN_DRAGGING, + + // Native context menu + VOCTRL_SHOW_MENU, + VOCTRL_UPDATE_MENU, }; // Helper to expose what kind of content is currently playing to the VO. diff --git a/video/out/w32_common.c b/video/out/w32_common.c index 66d1fc4701..36f48b9be7 100644 --- a/video/out/w32_common.c +++ b/video/out/w32_common.c @@ -40,6 +40,7 @@ #include "w32_common.h" #include "win32/displayconfig.h" #include "win32/droptarget.h" +#include "win32/menu.h" #include "osdep/io.h" #include "osdep/threads.h" #include "osdep/w32_keyboard.h" @@ -82,6 +83,8 @@ typedef enum MONITOR_DPI_TYPE { #define rect_w(r) ((r).right - (r).left) #define rect_h(r) ((r).bottom - (r).top) +#define WM_SHOWMENU (WM_USER + 1) + struct w32_api { HRESULT (WINAPI *pGetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); BOOL (WINAPI *pAdjustWindowRectExForDpi)(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi); @@ -109,6 +112,8 @@ struct vo_w32_state { HHOOK parent_win_hook; HWINEVENTHOOK parent_evt_hook; + struct menu_ctx *menu_ctx; + HMONITOR monitor; // Handle of the current screen char *color_profile; // Path of the current screen's color profile @@ -1486,6 +1491,14 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, w32->window = NULL; PostQuitMessage(0); break; + case WM_COMMAND: { + const char *cmd = mp_win32_menu_get_cmd(w32->menu_ctx, LOWORD(wParam)); + if (cmd) { + mp_cmd_t *cmdt = mp_input_parse_cmd(w32->input_ctx, bstr0(cmd), ""); + mp_input_queue_cmd(w32->input_ctx, cmdt); + } + break; + } case WM_SYSCOMMAND: switch (wParam & 0xFFF0) { case SC_SCREENSAVE: @@ -1687,6 +1700,9 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, return 0; } break; + case WM_SHOWMENU: + mp_win32_menu_show(w32->menu_ctx, w32->window); + break; } if (message == w32->tbtn_created_msg) { @@ -2060,6 +2076,7 @@ bool vo_w32_init(struct vo *vo) .dispatch = mp_dispatch_create(w32), }; w32->opts = w32->opts_cache->opts; + w32->menu_ctx = mp_win32_menu_init(); vo->w32 = w32; if (mp_thread_create(&w32->thread, gui_thread, w32)) @@ -2272,6 +2289,12 @@ static int gui_thread_control(struct vo_w32_state *w32, int request, void *arg) case VOCTRL_BEGIN_DRAGGING: w32->start_dragging = true; return VO_TRUE; + case VOCTRL_SHOW_MENU: + PostMessageW(w32->window, WM_SHOWMENU, 0, 0); + return VO_TRUE; + case VOCTRL_UPDATE_MENU: + mp_win32_menu_update(w32->menu_ctx, (struct mpv_node *)arg); + return VO_TRUE; } return VO_NOTIMPL; } @@ -2335,6 +2358,7 @@ void vo_w32_uninit(struct vo *vo) AvRevertMmThreadCharacteristics(w32->avrt_handle); + mp_win32_menu_uninit(w32->menu_ctx); talloc_free(w32); vo->w32 = NULL; } diff --git a/video/out/win32/menu.c b/video/out/win32/menu.c new file mode 100644 index 0000000000..25681e8ae3 --- /dev/null +++ b/video/out/win32/menu.c @@ -0,0 +1,231 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include +#include + +#include "libmpv/client.h" +#include "osdep/io.h" +#include "mpv_talloc.h" + +#include "menu.h" + +struct menu_ctx { + HMENU menu; + void *ta_data; // talloc context for MENUITEMINFOW.dwItemData +}; + +// append menu item to HMENU +static int append_menu(HMENU hmenu, UINT fMask, UINT fType, UINT fState, + wchar_t *title, HMENU submenu, void *data) +{ + static UINT id = WM_USER + 100; + MENUITEMINFOW mii = {0}; + + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_ID | fMask; + mii.wID = id++; + + if (fMask & MIIM_FTYPE) + mii.fType = fType; + if (fMask & MIIM_STATE) + mii.fState = fState; + if (fMask & MIIM_STRING) { + mii.dwTypeData = title; + mii.cch = wcslen(title); + } + if (fMask & MIIM_SUBMENU) + mii.hSubMenu = submenu; + if (fMask & MIIM_DATA) + mii.dwItemData = (ULONG_PTR)data; + + return InsertMenuItemW(hmenu, -1, TRUE, &mii) ? mii.wID : -1; +} + +// build fState for menu item creation +static int build_state(mpv_node *node) +{ + int fState = 0; + for (int i = 0; i < node->u.list->num; i++) { + mpv_node *item = &node->u.list->values[i]; + if (item->format != MPV_FORMAT_STRING) + continue; + + if (strcmp(item->u.string, "hidden") == 0) { + return -1; + } else if (strcmp(item->u.string, "checked") == 0) { + fState |= MFS_CHECKED; + } else if (strcmp(item->u.string, "disabled") == 0) { + fState |= MFS_DISABLED; + } + } + return fState; +} + +// build dwTypeData for menu item creation +static wchar_t *build_title(void *talloc_ctx, char *title, char *shortcut) +{ + if (shortcut && shortcut[0]) { + char *buf = talloc_asprintf(NULL, "%s\t%s", title, shortcut); + wchar_t *wbuf = mp_from_utf8(talloc_ctx, buf); + talloc_free(buf); + return wbuf; + } + return mp_from_utf8(talloc_ctx, title); +} + +// build HMENU from mpv node +// +// node structure: +// +// MPV_FORMAT_NODE_ARRAY +// MPV_FORMAT_NODE_MAP (menu item) +// "type" MPV_FORMAT_STRING +// "title" MPV_FORMAT_STRING +// "cmd" MPV_FORMAT_STRING +// "shortcut" MPV_FORMAT_STRING +// "state" MPV_FORMAT_NODE_ARRAY[MPV_FORMAT_STRING] +// "submenu" MPV_FORMAT_NODE_ARRAY[menu item] +static void build_menu(void *talloc_ctx, HMENU hmenu, struct mpv_node *node) +{ + if (node->format != MPV_FORMAT_NODE_ARRAY) + return; + + for (int i = 0; i < node->u.list->num; i++) { + mpv_node *item = &node->u.list->values[i]; + if (item->format != MPV_FORMAT_NODE_MAP) + continue; + + mpv_node_list *list = item->u.list; + + char *type = ""; + char *title = NULL; + char *cmd = NULL; + char *shortcut = NULL; + int fState = 0; + HMENU submenu = NULL; + + for (int j = 0; j < list->num; j++) { + char *key = list->keys[j]; + mpv_node *value = &list->values[j]; + + switch (value->format) { + case MPV_FORMAT_STRING: + if (strcmp(key, "title") == 0) { + title = value->u.string; + } else if (strcmp(key, "cmd") == 0) { + cmd = value->u.string; + } else if (strcmp(key, "type") == 0) { + type = value->u.string; + } else if (strcmp(key, "shortcut") == 0) { + shortcut = value->u.string; + } + break; + case MPV_FORMAT_NODE_ARRAY: + if (strcmp(key, "state") == 0) { + fState = build_state(value); + } else if (strcmp(key, "submenu") == 0) { + submenu = CreatePopupMenu(); + build_menu(talloc_ctx, submenu, value); + } + break; + default: + break; + } + } + + if (fState == -1) // hidden + continue; + + if (strcmp(type, "separator") == 0) { + append_menu(hmenu, MIIM_FTYPE, MFT_SEPARATOR, 0, NULL, NULL, NULL); + } else { + if (title == NULL || title[0] == '\0') + continue; + + UINT fMask = MIIM_STRING | MIIM_STATE; + bool grayed = false; + if (strcmp(type, "submenu") == 0) { + if (submenu == NULL) + submenu = CreatePopupMenu(); + fMask |= MIIM_SUBMENU; + grayed = GetMenuItemCount(submenu) == 0; + } else { + fMask |= MIIM_DATA; + grayed = cmd == NULL || cmd[0] == '\0' || cmd[0] == '#' || + strcmp(cmd, "ignore") == 0; + } + int id = append_menu(hmenu, fMask, 0, (UINT)fState, + build_title(talloc_ctx, title, shortcut), + submenu, talloc_strdup(talloc_ctx, cmd)); + if (id > 0 && grayed) + EnableMenuItem(hmenu, id, MF_BYCOMMAND | MF_GRAYED); + } + } +} + +struct menu_ctx *mp_win32_menu_init(void) +{ + struct menu_ctx *ctx = talloc_ptrtype(NULL, ctx); + ctx->menu = CreatePopupMenu(); + ctx->ta_data = talloc_new(ctx); + return ctx; +} + +void mp_win32_menu_uninit(struct menu_ctx *ctx) +{ + DestroyMenu(ctx->menu); + talloc_free(ctx); +} + +void mp_win32_menu_show(struct menu_ctx *ctx, HWND hwnd) +{ + POINT pt; + RECT rc; + + if (!GetCursorPos(&pt)) + return; + + GetClientRect(hwnd, &rc); + ScreenToClient(hwnd, &pt); + + if (!PtInRect(&rc, pt)) + return; + + ClientToScreen(hwnd, &pt); + TrackPopupMenuEx(ctx->menu, TPM_LEFTALIGN | TPM_LEFTBUTTON, pt.x, pt.y, + hwnd, NULL); +} + +void mp_win32_menu_update(struct menu_ctx *ctx, struct mpv_node *data) +{ + while (GetMenuItemCount(ctx->menu) > 0) + RemoveMenu(ctx->menu, 0, MF_BYPOSITION); + talloc_free_children(ctx->ta_data); + + build_menu(ctx->ta_data, ctx->menu, data); +} + +const char* mp_win32_menu_get_cmd(struct menu_ctx *ctx, UINT id) +{ + MENUITEMINFOW mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_DATA; + + GetMenuItemInfoW(ctx->menu, id, FALSE, &mii); + return (const char *)mii.dwItemData; +} diff --git a/video/out/win32/menu.h b/video/out/win32/menu.h new file mode 100644 index 0000000000..8b1fe72c19 --- /dev/null +++ b/video/out/win32/menu.h @@ -0,0 +1,32 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#ifndef MP_WIN32_MENU_H +#define MP_WIN32_MENU_H + +#include + +struct mpv_node; +struct menu_ctx; + +struct menu_ctx *mp_win32_menu_init(void); +void mp_win32_menu_uninit(struct menu_ctx *ctx); +void mp_win32_menu_show(struct menu_ctx *ctx, HWND hwnd); +void mp_win32_menu_update(struct menu_ctx *ctx, struct mpv_node *data); +const char* mp_win32_menu_get_cmd(struct menu_ctx *ctx, UINT id); + +#endif