diff --git a/README.md b/README.md index 529fb53..4496c4f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Ctrl+a show-text foobar #menu: Foo > Bar _ ignore #menu: - ``` -Add a keybinding that trigger the menu: +Add a keybinding that trigger the menu (required): ``` MBTN_RIGHT script-message-to menu show diff --git a/src/menu.c b/src/menu.c index 947f7b9..7e5d78a 100644 --- a/src/menu.c +++ b/src/menu.c @@ -6,12 +6,61 @@ #define MENU_PREFIX "#menu:" #define MENU_PREFIX_UOSC "#!" +#define MENU_PREFIX_DYN "#@" -struct item_data { - char *key; - char *cmd; +struct dyn_provider { + char *keyword; // keyword in menu title + void (*update)(void *talloc_ctx, HMENU hmenu); }; +struct dyn_menu { + HMENU hmenu; // submenu handle + void *talloc_ctx; // talloc context + void (*update)(void *talloc_ctx, HMENU hmenu); +}; + +struct dyn_menu_list { + struct dyn_menu *entries; + int num_entries; +}; + +// forward declarations for dynamic menu update functions +static void update_video_track_menu(void *talloc_ctx, HMENU hmenu); +static void update_audio_track_menu(void *talloc_ctx, HMENU hmenu); +static void update_sub_track_menu(void *talloc_ctx, HMENU hmenu); +static void update_secondary_sub_track_menu(void *talloc_ctx, HMENU hmenu); +static void update_chapter_menu(void *talloc_ctx, HMENU hmenu); +static void update_audio_device_menu(void *talloc_ctx, HMENU hmenu); + +// dynamic menu providers +static const struct dyn_provider dyn_providers[] = { + {"tracks/video", update_video_track_menu}, + {"tracks/audio", update_audio_track_menu}, + {"tracks/sub", update_sub_track_menu}, + {"tracks/sub-secondary", update_secondary_sub_track_menu}, + {"chapters", update_chapter_menu}, + {"audio-devices", update_audio_device_menu}, +}; + +// dynamic menu list +static struct dyn_menu_list *dyn_menus = NULL; + +static bool add_dyn_menu(void *talloc_ctx, HMENU hmenu, bstr keyword) { + for (int i = 0; i < ARRAYSIZE(dyn_providers); i++) { + struct dyn_provider provider = dyn_providers[i]; + if (!bstr_equals0(keyword, provider.keyword)) continue; + + struct dyn_menu item; + item.hmenu = hmenu; + item.talloc_ctx = talloc_new(talloc_ctx); + item.update = provider.update; + MP_TARRAY_APPEND(talloc_ctx, dyn_menus->entries, dyn_menus->num_entries, + item); + return true; + } + return false; +} + static HMENU find_submenu(HMENU hmenu, wchar_t *name) { MENUITEMINFOW mii; int count = GetMenuItemCount(hmenu); @@ -61,8 +110,8 @@ static wchar_t *format_title(void *talloc_ctx, bstr name, bstr key) { } // return submenu if it is, otherwise NULL -static HMENU append_menu(HMENU hmenu, UINT fMask, UINT fType, wchar_t *title, - void *data) { +static HMENU append_menu(HMENU hmenu, UINT fMask, UINT fType, UINT fState, + wchar_t *title, void *data) { static UINT id = 0; MENUITEMINFOW mii; memset(&mii, 0, sizeof(mii)); @@ -81,6 +130,8 @@ static HMENU append_menu(HMENU hmenu, UINT fMask, UINT fType, wchar_t *title, } } + if (fMask & MIIM_STATE) mii.fState = fState; + if (fMask & MIIM_STRING) { mii.dwTypeData = title; mii.cch = wcslen(title); @@ -99,53 +150,174 @@ static HMENU append_menu(HMENU hmenu, UINT fMask, UINT fType, wchar_t *title, return NULL; } +static void update_track_menu(void *talloc_ctx, HMENU hmenu, const char *type, + const char *prop) { + while (GetMenuItemCount(hmenu) > 0) RemoveMenu(hmenu, 0, MF_BYPOSITION); + talloc_free_children(talloc_ctx); + + void *tmp = talloc_new(NULL); + struct mp_track_list *list = mp_get_track_list(tmp, type); + if (list == NULL) return; + + bool is_sub = strcmp(type, "sub") == 0; + int64_t pos = mp_get_prop_int(prop); + + for (int i = 0; i < list->num_entries; i++) { + struct track_item *entry = &list->entries[i]; + + char *title = entry->title; + if (title == NULL || strlen(title) == 0) + title = talloc_asprintf(tmp, "%s %d", type, entry->id); + if (entry->lang != NULL && strlen(entry->lang) > 0) + title = talloc_asprintf(tmp, "%s [%s]", title, entry->lang); + UINT fState = entry->selected ? MFS_CHECKED : MFS_UNCHECKED; + if (is_sub && entry->selected && pos != entry->id) + fState |= MFS_DISABLED; + append_menu(hmenu, MIIM_STRING | MIIM_DATA | MIIM_STATE, 0, fState, + escape_title(talloc_ctx, bstr0(title)), + talloc_asprintf(talloc_ctx, "set %s %d", prop, entry->id)); + } + + if (is_sub) { + append_menu(hmenu, MIIM_STRING | MIIM_DATA | MIIM_STATE, 0, + pos < 0 ? MFS_CHECKED : MFS_UNCHECKED, + escape_title(talloc_ctx, bstr0("Off")), + talloc_asprintf(talloc_ctx, "set %s no", prop)); + } + + talloc_free(tmp); +} + +static void update_video_track_menu(void *talloc_ctx, HMENU hmenu) { + update_track_menu(talloc_ctx, hmenu, "video", "vid"); +} + +static void update_audio_track_menu(void *talloc_ctx, HMENU hmenu) { + update_track_menu(talloc_ctx, hmenu, "audio", "aid"); +} + +static void update_sub_track_menu(void *talloc_ctx, HMENU hmenu) { + update_track_menu(talloc_ctx, hmenu, "sub", "sid"); +} + +static void update_secondary_sub_track_menu(void *talloc_ctx, HMENU hmenu) { + update_track_menu(talloc_ctx, hmenu, "sub", "secondary-sid"); +} + +static void update_chapter_menu(void *talloc_ctx, HMENU hmenu) { + while (GetMenuItemCount(hmenu) > 0) RemoveMenu(hmenu, 0, MF_BYPOSITION); + talloc_free_children(talloc_ctx); + + void *tmp = talloc_new(NULL); + struct mp_chapter_list *list = mp_get_chapter_list(tmp); + if (list == NULL) return; + + for (int i = 0; i < list->num_entries; i++) { + struct chapter_item *entry = &list->entries[i]; + + char *title = entry->title; + if (title == NULL || strlen(title) == 0) + title = talloc_asprintf(tmp, "chapter %d", i + 1); + append_menu( + hmenu, MIIM_STRING | MIIM_DATA, 0, 0, + escape_title(talloc_ctx, bstr0(title)), + talloc_asprintf(talloc_ctx, "seek %f absolute", entry->time)); + } + int64_t pos = mp_get_prop_int("chapter"); + if (pos >= 0) + CheckMenuRadioItem(hmenu, 0, list->num_entries, pos, MF_BYPOSITION); + + talloc_free(tmp); +} + +static void update_audio_device_menu(void *talloc_ctx, HMENU hmenu) { + while (GetMenuItemCount(hmenu) > 0) RemoveMenu(hmenu, 0, MF_BYPOSITION); + talloc_free_children(talloc_ctx); + + void *tmp = talloc_new(NULL); + struct mp_audio_device_list *list = mp_get_audio_device_list(tmp); + if (list == NULL) return; + + char *name = mp_get_prop_string(tmp, "audio-device"); + int pos = -1; + for (int i = 0; i < list->num_entries; i++) { + struct audio_device *entry = &list->entries[i]; + if (strcmp(entry->name, name) == 0) pos = i; + + char *title = entry->desc; + if (title == NULL || strlen(title) == 0) + title = talloc_strdup(tmp, entry->name); + append_menu( + hmenu, MIIM_STRING | MIIM_DATA, 0, 0, + escape_title(talloc_ctx, bstr0(title)), + talloc_asprintf(talloc_ctx, "set audio-device %s", entry->name)); + } + if (pos >= 0) + CheckMenuRadioItem(hmenu, 0, list->num_entries, pos, MF_BYPOSITION); + + talloc_free(tmp); +} + +static void dyn_menu_init(void *talloc_ctx) { + dyn_menus = talloc_ptrtype(talloc_ctx, dyn_menus); + memset(dyn_menus, 0, sizeof(*dyn_menus)); +} + static bool is_seprarator(bstr text, bool uosc) { return bstr_equals0(text, "-") || (uosc && bstr_startswith0(text, "---")); } static void parse_menu(void *talloc_ctx, HMENU hmenu, bstr key, bstr cmd, bstr text, bool uosc) { - bstr name, rest; + bstr name, rest, comment; + name = bstr_split(text, ">", &rest); + name = bstr_split(name, "#", &comment); name = bstr_strip(name); - if (rest.len == 0) { + if (!rest.len) { if (is_seprarator(name, uosc)) { - append_menu(hmenu, MIIM_FTYPE, MFT_SEPARATOR, NULL, NULL); + append_menu(hmenu, MIIM_FTYPE, MFT_SEPARATOR, 0, NULL, NULL); } else { - if (cmd.len > 0 && !bstr_startswith0(cmd, "#")) { - struct item_data *data = talloc_ptrtype(talloc_ctx, data); - data->key = bstrdup0(data, key); - data->cmd = bstrdup0(data, cmd); - append_menu(hmenu, MIIM_STRING | MIIM_DATA, 0, - format_title(talloc_ctx, name, key), data); + bool dynamic = bstr_eatstart0(&comment, MENU_PREFIX_DYN); + if (!dynamic && cmd.len > 0 && !bstr_startswith0(cmd, "#")) { + append_menu(hmenu, MIIM_STRING | MIIM_DATA, 0, 0, + format_title(talloc_ctx, name, key), + bstrdup0(talloc_ctx, cmd)); } else { - append_menu(hmenu, MIIM_STRING | MIIM_SUBMENU, 0, - escape_title(talloc_ctx, name), NULL); + HMENU submenu = + append_menu(hmenu, MIIM_STRING | MIIM_SUBMENU, 0, 0, + escape_title(talloc_ctx, name), NULL); + if (dynamic && comment.len > 0) { + bstr keyword = bstr_split(comment, "#", NULL); + keyword = bstr_rstrip(keyword); + add_dyn_menu(talloc_ctx, submenu, keyword); + } } } } else { - HMENU submenu = append_menu(hmenu, MIIM_STRING | MIIM_SUBMENU, 0, + HMENU submenu = append_menu(hmenu, MIIM_STRING | MIIM_SUBMENU, 0, 0, escape_title(talloc_ctx, name), NULL); - parse_menu(talloc_ctx, submenu, key, cmd, rest, uosc); + if (!comment.len) parse_menu(talloc_ctx, submenu, key, cmd, rest, uosc); } } static bool split_menu(bstr line, bstr *left, bstr *right, bool uosc) { - if (line.len == 0) return false; + if (!line.len) return false; if (!bstr_split_tok(line, MENU_PREFIX, left, right)) { if (!uosc || !bstr_split_tok(line, MENU_PREFIX_UOSC, left, right)) return false; } - *right = bstr_split(*right, "#", NULL); *left = bstr_strip(*left); *right = bstr_strip(*right); return right->len > 0; } HMENU load_menu(struct plugin_ctx *ctx) { + dyn_menu_init(ctx); + void *tmp = talloc_new(NULL); - char *path = mp_get_prop(tmp, "input-conf"); + char *path = mp_get_prop_string(tmp, "input-conf"); if (path == NULL || strlen(path) == 0) path = "~~/input.conf"; HMENU hmenu = CreatePopupMenu(); @@ -154,7 +326,7 @@ HMENU load_menu(struct plugin_ctx *ctx) { while (data.len) { bstr line = bstr_strip_linebreaks(bstr_getline(data, &data)); line = bstr_lstrip(line); - if (line.len == 0) continue; + if (!line.len) continue; bstr key, cmd, left, right; if (bstr_eatstart0(&line, "#")) { @@ -174,17 +346,25 @@ HMENU load_menu(struct plugin_ctx *ctx) { return hmenu; } +void update_menu(struct plugin_ctx *ctx) { + if (dyn_menus == NULL) return; + for (int i = 0; i < dyn_menus->num_entries; i++) { + struct dyn_menu *item = &dyn_menus->entries[i]; + item->update(item->talloc_ctx, item->hmenu); + } +} + void show_menu(struct plugin_ctx *ctx, POINT *pt) { RECT rc; - GetClientRect(ctx->hwnd, &rc); ScreenToClient(ctx->hwnd, pt); + if (!PtInRect(&rc, *pt)) return; - if (PtInRect(&rc, *pt)) { - ClientToScreen(ctx->hwnd, pt); - TrackPopupMenuEx(ctx->hmenu, TPM_LEFTALIGN | TPM_LEFTBUTTON, pt->x, - pt->y, ctx->hwnd, NULL); - } + update_menu(ctx); + + ClientToScreen(ctx->hwnd, pt); + TrackPopupMenuEx(ctx->hmenu, TPM_LEFTALIGN | TPM_LEFTBUTTON, pt->x, pt->y, + ctx->hwnd, NULL); } void handle_menu(struct plugin_ctx *ctx, int id) { @@ -194,6 +374,5 @@ void handle_menu(struct plugin_ctx *ctx, int id) { mii.fMask = MIIM_DATA; if (!GetMenuItemInfoW(ctx->hmenu, id, FALSE, &mii)) return; - struct item_data *data = (struct item_data *)mii.dwItemData; - if (data != NULL) mp_command_async(data->cmd); + if (mii.dwItemData) mp_command_async((const char *)mii.dwItemData); } \ No newline at end of file diff --git a/src/misc/bstr.c b/src/misc/bstr.c index 7b8b422..d825b61 100644 --- a/src/misc/bstr.c +++ b/src/misc/bstr.c @@ -115,6 +115,13 @@ struct bstr bstr_lstrip(struct bstr str) return str; } +struct bstr bstr_rstrip(struct bstr str) +{ + while (str.len && mp_isspace(str.start[str.len - 1])) + str.len--; + return str; +} + struct bstr bstr_strip(struct bstr str) { str = bstr_lstrip(str); diff --git a/src/misc/bstr.h b/src/misc/bstr.h index 9d78aad..5be35ea 100644 --- a/src/misc/bstr.h +++ b/src/misc/bstr.h @@ -70,6 +70,7 @@ int bstrcspn(struct bstr str, const char *reject); int bstr_find(struct bstr haystack, struct bstr needle); struct bstr bstr_lstrip(struct bstr str); +struct bstr bstr_rstrip(struct bstr str); struct bstr bstr_strip(struct bstr str); struct bstr bstr_split(struct bstr str, const char *sep, struct bstr *rest); bool bstr_split_tok(bstr str, const char *tok, bstr *out_left, bstr *out_right); diff --git a/src/plugin.c b/src/plugin.c index 7c34736..32b75d7 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -78,8 +78,7 @@ 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); } static MP_THREAD_VOID dispatch_thread(void *ptr) { @@ -130,13 +129,138 @@ wchar_t *mp_from_utf8(void *talloc_ctx, const char *s) { return ret; } -char *mp_get_prop(void *talloc_ctx, const char *prop) { +char *mp_get_prop_string(void *talloc_ctx, const char *prop) { char *val = mpv_get_property_string(ctx->mpv, prop); char *ret = talloc_strdup(talloc_ctx, val); if (val != NULL) mpv_free(val); return ret; } +int64_t mp_get_prop_int(const char *prop) { + int64_t ret = -1; + mpv_get_property(ctx->mpv, prop, MPV_FORMAT_INT64, &ret); + return ret; +} + +struct mp_track_list *mp_get_track_list(void *talloc_ctx, const char *type) { + mpv_node node; + + if (mpv_get_property(ctx->mpv, "track-list", MPV_FORMAT_NODE, &node) < 0) + return NULL; + if (node.format != MPV_FORMAT_NODE_ARRAY || node.u.list->num == 0) { + mpv_free_node_contents(&node); + return NULL; + } + + struct mp_track_list *list = talloc_ptrtype(talloc_ctx, list); + memset(list, 0, sizeof(*list)); + + for (int i = 0; i < node.u.list->num; i++) { + mpv_node track = node.u.list->values[i]; + + struct track_item item; + memset(&item, 0, sizeof(item)); + + for (int j = 0; j < track.u.list->num; j++) { + char *key = track.u.list->keys[j]; + mpv_node value = track.u.list->values[j]; + if (strcmp(key, "id") == 0) { + item.id = value.u.int64; + } else if (strcmp(key, "title") == 0) { + item.title = talloc_strdup(talloc_ctx, value.u.string); + } else if (strcmp(key, "lang") == 0) { + item.lang = talloc_strdup(talloc_ctx, value.u.string); + } else if (strcmp(key, "type") == 0) { + item.type = talloc_strdup(talloc_ctx, value.u.string); + } else if (strcmp(key, "selected") == 0) { + item.selected = value.u.flag; + } + } + + if (type != NULL && strcmp(item.type, type) != 0) continue; + + MP_TARRAY_APPEND(talloc_ctx, list->entries, list->num_entries, item); + } + + mpv_free_node_contents(&node); + + return list; +} + +struct mp_chapter_list *mp_get_chapter_list(void *talloc_ctx) { + mpv_node node; + + if (mpv_get_property(ctx->mpv, "chapter-list", MPV_FORMAT_NODE, &node) < 0) + return NULL; + if (node.format != MPV_FORMAT_NODE_ARRAY || node.u.list->num == 0) { + mpv_free_node_contents(&node); + return NULL; + } + + struct mp_chapter_list *list = talloc_ptrtype(talloc_ctx, list); + memset(list, 0, sizeof(*list)); + + for (int i = 0; i < node.u.list->num; i++) { + mpv_node chapter = node.u.list->values[i]; + + struct chapter_item item; + memset(&item, 0, sizeof(item)); + + for (int j = 0; j < chapter.u.list->num; j++) { + char *key = chapter.u.list->keys[j]; + mpv_node value = chapter.u.list->values[j]; + if (strcmp(key, "title") == 0) { + item.title = talloc_strdup(talloc_ctx, value.u.string); + } else if (strcmp(key, "time") == 0) { + item.time = value.u.double_; + } + } + + MP_TARRAY_APPEND(talloc_ctx, list->entries, list->num_entries, item); + } + + mpv_free_node_contents(&node); + + return list; +} +struct mp_audio_device_list *mp_get_audio_device_list(void *talloc_ctx) { + mpv_node node; + + if (mpv_get_property(ctx->mpv, "audio-device-list", MPV_FORMAT_NODE, + &node) < 0) + return NULL; + if (node.format != MPV_FORMAT_NODE_ARRAY || node.u.list->num == 0) { + mpv_free_node_contents(&node); + return NULL; + } + + struct mp_audio_device_list *list = talloc_ptrtype(talloc_ctx, list); + memset(list, 0, sizeof(*list)); + + for (int i = 0; i < node.u.list->num; i++) { + mpv_node device = node.u.list->values[i]; + + struct audio_device item; + memset(&item, 0, sizeof(item)); + + for (int j = 0; j < device.u.list->num; j++) { + char *key = device.u.list->keys[j]; + mpv_node value = device.u.list->values[j]; + if (strcmp(key, "name") == 0) { + item.name = talloc_strdup(talloc_ctx, value.u.string); + } else if (strcmp(key, "description") == 0) { + item.desc = talloc_strdup(talloc_ctx, value.u.string); + } + } + + MP_TARRAY_APPEND(talloc_ctx, list->entries, list->num_entries, item); + } + + mpv_free_node_contents(&node); + + return list; +} + char *mp_expand_path(void *talloc_ctx, char *path) { mpv_node node; const char *args[] = {"expand-path", path, NULL}; diff --git a/src/plugin.h b/src/plugin.h index ae75a10..19e2220 100644 --- a/src/plugin.h +++ b/src/plugin.h @@ -27,8 +27,45 @@ struct plugin_ctx { WNDPROC wnd_proc; // previous window procedure }; +struct track_item { + int64_t id; // the ID as it's used for -sid/--aid/--vid + char *type; // string describing the media type + char *title; // track title as it is stored in the file + char *lang; // track language as identified by the file + bool selected; // if the track is currently decoded +}; + +struct mp_track_list { + struct track_item *entries; + int num_entries; +}; + +struct chapter_item { + char *title; // chapter title as stored in the file + double time; // chapter start time in seconds as float +}; + +struct mp_chapter_list { + struct chapter_item *entries; + int num_entries; +}; + +struct audio_device { + char *name; // device name + char *desc; // device description +}; + +struct mp_audio_device_list { + struct audio_device *entries; + int num_entries; +}; + wchar_t *mp_from_utf8(void *talloc_ctx, const char *s); -char *mp_get_prop(void *talloc_ctx, const char *prop); +char *mp_get_prop_string(void *talloc_ctx, const char *prop); +int64_t mp_get_prop_int(const char *prop); +struct mp_track_list *mp_get_track_list(void *talloc_ctx, const char *type); +struct mp_chapter_list *mp_get_chapter_list(void *talloc_ctx); +struct mp_audio_device_list *mp_get_audio_device_list(void *talloc_ctx); char *mp_expand_path(void *talloc_ctx, char *path); char *mp_read_file(void *talloc_ctx, char *path); void mp_command_async(const char *args);