diff --git a/plugins/raise-on-release/README.md b/plugins/raise-on-release/README.md index 11093f0..59a3ae6 100644 --- a/plugins/raise-on-release/README.md +++ b/plugins/raise-on-release/README.md @@ -3,8 +3,9 @@ Clicking a window that is behind others gives it keyboard focus at once, but brings it to the front only when you let go of the button. If the press turns into a drag, the window stays where it is, so you can drag a file or text out -of a background window and drop it on the window in front. macOS works this -way, and KDE does too since Scott Jenson pointed it out. +of a background window and drop it on the window in front, which then has +keyboard focus. macOS works this way, and KDE does too since Scott Jenson +pointed it out. ## Setup @@ -32,8 +33,14 @@ draws itself) of an ordinary window: - **Release:** the window comes to the front, so a plain click looks as it always did; only the raise happens a moment later. - **Drag:** the window is not raised if the pointer moves more than 8 pixels - with the button down, or if the app starts a drag-and-drop. After a drop, - focus is wherever the drop leaves it; the plugin changes nothing. + with the button down, or if the app starts a drag-and-drop. +- **End of a drag:** when the button comes up over another window (you + dropped on the window in front), that window gets keyboard focus, as macOS + keeps the app you drop on active. Nothing is raised or lowered. Back over + the window you dragged from, it keeps focus and stays behind. Over the + desktop, a panel or the dock, focus goes back to the window that had it + before the press. If focus has already moved on (the drop target activated + itself, or a window opened), it is left there. - **Moving or resizing:** when the app asks to move or resize the window (a drag on its own titlebar or edge), it is raised as the move starts. - **Focus moves elsewhere** while the button is down (for example the click @@ -58,5 +65,5 @@ options of its own, so it can be enabled in a running session. `meson test` runs `raise-on-release-press`. It covers which presses wait for the release (frames, panels, modifiers, passthrough, core still owning the -button) and when a held press stops being a click (threshold, drag-and-drop, -window moves). +button), when a held press stops being a click (threshold, drag-and-drop, +window moves), and where keyboard focus goes when a drag ends. diff --git a/plugins/raise-on-release/raise-on-release.xml b/plugins/raise-on-release/raise-on-release.xml index 46c85e9..afecaac 100644 --- a/plugins/raise-on-release/raise-on-release.xml +++ b/plugins/raise-on-release/raise-on-release.xml @@ -2,7 +2,7 @@ <_short>Raise on Release - <_long>Clicking a window behind others gives it keyboard focus at once and raises it when the button comes up. If the press becomes a drag, the window stays behind, so you can drag something from it onto the window in front. Takes the left button when Mouse buttons to focus views (core) leaves it out. + <_long>Clicking a window behind others gives it keyboard focus at once and raises it when the button comes up. If the press becomes a drag, the window stays behind, so you can drag something from it onto the window in front, which then keeps keyboard focus. Takes the left button when Mouse buttons to focus views (core) leaves it out. Window Management diff --git a/plugins/raise-on-release/src/plugin.cpp b/plugins/raise-on-release/src/plugin.cpp index cf05209..b29f941 100644 --- a/plugins/raise-on-release/src/plugin.cpp +++ b/plugins/raise-on-release/src/plugin.cpp @@ -1,13 +1,16 @@ // raise-on-release: clicking a window behind others gives it keyboard focus // at once but raises it only when the button comes up, and not at all if the // press became a drag, so something can be dragged out of a background window -// onto the one in front, as on macOS. It takes the left button only when -// core/focus_buttons leaves it out; idle, it only watches button presses. +// onto the one in front, as on macOS. When such a drag ends over another +// window, that window gets keyboard focus (still without raising anything). +// It takes the left button only when core/focus_buttons leaves it out; idle, +// it only watches button presses. #include "press.hpp" #include #include #include #include +#include #include #include #include @@ -24,6 +27,18 @@ namespace { using raise_on_release::action_t; using button_signal = wf::input_event_signal; +using weak_view = std::weak_ptr; + +weak_view keep(wayfire_view view) +{ + return view ? view->weak_from_this() : weak_view{}; +} + +wayfire_view alive(const weak_view& weak) +{ + auto view = weak.lock(); + return (view && view->is_mapped()) ? wayfire_view(view.get()) : nullptr; +} // One app window: a view and its dialogs share their topmost parent. bool same_window(wayfire_view a, wayfire_view b) @@ -39,7 +54,10 @@ class raise_on_release_plugin_t : public wf::plugin_interface_t wf::option_wrapper_t passthrough{"core/focus_buttons_passthrough"}; raise_on_release::tracker_t tracker; - std::weak_ptr pressed; + weak_view pressed; // the window under the press + weak_view focused; // where the press put keyboard focus (a modal dialog, maybe) + weak_view previous; // what had keyboard focus before the press + struct { weak_view pressed, focused, previous; } dragged; // until the release is processed wf::wl_listener_wrapper on_start_drag; bool core_focuses(uint32_t button) @@ -70,8 +88,10 @@ class raise_on_release_plugin_t : public wf::plugin_interface_t void focus_and_wait(wayfire_view view) { auto& core = wf::get_core(); + previous = keep(core.seat->get_active_view()); core.seat->focus_output(view->get_output()); core.seat->focus_view(view); + focused = keep(core.seat->get_active_view()); auto at = core.get_cursor_position(); tracker.press(at.x, at.y); pressed = view->weak_from_this(); @@ -84,14 +104,15 @@ class raise_on_release_plugin_t : public wf::plugin_interface_t wayfire_view pressed_view() { - auto view = pressed.lock(); - return (view && view->is_mapped()) ? wayfire_view(view.get()) : nullptr; + return alive(pressed); } void stop() { tracker.state = raise_on_release::tracker_t::IDLE; pressed.reset(); + focused.reset(); + previous.reset(); on_motion.disconnect(); on_motion_absolute.disconnect(); on_move_request.disconnect(); @@ -117,10 +138,17 @@ class raise_on_release_plugin_t : public wf::plugin_interface_t if (ev->event->state == WL_POINTER_BUTTON_STATE_RELEASED) { - bool click = tracker.release(); - auto view = pressed_view(); + auto outcome = tracker.release(); + auto view = pressed_view(); + if (outcome == raise_on_release::tracker_t::DRAGGED) + { + // Decide once the release (and any drop) has been sent. + dragged = {pressed, focused, previous}; + wf::get_core().connect(&on_released); + } + stop(); - if (click) + if (outcome == raise_on_release::tracker_t::RAISE) { raise(view); } @@ -186,6 +214,65 @@ class raise_on_release_plugin_t : public wf::plugin_interface_t } } + // Keyboard focus without raising, as at the press. + void focus(wayfire_view view) + { + auto& seat = wf::get_core().seat; + if (view->get_output()) + { + seat->focus_output(view->get_output()); + } + + seat->focus_view(view); + } + + // A drag out of a background window has ended and the drop, if any, has + // been sent: the window under the pointer gets keyboard focus, as on + // macOS. Released on the desktop or a panel, focus goes back to where it + // was before the press. + wf::signal::connection_t> on_released = + [this] (wf::post_input_event_signal *ev) + { + if ((ev->event->button != BTN_LEFT) || (ev->event->state != WL_POINTER_BUTTON_STATE_RELEASED)) + { + return; + } + + on_released.disconnect(); + auto& core = wf::get_core(); + auto source = alive(dragged.pressed); + auto ours = alive(dragged.focused); + auto before = alive(dragged.previous); + dragged = {}; + auto active = core.seat->get_active_view(); + auto at = core.scene()->find_node_at(core.get_cursor_position()); + auto under = at ? wf::node_to_view(at->node.get()) : nullptr; + if (under && !under->is_mapped()) + { + under = nullptr; + } + + raise_on_release::drop_t drop; + drop.source_focused = ours && (active == ours); + drop.over_source = source && under && same_window(source, under); + drop.over_app_window = under && wf::toplevel_cast(under) && + (under->role == wf::VIEW_ROLE_TOPLEVEL) && under->get_keyboard_focus_surface(); + drop.previous = before && (before != active) && before->get_keyboard_focus_surface(); + switch (raise_on_release::after_drag(drop)) + { + case raise_on_release::after_drag_t::KEEP: + return; + + case raise_on_release::after_drag_t::TARGET: + focus(under); + return; + + case raise_on_release::after_drag_t::PREVIOUS: + focus(before); + return; + } + }; + wf::signal::connection_t on_move_request = [this] (wf::view_move_request_signal *ev) { grabbed(ev->view); }; wf::signal::connection_t on_resize_request = @@ -207,6 +294,7 @@ class raise_on_release_plugin_t : public wf::plugin_interface_t { stop(); on_button.disconnect(); + on_released.disconnect(); } }; } diff --git a/plugins/raise-on-release/src/press.hpp b/plugins/raise-on-release/src/press.hpp index 7d8cf90..5e1abef 100644 --- a/plugins/raise-on-release/src/press.hpp +++ b/plugins/raise-on-release/src/press.hpp @@ -1,6 +1,7 @@ #pragma once // Pure raise-on-release logic, no Wayfire dependencies: what to do with a -// left-button press, and whether the held press is still a click. +// left-button press, whether the held press is still a click, and where +// keyboard focus goes when it ends as a drag. #include namespace raise_on_release @@ -47,7 +48,8 @@ inline action_t classify(const press_t& p) /* A deferred press, from button down to button up. Moving the pointer * further than `threshold` makes it a drag (no raise on release); an app * starting drag-and-drop does too, for good. The window manager moving or - * resizing the window (a client-side titlebar drag) raises it at once. */ + * resizing the window (a client-side titlebar drag) raises it at once, and + * then the release does nothing. */ struct tracker_t { enum state_t { IDLE, CLICK, MOVED, DND } state = IDLE; @@ -88,12 +90,54 @@ struct tracker_t return raise; } - // True if the window should be raised now. - bool release() + enum release_t + { + NOTHING, // no press held, or the window was already raised by a move + RAISE, // a click: raise the window now + DRAGGED, // a drag ended: see after_drag() + }; + + release_t release() { - bool raise = (state == CLICK); + release_t r = (state == CLICK) ? RAISE : (state == IDLE) ? NOTHING : DRAGGED; state = IDLE; - return raise; + return r; } }; + +// Where keyboard focus goes once the button is up after a drag out of a +// background window (and any drop has been sent), as on macOS where the +// window you drop on stays active. +enum class after_drag_t +{ + KEEP, // leave focus where it is + TARGET, // the window under the pointer, without raising it + PREVIOUS, // back to the window focused before the press +}; + +struct drop_t +{ + bool source_focused = true; // focus is still where the press put it + bool over_source = false; // the pointer is over the source window (or its dialogs) + bool over_app_window = false; // ... over another ordinary window that takes keyboard focus + bool previous = false; // the window focused before the press is another window, still open +}; + +inline after_drag_t after_drag(const drop_t& d) +{ + // Something else took focus meanwhile (the drop target activated itself, + // a window opened): leave it. Back on the source: it keeps focus. + if (!d.source_focused || d.over_source) + { + return after_drag_t::KEEP; + } + + if (d.over_app_window) + { + return after_drag_t::TARGET; + } + + // The desktop, a panel or the dock. + return d.previous ? after_drag_t::PREVIOUS : after_drag_t::KEEP; +} } diff --git a/plugins/raise-on-release/tests/press-test.cpp b/plugins/raise-on-release/tests/press-test.cpp index 9d4c907..6b37dbe 100644 --- a/plugins/raise-on-release/tests/press-test.cpp +++ b/plugins/raise-on-release/tests/press-test.cpp @@ -1,5 +1,5 @@ -// Unit tests for raise-on-release: which presses wait for the release, and -// when a held press stops being a click. +// Unit tests for raise-on-release: which presses wait for the release, when +// a held press stops being a click, and where focus goes after a drag. #include "../src/press.hpp" #include #include @@ -69,46 +69,47 @@ int main() // A click, even a slightly shaky one, raises on release. tracker_t t; - assert(!t.held() && !t.release()); + assert(!t.held() && (t.release() == tracker_t::NOTHING)); t.press(100, 100); assert(t.held()); t.motion(103, 104); // 5 px t.motion(108, 100); // 8 px: still a click - assert(t.release() && !t.held()); - assert(!t.release()); // only once + assert((t.release() == tracker_t::RAISE) && !t.held()); + assert(t.release() == tracker_t::NOTHING); // only once // Moving further than the threshold makes it a drag: no raise, even if - // the pointer comes back. + // the pointer comes back; focus is settled after the release instead. t.press(100, 100); t.motion(106, 106); // 8.5 px t.motion(100, 100); - assert(t.held() && !t.release()); + assert(t.held() && (t.release() == tracker_t::DRAGGED) && !t.held()); - // An app starting drag-and-drop: no raise, whether it started before or + // An app starting drag-and-drop: a drag, whether it started before or // after the pointer passed the threshold. t.press(10, 10); t.drag_started(); - assert(!t.release()); + assert(t.release() == tracker_t::DRAGGED); t.press(10, 10); t.motion(40, 10); t.drag_started(); - assert(!t.release()); - // ... and a drop moving nothing afterwards does not bring it back. + assert(t.release() == tracker_t::DRAGGED); + // ... and a drop moving nothing afterwards does not bring the raise back. t.press(10, 10); t.drag_started(); t.motion(10, 10); - assert(!t.window_grabbed() && !t.release()); + assert(!t.window_grabbed() && (t.release() == tracker_t::DRAGGED)); // Drag-and-drop outside a press is not ours. t.drag_started(); - assert(!t.held()); + assert(!t.held() && (t.release() == tracker_t::NOTHING)); // A client-side titlebar drag: the window manager starts moving it. - // Raise at once, before or after the threshold, and not again on release. + // Raise at once, before or after the threshold, and leave focus alone on + // release. t.press(0, 0); - assert(t.window_grabbed() && !t.held() && !t.release()); + assert(t.window_grabbed() && !t.held() && (t.release() == tracker_t::NOTHING)); t.press(0, 0); t.motion(0, 30); - assert(t.window_grabbed() && !t.release()); + assert(t.window_grabbed() && (t.release() == tracker_t::NOTHING)); // A move without a press (keyboard move) is not ours. assert(!t.window_grabbed()); @@ -117,7 +118,39 @@ int main() t.threshold = 2; t.press(0, 0); t.motion(2, 1); - assert(!t.release()); + assert(t.release() == tracker_t::DRAGGED); + + // After a drag: dropped on the window in front, that window gets focus + // (macOS keeps the app you drop on active). + drop_t d; + d.over_app_window = d.previous = true; + assert(after_drag(d) == after_drag_t::TARGET); + // ... also when the window before the press was the source itself. + d.previous = false; + assert(after_drag(d) == after_drag_t::TARGET); + + // Back over the source window (or its dialog): it keeps focus, behind. + d = {}; + d.over_source = d.previous = true; + assert(after_drag(d) == after_drag_t::KEEP); + + // Over the desktop, a panel, the dock or a window that takes no keyboard + // focus: focus goes back to the window that had it before the press ... + d = {}; + d.previous = true; + assert(after_drag(d) == after_drag_t::PREVIOUS); + // ... or stays on the source if that window is gone (or there was none). + d.previous = false; + assert(after_drag(d) == after_drag_t::KEEP); + + // Focus moved during the drag or the drop (the target activated itself, + // a dialog opened): never take it back, wherever the button came up. + d = {}; + d.source_focused = false; + d.over_app_window = d.previous = true; + assert(after_drag(d) == after_drag_t::KEEP); + d.over_app_window = false; + assert(after_drag(d) == after_drag_t::KEEP); std::puts("raise-on-release press tests passed"); return 0;