diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a287f82..6b0bf5d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- Added support for VST3 plugins interacting directly with the host's context + menu items. Most plugins that use VST3's context menu support let the host + handle drawing the actual menu, but it's also possible for plugins to + incorporate the host's menu items into their own custom context menu. So far + this feature has only been tested with [Surge + XT](https://github.com/surge-synthesizer/surge)'s Windows VST3 version since + very few if any other plugins do this right now, but other plugins may start + doing this too in the future. + ### Changed - Added support for Wine 6.23's new fixed winedbg command line argument diff --git a/src/common/logging/vst3.cpp b/src/common/logging/vst3.cpp index b098ff43..f0ddfe2f 100644 --- a/src/common/logging/vst3.cpp +++ b/src/common/logging/vst3.cpp @@ -216,7 +216,8 @@ bool Vst3Logger::log_request( const YaContextMenuTarget::ExecuteMenuItem& request) { return log_request_base(is_host_vst, [&](auto& message) { message << request.owner_instance_id << ": ::executeMenuItem(tag = " << request.tag << ")"; }); } @@ -1272,14 +1273,6 @@ bool Vst3Logger::log_request( }); } -bool Vst3Logger::log_request(bool is_host_vst, - const YaContextMenu::GetItemCount& request) { - return log_request_base(is_host_vst, [&](auto& message) { - message << request.owner_instance_id << ": ::getItemCount()"; - }); -} - bool Vst3Logger::log_request(bool is_host_vst, const YaContextMenu::AddItem& request) { return log_request_base(is_host_vst, [&](auto& message) { diff --git a/src/common/logging/vst3.h b/src/common/logging/vst3.h index 2df3095e..b4af31b6 100644 --- a/src/common/logging/vst3.h +++ b/src/common/logging/vst3.h @@ -225,7 +225,6 @@ class Vst3Logger { bool log_request( bool is_host_vst, const YaComponentHandlerBusActivation::RequestBusActivation&); - bool log_request(bool is_host_vst, const YaContextMenu::GetItemCount&); bool log_request(bool is_host_vst, const YaContextMenu::AddItem&); bool log_request(bool is_host_vst, const YaContextMenu::RemoveItem&); bool log_request(bool is_host_vst, const YaContextMenu::Popup&); diff --git a/src/common/serialization/vst3-impls/context-menu-target.h b/src/common/serialization/vst3-impls/context-menu-target.h index 32728c11..9434b66d 100644 --- a/src/common/serialization/vst3-impls/context-menu-target.h +++ b/src/common/serialization/vst3-impls/context-menu-target.h @@ -22,6 +22,9 @@ * This implementation used to live in `src/plugin/bridges/vst3-impls`, but * since plugins can also call context menu items added by the host this is * needed on both sides. + * + * NOTE: Bitwig does not actually set the tags here, so host menu items need to + * be identified through their item ID, not through the tag. */ template class YaContextMenuTargetImpl : public YaContextMenuTarget { @@ -45,11 +48,16 @@ class YaContextMenuTargetImpl : public YaContextMenuTarget { // From `IContextMenuTarget` tresult PLUGIN_API executeMenuItem(int32 tag) override { - return bridge_.send_message(YaContextMenuTarget::ExecuteMenuItem{ - .owner_instance_id = owner_instance_id(), - .context_menu_id = context_menu_id(), - .target_tag = target_tag(), - .tag = tag}); + // NOTE: This requires mutual recursion, because REAPER will call + // `getState()` whle the context menu is open, and `getState()` + // also has to be handled from the GUI thread + return bridge_.send_mutually_recursive_message( + YaContextMenuTarget::ExecuteMenuItem{ + .owner_instance_id = owner_instance_id(), + .context_menu_id = context_menu_id(), + .item_id = item_id(), + .target_tag = target_tag(), + .tag = tag}); } private: diff --git a/src/common/serialization/vst3.h b/src/common/serialization/vst3.h index f7d45c95..d4901000 100644 --- a/src/common/serialization/vst3.h +++ b/src/common/serialization/vst3.h @@ -260,10 +260,10 @@ using CallbackRequest = // `IConnectionPoint::notify` calls through // there YaConnectionPoint::Notify, - YaContextMenu::GetItemCount, YaContextMenu::AddItem, YaContextMenu::RemoveItem, YaContextMenu::Popup, + YaContextMenuTarget::ExecuteMenuItem, YaHostApplication::GetName, YaPlugFrame::ResizeView, YaPlugInterfaceSupport::IsPlugInterfaceSupported, diff --git a/src/common/serialization/vst3/context-menu-proxy.cpp b/src/common/serialization/vst3/context-menu-proxy.cpp index 74cd5b48..59ec7069 100644 --- a/src/common/serialization/vst3/context-menu-proxy.cpp +++ b/src/common/serialization/vst3/context-menu-proxy.cpp @@ -21,7 +21,7 @@ Vst3ContextMenuProxy::ConstructArgs::ConstructArgs() noexcept {} Vst3ContextMenuProxy::ConstructArgs::ConstructArgs( Steinberg::IPtr object, size_t owner_instance_id, - size_t context_menu_id) noexcept + size_t context_menu_id) : owner_instance_id(owner_instance_id), context_menu_id(context_menu_id), context_menu_args(object) {} diff --git a/src/common/serialization/vst3/context-menu-proxy.h b/src/common/serialization/vst3/context-menu-proxy.h index 4b35435b..65fc3dde 100644 --- a/src/common/serialization/vst3/context-menu-proxy.h +++ b/src/common/serialization/vst3/context-menu-proxy.h @@ -49,7 +49,7 @@ class Vst3ContextMenuProxy : public YaContextMenu { */ ConstructArgs(Steinberg::IPtr object, size_t owner_instance_id, - size_t context_menu_id) noexcept; + size_t context_menu_id); /** * The unique instance identifier of the proxy object instance this diff --git a/src/common/serialization/vst3/context-menu-target.cpp b/src/common/serialization/vst3/context-menu-target.cpp index 527e7e92..56b7ba6a 100644 --- a/src/common/serialization/vst3/context-menu-target.cpp +++ b/src/common/serialization/vst3/context-menu-target.cpp @@ -16,16 +16,6 @@ #include "context-menu-target.h" -YaContextMenuTarget::ConstructArgs::ConstructArgs() noexcept {} - -YaContextMenuTarget::ConstructArgs::ConstructArgs( - native_size_t owner_instance_id, - native_size_t context_menu_id, - int32 tag) noexcept - : owner_instance_id(owner_instance_id), - context_menu_id(context_menu_id), - tag(tag) {} - YaContextMenuTarget::YaContextMenuTarget(ConstructArgs&& args) noexcept : arguments_(std::move(args)){FUNKNOWN_CTOR} diff --git a/src/common/serialization/vst3/context-menu-target.h b/src/common/serialization/vst3/context-menu-target.h index 057d699a..38c5eceb 100644 --- a/src/common/serialization/vst3/context-menu-target.h +++ b/src/common/serialization/vst3/context-menu-target.h @@ -37,30 +37,26 @@ class YaContextMenuTarget : public Steinberg::Vst::IContextMenuTarget { * `YaContextMenuTargetImpl`. */ struct ConstructArgs { - ConstructArgs() noexcept; - - /** - * Read from an existing object. We will try to mimic this object, so - * we'll support any interfaces this object also supports. - * - * @param owner_instance_id The object instance that this target's - * context menu belongs to. - * @param context_menu_id The unique ID of the context menu requested by - * `owwner_instance_id`. - * @param tag The tag of the menu item this target belongs to. - */ - ConstructArgs(native_size_t owner_instance_id, - native_size_t context_menu_id, - int32 tag) noexcept; - native_size_t owner_instance_id; native_size_t context_menu_id; + /** + * The ID of the menu item this target belongs to, only used when + * calling host targets from the plugin. + * + * NOTE: Needed to work around a Bitwig bug, see the comment in + * `ExecuteMenuItem` + */ + int32 item_id; + /** + * The tag of the menu item this target belongs to. + */ int32 tag; template void serialize(S& s) { s.value8b(owner_instance_id); s.value8b(context_menu_id); + s.value4b(item_id); s.value4b(tag); } }; @@ -87,6 +83,12 @@ class YaContextMenuTarget : public Steinberg::Vst::IContextMenuTarget { */ inline size_t context_menu_id() const { return arguments_.context_menu_id; } + /** + * Get the ID of the menu item this target was obtained from. This value is + * only actually used when calling host context menu items from a plugin. + */ + inline int32 item_id() const { return arguments_.item_id; } + /** * Get the tag of the menu item this target was passed to. */ @@ -103,8 +105,22 @@ class YaContextMenuTarget : public Steinberg::Vst::IContextMenuTarget { native_size_t owner_instance_id; native_size_t context_menu_id; /** - * The tag this target was passed for. This should be the same as `tag`, - * but it doesn't have to be. + * The menu item ID this target belongs to. + * + * This is used when calling host context menu items from the plugin's + * side. + * + * NOTE: This is needed because Bitwig identifies its own menu items by + * opaque ID, and not through the tag. They use 0 for all tags. + */ + int32 item_id; + /** + * The tag of the target this method was called on. Presumably this + * would always be the same as the `tag` argument passed to this + * function, but it doesn't have to be. + * + * This is used when calling plugin context menu items from the host's + * side. */ int32 target_tag; @@ -114,6 +130,7 @@ class YaContextMenuTarget : public Steinberg::Vst::IContextMenuTarget { void serialize(S& s) { s.value8b(owner_instance_id); s.value8b(context_menu_id); + s.value4b(item_id); s.value4b(target_tag); s.value4b(tag); } diff --git a/src/common/serialization/vst3/context-menu/context-menu.cpp b/src/common/serialization/vst3/context-menu/context-menu.cpp index f016172e..67def627 100644 --- a/src/common/serialization/vst3/context-menu/context-menu.cpp +++ b/src/common/serialization/vst3/context-menu/context-menu.cpp @@ -20,7 +20,24 @@ YaContextMenu::ConstructArgs::ConstructArgs() noexcept {} YaContextMenu::ConstructArgs::ConstructArgs( Steinberg::IPtr object) noexcept - : supported(Steinberg::FUnknownPtr(object)) {} + : supported(Steinberg::FUnknownPtr(object)) { + Steinberg::FUnknownPtr context_menu(object); + if (context_menu) { + // Can't trust plugins to check for null pointers, so we'll just always + // pass something + Steinberg::Vst::IContextMenuTarget* dummyTarget = nullptr; + + // Prepopulate the context menu with these targets + // NOTE: Bitwig does not actually set the tags here, so host menu items + // need to be identified through their item ID, not through the + // tag + items.resize(context_menu->getItemCount()); + for (size_t i = 0; i < items.size(); i++) { + context_menu->getItem(static_cast(i), items[i], + &dummyTarget); + } + } +} YaContextMenu::YaContextMenu(ConstructArgs&& args) noexcept : arguments_(std::move(args)) {} diff --git a/src/common/serialization/vst3/context-menu/context-menu.h b/src/common/serialization/vst3/context-menu/context-menu.h index b40e37d0..6e98f930 100644 --- a/src/common/serialization/vst3/context-menu/context-menu.h +++ b/src/common/serialization/vst3/context-menu/context-menu.h @@ -29,6 +29,9 @@ /** * Wraps around `IContextMenu` for serialization purposes. This is instantiated * as part of `Vst3ContextMenuProxy`. + * + * Plugins can also call context menu items created by the host, in which case + * we'll proxy that call through to the host. */ class YaContextMenu : public Steinberg::Vst::IContextMenu { public: @@ -49,9 +52,18 @@ class YaContextMenu : public Steinberg::Vst::IContextMenu { */ bool supported; + /** + * The context menu items prepopulated by the host so the plugin can + * call them. These items will receive `YaContextMenuTarget` proxy + * targets in `Vst3ContextMenuProxyImpl`, so when the plugin calls them + * it will dispatch a call to the host instead. + */ + std::vector items; + template void serialize(S& s) { s.value1b(supported); + s.container(items, 1 << 16); } }; @@ -63,28 +75,10 @@ class YaContextMenu : public Steinberg::Vst::IContextMenu { inline bool supported() const noexcept { return arguments_.supported; } - /** - * Message to pass through a call to `IContextMenu::getItemCount()` to the - * corresponding context menu instance returned by the host. - */ - struct GetItemCount { - using Response = PrimitiveWrapper; - - native_size_t owner_instance_id; - native_size_t context_menu_id; - - template - void serialize(S& s) { - s.value8b(owner_instance_id); - s.value8b(context_menu_id); - } - }; - + // Since we pass along a list of initial items, we don't need to proxy this + // unless the host somehow adds more items after the plugin adds an item virtual int32 PLUGIN_API getItemCount() override = 0; - - // XXX: Can a plugin call this to get items created by the host? Why would - // they do that? We should find a host/plugin combination that supports - // `IComponentHandler3` first. + // Plugins can also call context menu items created by the host virtual tresult PLUGIN_API getItem(int32 index, Steinberg::Vst::IContextMenuItem& item /*out*/, diff --git a/src/plugin/bridges/vst3-impls/plugin-proxy.h b/src/plugin/bridges/vst3-impls/plugin-proxy.h index 268cd790..cf07d5e8 100644 --- a/src/plugin/bridges/vst3-impls/plugin-proxy.h +++ b/src/plugin/bridges/vst3-impls/plugin-proxy.h @@ -353,13 +353,14 @@ class Vst3PluginProxyImpl : public Vst3PluginProxy { Steinberg::IPtr menu; /** - * All targets we pass to `IContextMenu::addItem`. We'll store them per + * All targets passed to `IContextMenu::addItem`. We'll store them per * item tag, so we can drop them together with the menu. We probably * don't have to use smart pointers for this, but the docs are missing a * lot of details o how this should be implemented and there's no * example implementation around. */ - std::unordered_map> targets; + std::unordered_map> + plugin_targets; }; /** diff --git a/src/plugin/bridges/vst3.cpp b/src/plugin/bridges/vst3.cpp index 1b3ee249..56793224 100644 --- a/src/plugin/bridges/vst3.cpp +++ b/src/plugin/bridges/vst3.cpp @@ -183,15 +183,6 @@ Vst3PluginBridge::Vst3PluginBridge() request.type, request.dir, request.index, request.state); }, - [&](const YaContextMenu::GetItemCount& request) - -> YaContextMenu::GetItemCount::Response { - const auto& [proxy_object, _] = - get_proxy(request.owner_instance_id); - - return proxy_object.context_menus_ - .at(request.context_menu_id) - .menu->getItemCount(); - }, [&](YaContextMenu::AddItem& request) -> YaContextMenu::AddItem::Response { const auto& [proxy_object, _] = @@ -201,13 +192,13 @@ Vst3PluginBridge::Vst3PluginBridge() proxy_object.context_menus_.at(request.context_menu_id); if (request.target) { - context_menu.targets[request.item.tag] = + context_menu.plugin_targets[request.item.tag] = Steinberg::owned(new YaContextMenuTargetImpl( *this, std::move(*request.target))); return context_menu.menu->addItem( request.item, - context_menu.targets[request.item.tag]); + context_menu.plugin_targets[request.item.tag]); } else { return context_menu.menu->addItem(request.item, nullptr); @@ -222,8 +213,8 @@ Vst3PluginBridge::Vst3PluginBridge() proxy_object.context_menus_.at(request.context_menu_id); if (const auto it = - context_menu.targets.find(request.item.tag); - it != context_menu.targets.end()) { + context_menu.plugin_targets.find(request.item.tag); + it != context_menu.plugin_targets.end()) { return context_menu.menu->removeItem(request.item, it->second); } else { @@ -245,6 +236,30 @@ Vst3PluginBridge::Vst3PluginBridge() .menu->popup(request.x, request.y); }); }, + [&](YaContextMenuTarget::ExecuteMenuItem& request) + -> YaContextMenuTarget::ExecuteMenuItem::Response { + const auto& [proxy, _] = + get_proxy(request.owner_instance_id); + + // This is of course only used for calling host defined + // targets from the plugin, this will never be called when + // the plugin calls their own targets for whatever reason + Steinberg::Vst::IContextMenuItem item; + Steinberg::Vst::IContextMenuTarget* target = nullptr; + Steinberg::IPtr menu = + proxy.context_menus_.at(request.context_menu_id).menu; + if (menu->getItem(request.item_id, item, &target) == + Steinberg::kResultOk && + target) { + return target->executeMenuItem(request.tag); + } else { + logger_.log( + "WARNING: A IContextMenuTarget::ExecuteMenuItem " + "from the plugin could not be handled"); + + return Steinberg::kInvalidArgument; + } + }, [&](YaConnectionPoint::Notify& request) -> YaConnectionPoint::Notify::Response { const auto& [proxy_object, _] = diff --git a/src/wine-host/bridges/vst3-impls/context-menu-proxy.cpp b/src/wine-host/bridges/vst3-impls/context-menu-proxy.cpp index 4b02293c..61e6a548 100644 --- a/src/wine-host/bridges/vst3-impls/context-menu-proxy.cpp +++ b/src/wine-host/bridges/vst3-impls/context-menu-proxy.cpp @@ -18,11 +18,32 @@ #include +#include "../../common/serialization/vst3-impls/context-menu-target.h" + Vst3ContextMenuProxyImpl::Vst3ContextMenuProxyImpl( Vst3Bridge& bridge, - Vst3ContextMenuProxy::ConstructArgs&& args) noexcept - : Vst3ContextMenuProxy(std::move(args)), bridge_(bridge) { + Vst3ContextMenuProxy::ConstructArgs&& args) + : Vst3ContextMenuProxy(std::move(args)), + bridge_(bridge), + items_(std::move(YaContextMenu::arguments_.items)) { bridge.register_context_menu(*this); + + // The host has likely prepopulated the context menu with its own items. In + // that case we should create proxy targets for those so the plugin can call + // those menu items. + const int32 num_items = static_cast(items_.size()); + for (int32 item_idx = 0; item_idx < num_items; item_idx++) { + auto& item = items_[item_idx]; + + // NOTE: These host targets are indexed by the item's index because + // Bitwig doesn't assign tags to their own menu items + host_targets_[item_idx] = Steinberg::owned(new YaContextMenuTargetImpl( + bridge, YaContextMenuTarget::ConstructArgs{ + .owner_instance_id = owner_instance_id(), + .context_menu_id = context_menu_id(), + .item_id = static_cast(item_idx), + .tag = item.tag})); + } } Vst3ContextMenuProxyImpl::~Vst3ContextMenuProxyImpl() noexcept { @@ -49,9 +70,7 @@ Vst3ContextMenuProxyImpl::queryInterface(const Steinberg::TUID _iid, } int32 PLUGIN_API Vst3ContextMenuProxyImpl::getItemCount() { - return bridge_.send_message( - YaContextMenu::GetItemCount{.owner_instance_id = owner_instance_id(), - .context_menu_id = context_menu_id()}); + return static_cast(items_.size()); } tresult PLUGIN_API Vst3ContextMenuProxyImpl::getItem( @@ -63,31 +82,56 @@ tresult PLUGIN_API Vst3ContextMenuProxyImpl::getItem( // the plugin (but we'll implement a basic version anyways). if (index < 0 || index >= static_cast(items_.size())) { return Steinberg::kInvalidArgument; - } else { - item = items_[index]; - *target = context_menu_targets_[item.tag]; + } - return Steinberg::kResultOk; + item = items_[index]; + if (target) { + // The item is either a context menu item prepopulated by the host or an + // item created by the plugin itself + if (auto plugin_target = plugin_targets_.find(item.tag); + plugin_target != plugin_targets_.end()) { + *target = plugin_target->second; + return Steinberg::kResultOk; + } else if (auto proxy_target = host_targets_.find(index); + // NOTE: These proxy targets are indexed by the item's index + // because Bitwig doesn't assign tags to their context + // menu items + proxy_target != host_targets_.end()) { + *target = proxy_target->second; + return Steinberg::kResultOk; + } else { + *target = nullptr; + return Steinberg::kResultFalse; + } + } else { + std::cerr << "WARNING: Null pointer passed to 'IContextMenu::getItem()'" + << std::endl; + return Steinberg::kInvalidArgument; } } tresult PLUGIN_API Vst3ContextMenuProxyImpl::addItem(const Steinberg::Vst::IContextMenuItem& item, Steinberg::Vst::IContextMenuTarget* target) { - // TODO: I haven't come across a plugin that adds its own items, so this - // hasn't been tested yet + // TODO: I haven't come across a plugin that adds its own items to the + // host's context menu, so this hasn't been tested yet const tresult result = bridge_.send_message(YaContextMenu::AddItem{ .owner_instance_id = owner_instance_id(), .context_menu_id = context_menu_id(), .item = item, - .target = - (target ? std::make_optional( - owner_instance_id(), context_menu_id(), item.tag) - : std::nullopt)}); + .target = (target ? std::optional(YaContextMenuTarget::ConstructArgs{ + .owner_instance_id = owner_instance_id(), + .context_menu_id = context_menu_id(), + // This item ID isn't actually used here because + // it's only needed to work around a Bitwig bug + // when calling host menu items from a plugin + .item_id = static_cast(items_.size()), + .tag = item.tag}) + : std::nullopt)}); if (result == Steinberg::kResultOk) { items_.push_back(item); - context_menu_targets_[item.tag] = target; + plugin_targets_[item.tag] = target; } return result; @@ -110,7 +154,12 @@ tresult PLUGIN_API Vst3ContextMenuProxyImpl::removeItem( return candidate_item.tag == item.tag; }), items_.end()); - context_menu_targets_.erase(item.tag); + + // The target can be either a proxy target or a target added by the + // plugin + if (plugin_targets_.erase(item.tag) == 0) { + host_targets_.erase(item.tag); + } } return result; @@ -120,7 +169,7 @@ tresult PLUGIN_API Vst3ContextMenuProxyImpl::popup(Steinberg::UCoord x, Steinberg::UCoord y) { // NOTE: This requires mutual recursion, because REAPER will call // `getState()` whle the context menu is open, and `getState()` also - // has to be handled from the GUi thread + // has to be handled from the GUI thread return bridge_.send_mutually_recursive_message( YaContextMenu::Popup{.owner_instance_id = owner_instance_id(), .context_menu_id = context_menu_id(), diff --git a/src/wine-host/bridges/vst3-impls/context-menu-proxy.h b/src/wine-host/bridges/vst3-impls/context-menu-proxy.h index 2d2741a9..2782b2fd 100644 --- a/src/wine-host/bridges/vst3-impls/context-menu-proxy.h +++ b/src/wine-host/bridges/vst3-impls/context-menu-proxy.h @@ -20,9 +20,8 @@ class Vst3ContextMenuProxyImpl : public Vst3ContextMenuProxy { public: - Vst3ContextMenuProxyImpl( - Vst3Bridge& bridge, - Vst3ContextMenuProxy::ConstructArgs&& args) noexcept; + Vst3ContextMenuProxyImpl(Vst3Bridge& bridge, + Vst3ContextMenuProxy::ConstructArgs&& args); /** * When the reference count reaches zero and this destructor is called, @@ -52,19 +51,35 @@ class Vst3ContextMenuProxyImpl : public Vst3ContextMenuProxy { tresult PLUGIN_API popup(Steinberg::UCoord x, Steinberg::UCoord y) override; /** - * The targets passed when to `addItem` calls made by the plugin. This way + * The targets passed when to `addItem()` calls made by the plugin. This way * we can call these same targets later. The key here is the item's tag. + * + * If `getItem()` returns a context menu item with a tag that is not in this + * map then it's from an item belonging to the host, and we'll return a + * proxy target that would call the host's target instead. */ std::unordered_map> - context_menu_targets_; + plugin_targets_; private: Vst3Bridge& bridge_; + /** + * As mentioned above, these are the targets belonging to context items + * prepopulated by the host. Because Bitwig doesn't assign a tag to its own + * context menu items all of these this map is indexed by the **item id**. + * Calling one of these sends a message to the host to call the + * corresponding menu item. + */ + std::unordered_map> + host_targets_; + /** * The items passed when to `addItem` calls made by the plugin. This way we * can call these same targets later. + * + * This will be initialized with targets created by the host. */ std::vector items_; }; diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index 9a514466..2c3b21d8 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -387,10 +387,13 @@ void Vst3Bridge::run() { const auto& [instance, _] = get_instance(request.owner_instance_id); + // This is of course only used for calling plugin defined + // targets from the host, this will never be called when the + // host calls their own targets for whatever reason return instance.registered_context_menus .at(request.context_menu_id) .get() - .context_menu_targets_[request.target_tag] + .plugin_targets_[request.target_tag] ->executeMenuItem(request.tag); }, [&](YaEditController::SetComponentState& request)