diff --git a/CMakeLists.txt b/CMakeLists.txt index cc25a53..e66f0a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(menu SHARED mpv/ta/ta_talloc.c mpv/ta/ta_utils.c + src/dialog.c src/menu.c src/plugin.c ) diff --git a/README.md b/README.md index 036bbd5..3ac1db7 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,53 @@ you may use the messages below if you only want to update part of the menu. > [!TIP] > Want a usage example? Check [Scripting example](https://github.com/tsl0922/mpv-menu-plugin/wiki/Scripting-example) in the wiki. -#### `menu-ready` +#### Script Messages supported by `menu.dll`: + +##### `clipboard/get ` + +Retrieves data from the clipboard (text only). + +The result is replied via: `srcript-message-to clipboard-get-reply `. + +##### `clipboard/set ` + +Places data on the clipboard (text only). + +##### `dialog/open ` + +Show an open dialog. + +The result is replied via: `srcript-message-to dialog-open-reply `. + +##### `dialog/open-multi ` + +Show an open dialog that can select multiple files. + +The result is replied via: `srcript-message-to dialog-open-multi-reply ...`. + +##### `dialog/open-folder ` + +Show an open dialog that can select folder only. + +The result is replied via: `srcript-message-to dialog-open-folder-reply `. + +##### `dialog/save ` + +Show a save dialog. + +The result is replied via: `srcript-message-to dialog-save-reply `. + +#### Script Messages supported by `dyn_menu.lua`: + +##### `menu-ready` Broadcasted when `dyn_menu.lua` has initialized itself. -#### `get ` +##### `get ` -Get the menu item structure of `keyword`, and send a json reply to `src`. +Get the menu item structure of `keyword`. + +The result is replied via: `srcript-message-to menu-get-reply `. ```json { @@ -127,11 +167,9 @@ Get the menu item structure of `keyword`, and send a json reply to `src`. } ``` -The reply is sent via `srcript-message-to menu-get-reply `. - If `keyword` is not found, the result json will contain an additional `error` field, and no `item` field. -#### `update ` +##### `update ` Update the menu item structure of `keyword` with `json`. diff --git a/src/dialog.c b/src/dialog.c new file mode 100644 index 0000000..c5e4e0b --- /dev/null +++ b/src/dialog.c @@ -0,0 +1,228 @@ +#include +#include +#include +#include "mpv_talloc.h" +#include "dialog.h" + +#define DIALOG_FILTER_PROP "user-data/menu/dialog/filters" +#define DIALOG_DEF_PATH_PROP "user-data/menu/dialog/default-path" +#define DIALOG_DEF_NAME_PROP "user-data/menu/dialog/default-name" + +// add filters to dialog from user property, default to first one +static void add_filters(mpv_handle *mpv, IFileDialog *pfd) { + mpv_node node = {0}; + if (mpv_get_property(mpv, DIALOG_FILTER_PROP, MPV_FORMAT_NODE, &node) < 0) + return; + if (node.format != MPV_FORMAT_NODE_ARRAY) goto done; + + void *tmp = talloc_new(NULL); + mpv_node_list *list = node.u.list; + COMDLG_FILTERSPEC *specs = talloc_array(tmp, COMDLG_FILTERSPEC, list->num); + UINT count = 0; + + for (int i = 0; i < list->num; i++) { + mpv_node *item = &list->values[i]; + if (item->format != MPV_FORMAT_NODE_MAP) continue; + + char *name = NULL, *spec = NULL; + mpv_node_list *values = item->u.list; + + for (int j = 0; j < values->num; j++) { + char *key = values->keys[j]; + mpv_node *value = &values->values[j]; + if (value->format != MPV_FORMAT_STRING) continue; + + if (strcmp(key, "name") == 0) name = value->u.string; + if (strcmp(key, "spec") == 0) spec = value->u.string; + } + + if (name != NULL && spec != NULL) { + specs[count].pszName = mp_from_utf8(tmp, name); + specs[count].pszSpec = mp_from_utf8(tmp, spec); + count++; + } + } + + if (count > 0) { + pfd->lpVtbl->SetFileTypes(pfd, count, specs); + pfd->lpVtbl->SetFileTypeIndex(pfd, 1); + pfd->lpVtbl->SetDefaultExtension(pfd, specs[0].pszSpec); + } + + talloc_free(tmp); + +done: + mpv_free_node_contents(&node); +} + +// set default path from user property +static void set_default_path(mpv_handle *mpv, IFileDialog *pfd) { + char *path = mpv_get_property_string(mpv, DIALOG_DEF_PATH_PROP); + if (path == NULL) return; + + IShellItem *folder; + wchar_t *w_path = mp_from_utf8(NULL, path); + + if (SUCCEEDED(SHCreateItemFromParsingName(w_path, NULL, &IID_IShellItem, + (void **)&folder))) + pfd->lpVtbl->SetDefaultFolder(pfd, folder); + + talloc_free(w_path); + mpv_free(path); +} + +// set default name used for save dialog +static void set_default_name(mpv_handle *mpv, IFileDialog *pfd) { + char *name = mpv_get_property_string(mpv, DIALOG_DEF_NAME_PROP); + if (name == NULL) return; + + wchar_t *w_name = mp_from_utf8(NULL, name); + pfd->lpVtbl->SetFileName(pfd, w_name); + talloc_free(w_name); +} + +static void add_options(IFileDialog *pfd, DWORD options) { + DWORD dwOptions; + if (pfd->lpVtbl->GetOptions(pfd, &dwOptions) == S_OK) { + pfd->lpVtbl->SetOptions(pfd, dwOptions | options); + } +} + +// single file open dialog +char *open_dialog(void *talloc_ctx, plugin_ctx *ctx) { + IFileOpenDialog *pfd = NULL; + if (FAILED(CoCreateInstance(&CLSID_FileOpenDialog, NULL, + CLSCTX_INPROC_SERVER, &IID_IFileDialog, + (void **)&pfd))) + return NULL; + + add_filters(ctx->mpv, (IFileDialog *)pfd); + set_default_path(ctx->mpv, (IFileDialog *)pfd); + add_options((IFileDialog *)pfd, FOS_FORCEFILESYSTEM); + + char *path = NULL; + + if (SUCCEEDED(pfd->lpVtbl->Show(pfd, ctx->hwnd))) { + IShellItem *psi; + if (SUCCEEDED(pfd->lpVtbl->GetResult(pfd, &psi))) { + wchar_t *w_path; + if (SUCCEEDED(psi->lpVtbl->GetDisplayName(psi, SIGDN_FILESYSPATH, + &w_path))) { + path = mp_to_utf8(talloc_ctx, w_path); + CoTaskMemFree(w_path); + } + psi->lpVtbl->Release(psi); + } + } + + pfd->lpVtbl->Release(pfd); + + return path; +} + +// multiple file open dialog +char **open_dialog_multi(void *talloc_ctx, plugin_ctx *ctx) { + IFileOpenDialog *pfd = NULL; + if (FAILED(CoCreateInstance(&CLSID_FileOpenDialog, NULL, + CLSCTX_INPROC_SERVER, &IID_IFileOpenDialog, + (void **)&pfd))) + return NULL; + + add_filters(ctx->mpv, (IFileDialog *)pfd); + set_default_path(ctx->mpv, (IFileDialog *)pfd); + add_options((IFileDialog *)pfd, FOS_FORCEFILESYSTEM | FOS_ALLOWMULTISELECT); + + char **paths = NULL; + + if (SUCCEEDED(pfd->lpVtbl->Show(pfd, ctx->hwnd))) { + IShellItemArray *psia; + if (SUCCEEDED(pfd->lpVtbl->GetResults(pfd, &psia))) { + DWORD count; + if (SUCCEEDED(psia->lpVtbl->GetCount(psia, &count))) { + paths = + talloc_zero_size(talloc_ctx, sizeof(char *) * (count + 1)); + for (DWORD i = 0; i < count; i++) { + IShellItem *psi; + if (SUCCEEDED(psia->lpVtbl->GetItemAt(psia, i, &psi))) { + wchar_t *w_path; + if (SUCCEEDED(psi->lpVtbl->GetDisplayName( + psi, SIGDN_FILESYSPATH, &w_path))) { + paths[i] = mp_to_utf8(talloc_ctx, w_path); + CoTaskMemFree(w_path); + } + psi->lpVtbl->Release(psi); + } + } + } + psia->lpVtbl->Release(psia); + } + } + + pfd->lpVtbl->Release(pfd); + + return paths; +} + +// folder open dialog +char *open_folder(void *talloc_ctx, plugin_ctx *ctx) { + IFileOpenDialog *pfd = NULL; + if (FAILED(CoCreateInstance(&CLSID_FileOpenDialog, NULL, + CLSCTX_INPROC_SERVER, &IID_IFileOpenDialog, + (void **)&pfd))) + return NULL; + + set_default_path(ctx->mpv, (IFileDialog *)pfd); + add_options((IFileDialog *)pfd, FOS_FORCEFILESYSTEM | FOS_PICKFOLDERS); + + char *path = NULL; + + if (SUCCEEDED(pfd->lpVtbl->Show(pfd, ctx->hwnd))) { + IShellItem *psi; + if (SUCCEEDED(pfd->lpVtbl->GetResult(pfd, &psi))) { + wchar_t *w_path; + if (SUCCEEDED(psi->lpVtbl->GetDisplayName(psi, SIGDN_FILESYSPATH, + &w_path))) { + path = mp_to_utf8(talloc_ctx, w_path); + CoTaskMemFree(w_path); + } + psi->lpVtbl->Release(psi); + } + } + + pfd->lpVtbl->Release(pfd); + + return path; +} + +// save dialog +char *save_dialog(void *talloc_ctx, plugin_ctx *ctx) { + IFileSaveDialog *pfd = NULL; + if (FAILED(CoCreateInstance(&CLSID_FileSaveDialog, NULL, + CLSCTX_INPROC_SERVER, &IID_IFileSaveDialog, + (void **)&pfd))) + return NULL; + + add_filters(ctx->mpv, (IFileDialog *)pfd); + set_default_path(ctx->mpv, (IFileDialog *)pfd); + set_default_name(ctx->mpv, (IFileDialog *)pfd); + add_options((IFileDialog *)pfd, FOS_FORCEFILESYSTEM); + + char *path = NULL; + + if (SUCCEEDED(pfd->lpVtbl->Show(pfd, ctx->hwnd))) { + IShellItem *psi; + if (SUCCEEDED(pfd->lpVtbl->GetResult(pfd, &psi))) { + wchar_t *w_path; + if (SUCCEEDED(psi->lpVtbl->GetDisplayName(psi, SIGDN_FILESYSPATH, + &w_path))) { + path = mp_to_utf8(talloc_ctx, w_path); + CoTaskMemFree(w_path); + } + psi->lpVtbl->Release(psi); + } + } + + pfd->lpVtbl->Release(pfd); + + return path; +} \ No newline at end of file diff --git a/src/dialog.h b/src/dialog.h new file mode 100644 index 0000000..36b5e85 --- /dev/null +++ b/src/dialog.h @@ -0,0 +1,13 @@ +// Copyright (c) 2023 tsl0922. All rights reserved. +// SPDX-License-Identifier: GPL-2.0-only + +#ifndef MPV_PLUGIN_DIALOG_H +#define MPV_PLUGIN_DIALOG_H + +#include "plugin.h" + +char *open_dialog(void *talloc_ctx, plugin_ctx *ctx); +char **open_dialog_multi(void *talloc_ctx, plugin_ctx *ctx); +char *open_folder(void *talloc_ctx, plugin_ctx *ctx); +char *save_dialog(void *talloc_ctx, plugin_ctx *ctx); +#endif \ No newline at end of file diff --git a/src/plugin.c b/src/plugin.c index 0dbf3d7..7d459d6 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -3,10 +3,12 @@ #include #include +#include #include #include #include "misc/bstr.h" #include "misc/ctype.h" +#include "dialog.h" #include "menu.h" #include "plugin.h" @@ -34,6 +36,39 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { return CallWindowProcW(ctx->wnd_proc, hWnd, uMsg, wParam, lParam); } +static char *get_clipboard(void *talloc_ctx) { + if (!OpenClipboard(ctx->hwnd)) return NULL; + + HANDLE hData = GetClipboardData(CF_TEXT); + char *ret = NULL; + if (hData != NULL) { + char *data = (char *)GlobalLock(hData); + if (data != NULL) { + ret = talloc_strdup(talloc_ctx, data); + GlobalUnlock(hData); + } + } + CloseClipboard(); + + return ret; +} + +static void set_clipboard(char *text) { + if (!OpenClipboard(ctx->hwnd)) return; + EmptyClipboard(); + + HGLOBAL hData = GlobalAlloc(GMEM_MOVEABLE, strlen(text) + 1); + if (hData != NULL) { + char *data = (char *)GlobalLock(hData); + if (data != NULL) { + strcpy(data, text); + GlobalUnlock(hData); + SetClipboardData(CF_TEXT, hData); + } + } + CloseClipboard(); +} + // read and parse config file static void read_conf(plugin_config *conf, const char *name) { void *tmp = talloc_new(NULL); @@ -93,7 +128,65 @@ static void handle_client_message(mpv_event *event) { if (msg->num_args < 1) return; const char *cmd = msg->args[0]; - if (strcmp(cmd, "show") == 0) PostMessageW(ctx->hwnd, WM_SHOWMENU, 0, 0); + if (strcmp(cmd, "show") == 0) { + PostMessageW(ctx->hwnd, WM_SHOWMENU, 0, 0); + } else if (msg->num_args > 1) { + if (strcmp(cmd, "clipboard/get") == 0) { + char *text = get_clipboard(NULL); + if (text == NULL) return; + + mpv_command(ctx->mpv, + (const char *[]){"script-message-to", msg->args[1], + "clipboard-get-reply", text, NULL}); + talloc_free(text); + } else if (strcmp(cmd, "clipboard/set") == 0) { + set_clipboard(msg->args[1]); + } else if (strcmp(cmd, "dialog/open") == 0) { + char *path = open_dialog(NULL, ctx); + if (path == NULL) return; + + mpv_command(ctx->mpv, + (const char *[]){"script-message-to", msg->args[1], + "dialog-open-reply", path, NULL}); + talloc_free(path); + } else if (strcmp(cmd, "dialog/open-multi") == 0) { + void *tmp = talloc_new(NULL); + char **paths = open_dialog_multi(tmp, ctx); + if (paths == NULL) { + talloc_free(tmp); + return; + } + + int count = 0; + while (paths[count] != NULL) count++; + + char **args = talloc_array(tmp, char *, count + 4); + args[0] = talloc_strdup(tmp, "script-message-to"); + args[1] = talloc_strdup(tmp, msg->args[1]); + args[2] = talloc_strdup(tmp, "dialog-open-multi-reply"); + for (int i = 0; i < count; i++) args[i + 3] = paths[i]; + args[count + 3] = NULL; + + mpv_command(ctx->mpv, (const char **)args); + talloc_free(tmp); + } else if (strcmp(cmd, "dialog/open-folder") == 0) { + char *path = open_folder(NULL, ctx); + if (path == NULL) return; + + mpv_command(ctx->mpv, (const char *[]){ + "script-message-to", msg->args[1], + "dialog-open-folder-reply", path, NULL}); + talloc_free(path); + } else if (strcmp(cmd, "dialog/save") == 0) { + char *path = save_dialog(NULL, ctx); + if (path == NULL) return; + + mpv_command(ctx->mpv, + (const char *[]){"script-message-to", msg->args[1], + "dialog-save-reply", path, NULL}); + talloc_free(path); + } + } } // create and init plugin context @@ -119,6 +212,8 @@ static void destroy_plugin_ctx() { // entry point of plugin MPV_EXPORT int mpv_open_cplugin(mpv_handle *handle) { + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + create_plugin_ctx(handle); if (ctx->conf->load) { @@ -150,6 +245,8 @@ MPV_EXPORT int mpv_open_cplugin(mpv_handle *handle) { mpv_unobserve_property(handle, 0); destroy_plugin_ctx(); + CoUninitialize(); + return 0; } @@ -162,6 +259,15 @@ wchar_t *mp_from_utf8(void *talloc_ctx, const char *s) { return ret; } +// convert wchar_t string to utf8 +char *mp_to_utf8(void *talloc_ctx, const wchar_t *s) { + int count = WideCharToMultiByte(CP_UTF8, 0, s, -1, NULL, 0, NULL, NULL); + if (count <= 0) abort(); + char *ret = talloc_array(talloc_ctx, char, count); + WideCharToMultiByte(CP_UTF8, 0, s, -1, ret, count, NULL, NULL); + return ret; +} + // get property value as string char *mp_get_prop_string(void *talloc_ctx, const char *prop) { char *val = mpv_get_property_string(ctx->mpv, prop); diff --git a/src/plugin.h b/src/plugin.h index 4d591a3..746e490 100644 --- a/src/plugin.h +++ b/src/plugin.h @@ -4,6 +4,7 @@ #ifndef MPV_PLUGIN_H #define MPV_PLUGIN_H +#include #include #include #include "misc/dispatch.h" @@ -27,6 +28,7 @@ typedef struct { } plugin_ctx; wchar_t *mp_from_utf8(void *talloc_ctx, const char *s); +char *mp_to_utf8(void *talloc_ctx, const wchar_t *s); char *mp_get_prop_string(void *talloc_ctx, const char *prop); char *mp_expand_path(void *talloc_ctx, char *path); char *mp_read_file(void *talloc_ctx, char *path);