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
@@ -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)