1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
|
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 @@
<wayfire>
<plugin name="raise-on-release">
<_short>Raise on Release</_short>
- <_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>
+ <_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.</_long>
<category>Window Management</category>
</plugin>
</wayfire>
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 <wayfire/core.hpp>
#include <wayfire/option-wrapper.hpp>
#include <wayfire/output.hpp>
#include <wayfire/plugin.hpp>
+#include <wayfire/scene.hpp>
#include <wayfire/seat.hpp>
#include <wayfire/signal-definitions.hpp>
#include <wayfire/toplevel-view.hpp>
@@ -24,6 +27,18 @@ namespace
{
using raise_on_release::action_t;
using button_signal = wf::input_event_signal<wlr_pointer_button_event>;
+using weak_view = std::weak_ptr<wf::view_interface_t>;
+
+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<bool> passthrough{"core/focus_buttons_passthrough"};
raise_on_release::tracker_t tracker;
- std::weak_ptr<wf::view_interface_t> 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<wf::post_input_event_signal<wlr_pointer_button_event>> on_released =
+ [this] (wf::post_input_event_signal<wlr_pointer_button_event> *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<wf::view_move_request_signal> on_move_request =
[this] (wf::view_move_request_signal *ev) { grabbed(ev->view); };
wf::signal::connection_t<wf::view_resize_request_signal> 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 <cmath>
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 <cassert>
#include <cstdio>
@@ -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;
|