Skip to content

Commit

Permalink
Add a fallback for non-EWMH compliant WMs
Browse files Browse the repository at this point in the history
I'm not sure how many people use WMs that don't implement
`_NET_ACTIVE_WINDOW`, but at least this will give those people some way
to grab input focus, even if it's not as good as the new method.
  • Loading branch information
robbert-vdh committed Oct 9, 2020
1 parent 5a64769 commit 225d6a0
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 27 deletions.
126 changes: 100 additions & 26 deletions src/wine-host/editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

#include "editor.h"

#include <iostream>

// The Win32 API requires you to hardcode identifiers for tiemrs
constexpr size_t idle_timer_id = 1337;

Expand Down Expand Up @@ -61,6 +63,12 @@ bool is_child_window_or_same(xcb_connection_t& x11_connection,
* any of the connected screens.
*/
Size get_maximum_screen_dimensions(xcb_connection_t& x11_connection);
/**
* Get the root window for the specified window. The returned root window will
* depend on the screen the window is on.
*/
xcb_window_t get_root_window(xcb_connection_t& x11_connection,
xcb_window_t window);
/**
* Return the X11 window handle for the window if it's currently open.
*/
Expand Down Expand Up @@ -133,11 +141,20 @@ Editor::Editor(const Configuration& config,
xcb_intern_atom_reply_t* atom_reply =
xcb_intern_atom_reply(x11_connection.get(), atom_cookie, &error);
assert(!error);
// TODO: Check for XCB_ATOM_NONE. If the user is using some obscure WM that
// does not support _NET_WM_NAME, then we can print a warning message
// and fall back to grabbing focus on `WM_PARENTNOTIFY`.

// In case the atom does not exist or the WM does not support this hint,
// we'll print a warning and fall back to grabbing focus when the user
// clicks on the window (which should trigger a `WM_PARENTNOTIFY`).
active_window_property = atom_reply->atom;
free(atom_reply);
if (!supports_ewmh_active_window()) {
std::cout << "WARNING: The current window manager does not support the"
<< std::endl;
std::cout << " '" << active_window_property_name
<< "' property. Falling back to a" << std::endl;
std::cout << " less reliable keyboard input grabbing method."
<< std::endl;
}

// Because we're not using XEmbed Wine will interpret any local coordinates
// as global coordinates. To work around this we'll tell the Wine window
Expand Down Expand Up @@ -263,7 +280,10 @@ void Editor::handle_x11_events() const {
case XCB_FOCUS_IN:
fix_local_coordinates();

if (is_wine_window_active()) {
// In case the WM somehow does not support `_NET_ACTIVE_WINDOW`,
// a more naive focus grabbing method implemented in the
// `WM_PARENTNOTIFY` handler will be used.
if (supports_ewmh_active_window() && is_wine_window_active()) {
set_input_focus(true);
}
break;
Expand All @@ -275,7 +295,7 @@ void Editor::handle_x11_events() const {
// to mess with keyboard focus when hovering over the window while
// for instance a dialog is open.
case XCB_LEAVE_NOTIFY:
if (is_wine_window_active()) {
if (supports_ewmh_active_window() && is_wine_window_active()) {
set_input_focus(false);
}
break;
Expand All @@ -286,8 +306,6 @@ void Editor::handle_x11_events() const {
}

void Editor::fix_local_coordinates() const {
xcb_generic_error_t* error;

// We're purposely not using XEmbed. This has the consequence that wine
// still thinks that any X and Y coordinates are relative to the x11 window
// root instead of the parent window provided by the DAW, causing all sorts
Expand All @@ -299,16 +317,11 @@ void Editor::fix_local_coordinates() const {
// window created by the plugin itself. In this case it doesn't matter that
// the Win32 window is larger than the part of the client area the plugin
// draws to since any excess will be clipped off by the parent window.
const xcb_query_tree_cookie_t query_cookie =
xcb_query_tree(x11_connection.get(), parent_window);
xcb_query_tree_reply_t* query_reply =
xcb_query_tree_reply(x11_connection.get(), query_cookie, &error);
assert(!error);
xcb_window_t root = query_reply->root;
free(query_reply);
const xcb_window_t root = get_root_window(*x11_connection, parent_window);

// We can't directly use the `event.x` and `event.y` coordinates because the
// parent window may also be embedded inside another window.
xcb_generic_error_t* error;
const xcb_translate_coordinates_cookie_t translate_cookie =
xcb_translate_coordinates(x11_connection.get(), parent_window, root, 0,
0);
Expand Down Expand Up @@ -356,19 +369,18 @@ void Editor::set_input_focus(bool grab) const {
}

bool Editor::is_wine_window_active() const {
if (!supports_ewmh_active_window()) {
return false;
}

// We will only grab focus when the Wine window is active. To do this we'll
// read the `_NET_ACTIVE_WINDOW` property from the root window (which can
// change when the window gets moved to another screen, so we won't cache
// this).
xcb_generic_error_t* error;
const xcb_query_tree_cookie_t root_query_cookie =
xcb_query_tree(x11_connection.get(), wine_window);
xcb_query_tree_reply_t* root_query_reply =
xcb_query_tree_reply(x11_connection.get(), root_query_cookie, &error);
assert(!error);
const xcb_window_t root_window = root_query_reply->root;
free(root_query_reply);
const xcb_window_t root_window =
get_root_window(*x11_connection, wine_window);

xcb_generic_error_t* error;
const xcb_get_property_cookie_t property_cookie =
xcb_get_property(x11_connection.get(), false, root_window,
active_window_property, XCB_ATOM_WINDOW, 0, 1);
Expand All @@ -382,6 +394,40 @@ bool Editor::is_wine_window_active() const {
return is_child_window_or_same(*x11_connection, wine_window, active_window);
}

bool Editor::supports_ewmh_active_window() const {
if (supports_ewmh_active_window_cache) {
return *supports_ewmh_active_window_cache;
}

// It could be that the `_NET_ACTIVE_WINDOW` atom exists (because it was
// created by another application) but that the root window does not have
// the property
if (active_window_property == XCB_ATOM_NONE) {
supports_ewmh_active_window_cache = false;
return false;
}

const xcb_window_t root_window =
get_root_window(*x11_connection, wine_window);

// If the `_NET_ACTIVE_WINDOW` property does not exist on the root window,
// the returned property type will be `XCB_ATOM_NONE` as specified in the
// X11 manual
xcb_generic_error_t* error;
const xcb_get_property_cookie_t property_cookie =
xcb_get_property(x11_connection.get(), false, root_window,
active_window_property, XCB_ATOM_WINDOW, 0, 1);
xcb_get_property_reply_t* property_reply =
xcb_get_property_reply(x11_connection.get(), property_cookie, &error);
assert(!error);
bool active_window_property_exists =
property_reply->format != XCB_ATOM_NONE;
free(property_reply);

supports_ewmh_active_window_cache = active_window_property_exists;
return active_window_property_exists;
}

LRESULT CALLBACK window_proc(HWND handle,
UINT message,
WPARAM wParam,
Expand Down Expand Up @@ -417,17 +463,31 @@ LRESULT CALLBACK window_proc(HWND handle,
editor->send_idle_event();
return 0;
} break;
// In case the WM does not support the EWMH active window property,
// we'll fall back to grabbing focus when the user clicks on the window
// by listening to the generated `WM_PARENTNOTIFY` messages. Otherwise
// we have some more sophisticated behaviour using `EnterNotify` and
// `LeaveNotify` X11 events. This will only be necessary for very
// barebones window managers.
case WM_PARENTNOTIFY: {
auto editor = reinterpret_cast<Editor*>(
GetWindowLongPtr(handle, GWLP_USERDATA));
if (!editor || editor->supports_ewmh_active_window()) {
break;
}

editor->set_input_focus(true);
} break;
}

return DefWindowProc(handle, message, wParam, lParam);
}

xcb_window_t find_topmost_window(xcb_connection_t& x11_connection,
xcb_window_t starting_at) {
xcb_generic_error_t* error;

xcb_window_t current_window = starting_at;

xcb_generic_error_t* error;
xcb_query_tree_cookie_t query_cookie =
xcb_query_tree(&x11_connection, starting_at);
xcb_query_tree_reply_t* query_reply =
Expand All @@ -452,10 +512,9 @@ xcb_window_t find_topmost_window(xcb_connection_t& x11_connection,
bool is_child_window_or_same(xcb_connection_t& x11_connection,
xcb_window_t child,
xcb_window_t parent) {
xcb_generic_error_t* error;

xcb_window_t current_window = child;

xcb_generic_error_t* error;
xcb_query_tree_cookie_t query_cookie =
xcb_query_tree(&x11_connection, child);
xcb_query_tree_reply_t* query_reply =
Expand Down Expand Up @@ -502,6 +561,21 @@ Size get_maximum_screen_dimensions(xcb_connection_t& x11_connection) {
return maximum_screen_size;
}

xcb_window_t get_root_window(xcb_connection_t& x11_connection,
xcb_window_t window) {
xcb_generic_error_t* error;
const xcb_query_tree_cookie_t query_cookie =
xcb_query_tree(&x11_connection, window);
xcb_query_tree_reply_t* query_reply =
xcb_query_tree_reply(&x11_connection, query_cookie, &error);
assert(!error);

const xcb_window_t root = query_reply->root;
free(query_reply);

return root;
}

xcb_window_t get_x11_handle(HWND win32_handle) {
return reinterpret_cast<size_t>(
GetProp(win32_handle, "__wine_x11_whole_window"));
Expand Down
22 changes: 21 additions & 1 deletion src/wine-host/editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ class Editor {
*/
HWND get_win32_handle();

/**
* Returns `true` if the window manager supports the EWMH active window
* protocol through the `_NET_ACTIVE_WINDOW` attribute. Some more
* minimalistic window managers may not support this. In that case we'll
* show a warning and fall back to a more hacky approach to grabbing input
* focus. This involves checking whether the `_NET_ACTIVE_WINDOW` atom
* exists and whether the property is set on the root window. The result is
* cached in `supports_ewmh_active_window_cache`.
*/
bool supports_ewmh_active_window() const;

/**
* Send a single `effEditIdle` event to the plugin to allow it to update its
* GUI state. This is called periodically from a timer while the GUI is
Expand Down Expand Up @@ -157,7 +168,10 @@ class Editor {
private:
/**
* Returns `true` if the currently active window (as per
* `_NET_ACTIVE_WINDOW`) contains `wine_window`.
* `_NET_ACTIVE_WINDOW`) contains `wine_window`. If the window manager does
* not support this hint, this will always return false.
*
* @see Editor::supports_ewmh_active_window
*/
bool is_wine_window_active() const;

Expand Down Expand Up @@ -238,4 +252,10 @@ class Editor {
* The atom corresponding to `_NET_ACTIVE_WINDOW`.
*/
xcb_atom_t active_window_property;
/**
* Whether the root window supports the `_NET_ACTIVE_WINDOW` hint. We'll
* check this once and then cache the results in
* `supports_ewmh_active_window()`.
*/
mutable std::optional<bool> supports_ewmh_active_window_cache;
};

0 comments on commit 225d6a0

Please sign in to comment.