Fix focus handling when reopening REAPER FX window

REAPER initializes the plugin's editor first before reparenting the
parent window to the FX window, so our `topmost_window` didn't actually
refer to the FX window.
This commit is contained in:
Robbert van der Helm
2021-07-15 14:21:50 +02:00
parent ce1cf41f45
commit f02341e77f
4 changed files with 87 additions and 22 deletions
+10 -7
View File
@@ -70,6 +70,16 @@ Versioning](https://semver.org/spec/v2.0.0.html).
plugins would at runtime change their query interface to support more VST3 plugins would at runtime change their query interface to support more VST3
interfaces, including the mandatory edit controller interface. Yabridge now interfaces, including the mandatory edit controller interface. Yabridge now
requeries the supported interfaces at a later stage to work around this. requeries the supported interfaces at a later stage to work around this.
- Fixed VST2 plugins in **Ardour** not receiving all transport information,
breaking host sync and LFOs in certain plugins. This was a regression from
yabridge 3.2.0.
- Fixed input focus handling being broken **REAPER** after reopning a closed FX
window. Now moving the mouse cursor outside of the plugin's GUI will always
release input focus, even after closing the window.
- Fixed _Insert Piz Here_'s _midiLooper_ crashing in **REAPER** when the plugin
tries to use REAPER's [host function
API](https://www.reaper.fm/sdk/vst/vst_ext.php#vst_host). This currently isn't
supported by yabridge. We now explicitly ignore these requests.
- Fixed JUCE VST3 plugins like Tokyo Dawn Records' _SlickEQ M_ causing the host - Fixed JUCE VST3 plugins like Tokyo Dawn Records' _SlickEQ M_ causing the host
to freeze when they send a parameter change from the audio thread using the to freeze when they send a parameter change from the audio thread using the
wrong VST3 API while the plugin is also trying to resize the window from the wrong VST3 API while the plugin is also trying to resize the window from the
@@ -77,13 +87,6 @@ Versioning](https://semver.org/spec/v2.0.0.html).
the Smart Ops panel after having used it once. To fix this, yabridge's the Smart Ops panel after having used it once. To fix this, yabridge's
Wine-side VST3 mutual recursion mechanism now only operates when invoked from Wine-side VST3 mutual recursion mechanism now only operates when invoked from
the GUI thread. the GUI thread.
- Fixed VST2 plugins in **Ardour** not receiving all transport information,
breaking host sync and LFOs in certain plugins. This was a regression from
yabridge 3.2.0.
- Fixed _Insert Piz Here_'s _midiLooper_ crashing in **REAPER** when the plugin
tries to use REAPER's [host function
API](https://www.reaper.fm/sdk/vst/vst_ext.php#vst_host). This currently isn't
supported by yabridge. We now explicitly ignore these requests.
- Fixed yabridge's logging seeking the STDERR stream to position 0 every time it - Fixed yabridge's logging seeking the STDERR stream to position 0 every time it
writes a log message. This would be noticeable when piping the host's STDERR writes a log message. This would be noticeable when piping the host's STDERR
stream to a file and `YABRIDGE_DEBUG_LEVEL` wasn't set. stream to a file and `YABRIDGE_DEBUG_LEVEL` wasn't set.
+1 -1
View File
@@ -1171,7 +1171,7 @@ void Vst3Bridge::run() {
void Vst3Bridge::handle_x11_events() noexcept { void Vst3Bridge::handle_x11_events() noexcept {
std::lock_guard lock(object_instances_mutex); std::lock_guard lock(object_instances_mutex);
for (const auto& [instance_id, object] : object_instances) { for (auto& [instance_id, object] : object_instances) {
if (object.editor) { if (object.editor) {
object.editor->handle_x11_events(); object.editor->handle_x11_events();
} }
+62 -12
View File
@@ -48,6 +48,21 @@ using namespace std::literals::chrono_literals;
*/ */
constexpr size_t idle_timer_id = 1337; constexpr size_t idle_timer_id = 1337;
/**
* The X11 event mask for the topmost window, which in most DAWs except for
* Ardour and REAPER will be the same as `parent_window`.
*/
constexpr uint32_t topmost_event_mask =
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_VISIBILITY_CHANGE;
/**
* The X11 event mask for the parent window. We need this structure notify here
* as well to detect reparents.
*/
constexpr uint32_t parent_event_mask =
topmost_event_mask | XCB_EVENT_MASK_FOCUS_CHANGE |
XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW;
/** /**
* The name of the X11 property on the root window used to denote the active * The name of the X11 property on the root window used to denote the active
* window in EWMH compliant window managers. * window in EWMH compliant window managers.
@@ -278,13 +293,8 @@ Editor::Editor(MainContext& main_context,
// release input focus as necessary. // release input focus as necessary.
// If we do enable XEmbed support, we'll also listen for visibility changes // If we do enable XEmbed support, we'll also listen for visibility changes
// and trigger the embedding when the window becomes visible // and trigger the embedding when the window becomes visible
const uint32_t topmost_event_mask =
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_VISIBILITY_CHANGE;
xcb_change_window_attributes(x11_connection.get(), topmost_window, xcb_change_window_attributes(x11_connection.get(), topmost_window,
XCB_CW_EVENT_MASK, &topmost_event_mask); XCB_CW_EVENT_MASK, &topmost_event_mask);
const uint32_t parent_event_mask =
XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_ENTER_WINDOW |
XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_VISIBILITY_CHANGE;
xcb_change_window_attributes(x11_connection.get(), parent_window, xcb_change_window_attributes(x11_connection.get(), parent_window,
XCB_CW_EVENT_MASK, &parent_event_mask); XCB_CW_EVENT_MASK, &parent_event_mask);
xcb_flush(x11_connection.get()); xcb_flush(x11_connection.get());
@@ -334,7 +344,7 @@ Editor::Editor(MainContext& main_context,
} }
} }
void Editor::handle_x11_events() const noexcept { void Editor::handle_x11_events() noexcept {
// NOTE: Ardour will unmap the window instead of closing the editor. When // NOTE: Ardour will unmap the window instead of closing the editor. When
// the window is unmapped `wine_window` doesn't exist and any X11 // the window is unmapped `wine_window` doesn't exist and any X11
// function calls involving it will fail. All functions called from // function calls involving it will fail. All functions called from
@@ -346,6 +356,17 @@ void Editor::handle_x11_events() const noexcept {
const uint8_t event_type = const uint8_t event_type =
generic_event->response_type & xcb_event_type_mask; generic_event->response_type & xcb_event_type_mask;
switch (event_type) { switch (event_type) {
// NOTE: When reopening a closed editor window in REAPER, REAPER
// will initialize the editor first, and only then will it
// reparent `parent_window` to a new FX window. This means
// that `topmost_window` will be the same as
// `parent_window` in REAPER if you reopen a plugin GUI,
// which breaks our input focus handling. To work around
// this, we will just check if the topmost window has
// changed whenever the parent window gets reparented.
case XCB_REPARENT_NOTIFY: {
redetect_topmost_window();
} break;
// We're listening for `ConfigureNotify` events on the topmost // We're listening for `ConfigureNotify` events on the topmost
// window before the root window, i.e. the window that's // window before the root window, i.e. the window that's
// actually going to get dragged around the by the user. In most // actually going to get dragged around the by the user. In most
@@ -354,19 +375,19 @@ void Editor::handle_x11_events() const noexcept {
// window, the local coordinates should be updated. The // window, the local coordinates should be updated. The
// additional `EnterWindow` check is sometimes necessary for // additional `EnterWindow` check is sometimes necessary for
// using multiple editor windows within a single plugin group. // using multiple editor windows within a single plugin group.
case XCB_CONFIGURE_NOTIFY: case XCB_CONFIGURE_NOTIFY: {
if (!use_xembed) { if (!use_xembed) {
fix_local_coordinates(); fix_local_coordinates();
} }
break; } break;
// Start the XEmbed procedure when the window becomes visible, // Start the XEmbed procedure when the window becomes visible,
// since most hosts will only show the window after the plugin // since most hosts will only show the window after the plugin
// has embedded itself into it. // has embedded itself into it.
case XCB_VISIBILITY_NOTIFY: case XCB_VISIBILITY_NOTIFY: {
if (use_xembed) { if (use_xembed) {
do_xembed(); do_xembed();
} }
break; } break;
// We want to grab keyboard input focus when the user hovers // We want to grab keyboard input focus when the user hovers
// over our embedded Wine window AND that window is a child of // over our embedded Wine window AND that window is a child of
// the currently active window. This ensures that the behavior // the currently active window. This ensures that the behavior
@@ -376,7 +397,7 @@ void Editor::handle_x11_events() const noexcept {
// for when returning to the main plugin window after closing a // for when returning to the main plugin window after closing a
// dialog, since that often won't trigger an `EnterNotify'. // dialog, since that often won't trigger an `EnterNotify'.
case XCB_ENTER_NOTIFY: case XCB_ENTER_NOTIFY:
case XCB_FOCUS_IN: case XCB_FOCUS_IN: {
if (!use_xembed) { if (!use_xembed) {
fix_local_coordinates(); fix_local_coordinates();
} }
@@ -389,7 +410,7 @@ void Editor::handle_x11_events() const noexcept {
is_wine_window_active()) { is_wine_window_active()) {
set_input_focus(true); set_input_focus(true);
} }
break; } break;
// When the user moves their mouse away from the Wine window // When the user moves their mouse away from the Wine window
// _while the window provided by the host it is contained in is // _while the window provided by the host it is contained in is
// still active_, we will give back keyboard focus to that // still active_, we will give back keyboard focus to that
@@ -566,6 +587,35 @@ bool Editor::is_wine_window_active() const {
return is_child_window_or_same(*x11_connection, wine_window, active_window); return is_child_window_or_same(*x11_connection, wine_window, active_window);
} }
void Editor::redetect_topmost_window() noexcept {
const xcb_window_t new_topmost_window =
find_ancestor_windows(*x11_connection, parent_window).back();
if (new_topmost_window == topmost_window) {
return;
}
// We need to readjust the event masks for the new topmost window, keeping
// the (very probable) possibility in mind that the old topmost window is
// the same as the parent window or that the parent window now is the
// topmost window.
if (topmost_window != parent_window) {
constexpr uint32_t no_event_mask = XCB_EVENT_MASK_NO_EVENT;
xcb_change_window_attributes(x11_connection.get(), topmost_window,
XCB_CW_EVENT_MASK, &no_event_mask);
}
if (new_topmost_window == parent_window) {
xcb_change_window_attributes(x11_connection.get(), new_topmost_window,
XCB_CW_EVENT_MASK, &parent_event_mask);
} else {
xcb_change_window_attributes(x11_connection.get(), new_topmost_window,
XCB_CW_EVENT_MASK, &topmost_event_mask);
}
topmost_window = new_topmost_window;
xcb_flush(x11_connection.get());
}
bool Editor::supports_ewmh_active_window() const { bool Editor::supports_ewmh_active_window() const {
if (supports_ewmh_active_window_cache) { if (supports_ewmh_active_window_cache) {
return *supports_ewmh_active_window_cache; return *supports_ewmh_active_window_cache;
+14 -2
View File
@@ -163,7 +163,7 @@ class Editor {
/** /**
* Handle X11 events sent to the window our editor is embedded in. * Handle X11 events sent to the window our editor is embedded in.
*/ */
void handle_x11_events() const noexcept; void handle_x11_events() noexcept;
/** /**
* Get the Win32 window handle so it can be passed to an `effEditOpen()` * Get the Win32 window handle so it can be passed to an `effEditOpen()`
@@ -225,6 +225,12 @@ class Editor {
*/ */
bool is_wine_window_active() const; bool is_wine_window_active() const;
/**
* After `parent_window` gets reparented, we may need to redetect the
* topmost window and adjust the events we're subscribed to accordingly.
*/
void redetect_topmost_window() noexcept;
/** /**
* Send an XEmbed message to a window. This does not include a flush. See * Send an XEmbed message to a window. This does not include a flush. See
* the spec for more information: * the spec for more information:
@@ -313,8 +319,14 @@ class Editor {
* REAPER) embed `parent_window` into another window. We have to listen for * REAPER) embed `parent_window` into another window. We have to listen for
* configuration changes on this topmost window to know when the window is * configuration changes on this topmost window to know when the window is
* being dragged around. * being dragged around.
*
* NOTE: When reopening a REAPER FX window that has previously been closed,
* REAPER will initialize the first plugin's editor first before
* opening the window. This means that the topmost FX window doesn't
* actually exist yet at that point, so we need to redetect this
* later.
*/ */
const xcb_window_t topmost_window; xcb_window_t topmost_window;
/** /**
* The atom corresponding to `_NET_ACTIVE_WINDOW`. * The atom corresponding to `_NET_ACTIVE_WINDOW`.