Proxy host context menu items for VST3 plugins

This wasn't implemented yet because no plugin tried using the interface
in this way before this, but Surge XT incorporates the host's context
menu items into their own (much more elaborate) context menu. To
accommodate this, we now copy over all of the host's prepopulated
context menu items to the Wine plugin host, and calling the targets
associated with any of those items will cause the target on the
associated context menu item on the host to be called.

This is slightly more complicated than what would otherwise be necessary
because Bitwig does not assign tags to their context menu items and
instead always uses 0.
This commit is contained in:
Robbert van der Helm
2022-01-03 17:04:00 +01:00
parent 89cd1e9ee3
commit c625deadef
16 changed files with 219 additions and 107 deletions
+11
View File
@@ -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
+2 -9
View File
@@ -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 << ": <IContextMenuTarget* #"
<< request.context_menu_id << " #" << request.target_tag
<< request.context_menu_id << ":" << request.item_id << ":"
<< request.target_tag
<< ">::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 << ": <IContextMenu* #"
<< request.context_menu_id << ">::getItemCount()";
});
}
bool Vst3Logger::log_request(bool is_host_vst,
const YaContextMenu::AddItem& request) {
return log_request_base(is_host_vst, [&](auto& message) {
-1
View File
@@ -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&);
@@ -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 <typename Bridge>
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:
+1 -1
View File
@@ -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,
@@ -21,7 +21,7 @@ Vst3ContextMenuProxy::ConstructArgs::ConstructArgs() noexcept {}
Vst3ContextMenuProxy::ConstructArgs::ConstructArgs(
Steinberg::IPtr<Steinberg::FUnknown> 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) {}
@@ -49,7 +49,7 @@ class Vst3ContextMenuProxy : public YaContextMenu {
*/
ConstructArgs(Steinberg::IPtr<FUnknown> 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
@@ -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}
@@ -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 <typename S>
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);
}
@@ -20,7 +20,24 @@ YaContextMenu::ConstructArgs::ConstructArgs() noexcept {}
YaContextMenu::ConstructArgs::ConstructArgs(
Steinberg::IPtr<Steinberg::FUnknown> object) noexcept
: supported(Steinberg::FUnknownPtr<Steinberg::Vst::IContextMenu>(object)) {}
: supported(Steinberg::FUnknownPtr<Steinberg::Vst::IContextMenu>(object)) {
Steinberg::FUnknownPtr<Steinberg::Vst::IContextMenu> 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<int32>(i), items[i],
&dummyTarget);
}
}
}
YaContextMenu::YaContextMenu(ConstructArgs&& args) noexcept
: arguments_(std::move(args)) {}
@@ -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<Steinberg::Vst::IContextMenuItem> items;
template <typename S>
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<int32>;
native_size_t owner_instance_id;
native_size_t context_menu_id;
template <typename S>
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*/,
+3 -2
View File
@@ -353,13 +353,14 @@ class Vst3PluginProxyImpl : public Vst3PluginProxy {
Steinberg::IPtr<Steinberg::Vst::IContextMenu> 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<int32, Steinberg::IPtr<YaContextMenuTarget>> targets;
std::unordered_map<int32, Steinberg::IPtr<YaContextMenuTarget>>
plugin_targets;
};
/**
+28 -13
View File
@@ -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<Steinberg::Vst::IContextMenu> 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, _] =
@@ -18,11 +18,32 @@
#include <iostream>
#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<int32>(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<int32>(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<int32>(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<int32>(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<YaContextMenuTarget::ConstructArgs>(
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<int32>(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(),
@@ -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<int32,
Steinberg::IPtr<Steinberg::Vst::IContextMenuTarget>>
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<int32, Steinberg::IPtr<YaContextMenuTarget>>
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<Steinberg::Vst::IContextMenuItem> items_;
};
+4 -1
View File
@@ -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)