Skip to content

Commit cf7491d

Browse files
rustn00bYaLTeR
authored andcommitted
Add MRU window navigation actions
The MRU actions `focus-window-mru-previous` and `focus-window-mru-next` are used to navigate windows in most-recently-used or least-recently-used order. Whenever a window is focused, it records a timestamp that be used to sort windows in MRU order. This timestamp is not updated immediately, but only after a small delay (lock-in period) to ensure that the focus wasn't transfered to another window in the meantime. This strategy avoids upsetting the MRU order with focus events generated by intermediate windows when moving between two non contiguous windows. The lock-in delay can be configured using the `focus-lockin-ms` configuration argument. Calling either of the `focus-window-mru` actions starts an MRU window traversal sequence if one isn't already in progress. When a sequence is in progress, focus timestamps are no longer updated. A traversal sequence ends when: - either the `Mod` key is released, the focus then stays on the chosen window and its timestamp is immediately refreshed, - or if the `Escape` key is pressed, the focus returns to the window that initially had the focus when the sequence was started. Rename WindowMRU fields Improve window close handling during MRU traversal When the focused window is closed during an MRU traversal, it moves to the previous window in MRU order instead of the default behavior. Removed dbg! calls Merge remote-tracking branch 'upstream/main' into window-mru Hardcode Alt-Tab/Alt-shift-Tab for MRU window nav - Add a `PRESET_BINDINGS` containing MRU navigation actions. `PRESET_BINDINGS` are overridden by user configuration so these remain available if the user needs them for another purpose - Releasing the `Alt` key ends any in-progress MRU window traversal Remove `focus-window-mru` actions from config These actions are configured in presets but no longer available for the bindings section of the configuration Cancel MRU traversal with Alt-Esc Had been forgotten in prior commit and was using `Mod` instead of `Alt` Rephrase some comments Fix Alt-Esc not cancelling window-mru Merge remote-tracking branch 'upstream/main' into window-mru Lock-in focus immediately on user interaction As per suggestion by @bbb651, focus is locked-in immediately if a window is interacted with, ie. receives key events or pointer clicks. This change is also an opportunity to make the lockin timer less aggresive. Merge remote-tracking branch 'upstream/main' into window-mru Simplify WindowMRU::new Now that there is a more general Niri::lockin_focus method, leverage it in WindowMRU. Replace Duration with Instant in WindowMRU timestamp Merge remote-tracking branch 'upstream/main' into window-mru Address PR comments - partial - Swapped meaning of next and previous for MRU traversal - Fixed comment that still referred to `Mod` as leader key for MRU traversal instead of `Alt` - Fixed doc comments that were missing a period - Stop using BinaryHeap in `WindowMRU::new()` - Replaced `WindowMRU::mru_with()` method with a simpler `advance()` - Simplified `Alt` key release handling code in `State::on_keyboard()` Simplify early-mru-commit logic No longer perform the mru-commit/lockin_focus in the next event loop callback. Instead it is handled directly when it is determined that an event (pointer or kbd) is forwarded to the active window. Handle PR comments - `focus_lockin` variables and configuration item renamed to `mru_commit`. - added the Esc key to `suppressed_keys` if it was used to cancel an MRU traversal. - removed `WindowMRU::mru_next` and `WindowMRU::mru_previous` methods as they didn't really provide more than the generic `WindowMRU::advance` method. - removed obsolete `Niri::event_forwarded_to_focused_client` boolean - added calls to `mru_commit()` (formerly `focus_lockin`) in: - `State::on_pointer_axis()` - `State::on_tablet_tool_axis()` - `State::on_tablet_tool_tip()` - `State::on_tablet_tool_proximity()` - `State::on_tablet_tool_button()` - `State::on_gesture_swipe_begin()` - `State::on_gesture_pinch_begin()` - `State::on_gesture_hold_begin()` - `State::on_touch_down()` Merge remote-tracking branch 'upstream/main' into window-mru Merge remote-tracking branch 'upstream/main' into window-mru
1 parent 4c98b87 commit cf7491d

File tree

7 files changed

+321
-19
lines changed

7 files changed

+321
-19
lines changed

niri-config/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use smithay::reexports::input;
2525

2626
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.2, 0.2, 0.2, 1.]);
2727

28+
pub const DEFAULT_MRU_COMMIT_MS: u64 = 750;
29+
2830
pub mod layer_rule;
2931

3032
mod utils;
@@ -72,6 +74,8 @@ pub struct Config {
7274
pub debug: DebugConfig,
7375
#[knuffel(children(name = "workspace"))]
7476
pub workspaces: Vec<Workspace>,
77+
#[knuffel(child, unwrap(argument), default = DEFAULT_MRU_COMMIT_MS)]
78+
pub mru_commit_ms: u64,
7579
}
7680

7781
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
@@ -1433,6 +1437,10 @@ pub enum Action {
14331437
FocusWindow(u64),
14341438
FocusWindowInColumn(#[knuffel(argument)] u8),
14351439
FocusWindowPrevious,
1440+
#[knuffel(skip)]
1441+
FocusWindowMruNext,
1442+
#[knuffel(skip)]
1443+
FocusWindowMruPrevious,
14361444
FocusColumnLeft,
14371445
FocusColumnRight,
14381446
FocusColumnFirst,
@@ -1619,6 +1627,8 @@ impl From<niri_ipc::Action> for Action {
16191627
niri_ipc::Action::FocusWindow { id } => Self::FocusWindow(id),
16201628
niri_ipc::Action::FocusWindowInColumn { index } => Self::FocusWindowInColumn(index),
16211629
niri_ipc::Action::FocusWindowPrevious {} => Self::FocusWindowPrevious,
1630+
niri_ipc::Action::FocusWindowMruNext {} => Self::FocusWindowMruNext,
1631+
niri_ipc::Action::FocusWindowMruPrevious {} => Self::FocusWindowMruPrevious,
16221632
niri_ipc::Action::FocusColumnLeft {} => Self::FocusColumnLeft,
16231633
niri_ipc::Action::FocusColumnRight {} => Self::FocusColumnRight,
16241634
niri_ipc::Action::FocusColumnFirst {} => Self::FocusColumnFirst,
@@ -3741,6 +3751,8 @@ mod tests {
37413751
}
37423752
workspace "workspace-2"
37433753
workspace "workspace-3"
3754+
3755+
mru-commit-ms 123
37443756
"##,
37453757
Config {
37463758
input: Input {
@@ -4209,6 +4221,7 @@ mod tests {
42094221
render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")),
42104222
..Default::default()
42114223
},
4224+
mru_commit_ms: 123u64,
42124225
},
42134226
);
42144227
}

niri-ipc/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ pub enum Action {
225225
},
226226
/// Focus the previously focused window.
227227
FocusWindowPrevious {},
228+
/// Focus the next window in most-recently-used order.
229+
FocusWindowMruNext {},
230+
/// Focus the previous window in most-recently-used order.
231+
FocusWindowMruPrevious {},
228232
/// Focus the column to the left.
229233
FocusColumnLeft {},
230234
/// Focus the column to the right.

src/handlers/compositor.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,11 @@ impl CompositorHandler for State {
274274
}
275275

276276
if was_active {
277-
self.maybe_warp_cursor_to_focus();
277+
if self.niri.window_mru.is_some() {
278+
self.focus_window_mru_next();
279+
} else {
280+
self.maybe_warp_cursor_to_focus();
281+
}
278282
}
279283

280284
// Newly-unmapped toplevels must perform the initial commit-configure sequence

src/handlers/xdg_shell.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,11 @@ impl XdgShellHandler for State {
676676
}
677677

678678
if was_active {
679-
self.maybe_warp_cursor_to_focus();
679+
if self.niri.window_mru.is_some() {
680+
self.focus_window_mru_next();
681+
} else {
682+
self.maybe_warp_cursor_to_focus();
683+
}
680684
}
681685

682686
if let Some(output) = output {

src/input/mod.rs

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::any::Any;
22
use std::cmp::min;
33
use std::collections::hash_map::Entry;
44
use std::collections::HashSet;
5-
use std::time::Duration;
5+
use std::time::{Duration, Instant};
66

77
use calloop::timer::{TimeoutAction, Timer};
88
use input::event::gesture::GestureEventCoordinates as _;
@@ -371,7 +371,6 @@ impl State {
371371
serial,
372372
time,
373373
|this, mods, keysym| {
374-
let bindings = &this.niri.config.borrow().binds;
375374
let key_code = event.key_code();
376375
let modified = keysym.modified_sym();
377376
let raw = keysym.raw_latin_sym_or_raw_current_sym();
@@ -387,19 +386,64 @@ impl State {
387386
}
388387
}
389388

390-
should_intercept_key(
391-
&mut this.niri.suppressed_keys,
392-
bindings,
393-
comp_mod,
394-
key_code,
395-
modified,
396-
raw,
397-
pressed,
398-
*mods,
399-
&this.niri.screenshot_ui,
400-
this.niri.config.borrow().input.disable_power_key_handling,
401-
is_inhibiting_shortcuts,
402-
)
389+
// check if alt key was released while there was an active
390+
// window-mru list. If so, drop the list and update the current window's timestamp.
391+
// window-mru is cancelled *even* when state is locked, however the
392+
// focus timestamp on the active window will not be updated
393+
if !mods.alt && this.niri.window_mru.take().is_some() && !this.niri.is_locked() {
394+
if let Some(m) = this
395+
.niri
396+
.layout
397+
.active_workspace_mut()
398+
.and_then(|ws| ws.active_window_mut())
399+
{
400+
m.update_focus_timestamp(Instant::now());
401+
}
402+
}
403+
404+
// If the ESC key was pressed with the Alt modifier and
405+
// there is an active window-mru, cancel the window-mru and
406+
// refocus the initial window (first in the list).
407+
if pressed && mods.alt && raw == Some(Keysym::Escape) {
408+
if let Some(id) = this
409+
.niri
410+
.window_mru
411+
.take()
412+
.and_then(|wmru| wmru.ids.into_iter().next())
413+
{
414+
this.niri.suppressed_keys.insert(key_code);
415+
let window = this.niri.layout.windows().find(|(_, m)| m.id() == id);
416+
let window = window.map(|(_, m)| m.window.clone());
417+
if let Some(window) = window {
418+
this.focus_window(&window);
419+
return FilterResult::Intercept(None);
420+
}
421+
}
422+
}
423+
424+
let intercept_result = {
425+
let bindings = &this.niri.config.borrow().binds;
426+
should_intercept_key(
427+
&mut this.niri.suppressed_keys,
428+
bindings,
429+
comp_mod,
430+
key_code,
431+
modified,
432+
raw,
433+
pressed,
434+
*mods,
435+
&this.niri.screenshot_ui,
436+
this.niri.config.borrow().input.disable_power_key_handling,
437+
is_inhibiting_shortcuts,
438+
)
439+
};
440+
if matches!(intercept_result, FilterResult::Forward) {
441+
// Interaction with the active window, immediately update
442+
// the active window's focus timestamp without waiting for a
443+
// possible pending MRU lock-in delay.
444+
this.niri.mru_commit();
445+
}
446+
intercept_result
403447
},
404448
) else {
405449
return;
@@ -697,6 +741,12 @@ impl State {
697741
self.focus_window(&window);
698742
}
699743
}
744+
Action::FocusWindowMruNext => {
745+
self.focus_window_mru_next();
746+
}
747+
Action::FocusWindowMruPrevious => {
748+
self.focus_window_mru_previous();
749+
}
700750
Action::SwitchLayout(action) => {
701751
let keyboard = &self.niri.seat.get_keyboard().unwrap();
702752
keyboard.with_xkb_state(self, |mut state| match action {
@@ -2175,6 +2225,10 @@ impl State {
21752225
}
21762226
}
21772227

2228+
// The event is getting forwarded to a client, consider that the
2229+
// MRU Window order shoud be committed.
2230+
self.niri.mru_commit();
2231+
21782232
pointer.button(
21792233
self,
21802234
&ButtonEvent {
@@ -2392,6 +2446,8 @@ impl State {
23922446

23932447
pointer.axis(self, frame);
23942448
pointer.frame(self);
2449+
2450+
self.niri.mru_commit();
23952451
}
23962452

23972453
fn on_tablet_tool_axis<I: InputBackend>(&mut self, event: I::TabletToolAxisEvent)
@@ -2437,6 +2493,8 @@ impl State {
24372493

24382494
self.niri.pointer_hidden = false;
24392495
self.niri.tablet_cursor_location = Some(pos);
2496+
2497+
self.niri.mru_commit();
24402498
}
24412499

24422500
// Redraw to update the cursor position.
@@ -2467,6 +2525,7 @@ impl State {
24672525
self.niri.queue_redraw_all();
24682526
}
24692527
self.niri.focus_layer_surface_if_on_demand(under.layer);
2528+
self.niri.mru_commit();
24702529
}
24712530
}
24722531
TabletToolTipState::Up => {
@@ -2501,6 +2560,7 @@ impl State {
25012560
SERIAL_COUNTER.next_serial(),
25022561
event.time_msec(),
25032562
);
2563+
self.niri.mru_commit();
25042564
}
25052565
self.niri.pointer_hidden = false;
25062566
self.niri.tablet_cursor_location = Some(pos);
@@ -2536,6 +2596,7 @@ impl State {
25362596
SERIAL_COUNTER.next_serial(),
25372597
event.time_msec(),
25382598
);
2599+
self.niri.mru_commit();
25392600
}
25402601
}
25412602

@@ -2562,6 +2623,7 @@ impl State {
25622623
fingers: event.fingers(),
25632624
},
25642625
);
2626+
self.niri.mru_commit();
25652627
}
25662628

25672629
fn on_gesture_swipe_update<I: InputBackend + 'static>(
@@ -2714,6 +2776,7 @@ impl State {
27142776
fingers: event.fingers(),
27152777
},
27162778
);
2779+
self.niri.mru_commit();
27172780
}
27182781

27192782
fn on_gesture_pinch_update<I: InputBackend>(&mut self, event: I::GesturePinchUpdateEvent) {
@@ -2768,6 +2831,7 @@ impl State {
27682831
fingers: event.fingers(),
27692832
},
27702833
);
2834+
self.niri.mru_commit();
27712835
}
27722836

27732837
fn on_gesture_hold_end<I: InputBackend>(&mut self, event: I::GestureHoldEndEvent) {
@@ -2880,6 +2944,7 @@ impl State {
28802944

28812945
// We're using touch, hide the pointer.
28822946
self.niri.pointer_hidden = true;
2947+
self.niri.mru_commit();
28832948
}
28842949
fn on_touch_up<I: InputBackend>(&mut self, evt: I::TouchUpEvent) {
28852950
let Some(handle) = self.niri.seat.get_touch() else {
@@ -3083,6 +3148,41 @@ fn find_bind(
30833148
find_configured_bind(bindings, comp_mod, trigger, mods)
30843149
}
30853150

3151+
/// Preset bindings can be overridden in the user configuration.
3152+
/// The reason for treating them differently is that their key + modifier
3153+
/// combination needs to be frozen for some reason.
3154+
const PRESET_BINDINGS: &[Bind] = &[
3155+
// The following two bindings cover MRU window navigation. They are
3156+
// preset because the `Alt` key is treated specially in `on_keyboard`.
3157+
// When it is released the active MRU traversal is considered to have
3158+
// completed. If the user were allowed to change the MRU bindings
3159+
// below, the navigation mechanism would no longer work as intended.
3160+
Bind {
3161+
key: Key {
3162+
trigger: Trigger::Keysym(Keysym::Tab),
3163+
modifiers: Modifiers::ALT,
3164+
},
3165+
action: Action::FocusWindowMruNext,
3166+
repeat: true,
3167+
cooldown: None,
3168+
allow_when_locked: false,
3169+
allow_inhibiting: true,
3170+
hotkey_overlay_title: None,
3171+
},
3172+
Bind {
3173+
key: Key {
3174+
trigger: Trigger::Keysym(Keysym::Tab),
3175+
modifiers: Modifiers::ALT.union(Modifiers::SHIFT),
3176+
},
3177+
action: Action::FocusWindowMruPrevious,
3178+
repeat: true,
3179+
cooldown: None,
3180+
allow_when_locked: false,
3181+
allow_inhibiting: true,
3182+
hotkey_overlay_title: None,
3183+
},
3184+
];
3185+
30863186
fn find_configured_bind(
30873187
bindings: &Binds,
30883188
comp_mod: CompositorMod,
@@ -3100,7 +3200,9 @@ fn find_configured_bind(
31003200
modifiers |= Modifiers::COMPOSITOR;
31013201
}
31023202

3103-
for bind in &bindings.0 {
3203+
// iterate through configured bindings looking for a match, and
3204+
// then check in `PRESET_BINDINGS` if none were found
3205+
for bind in bindings.0.iter().chain(PRESET_BINDINGS.iter()) {
31043206
if bind.key.trigger != trigger {
31053207
continue;
31063208
}

0 commit comments

Comments
 (0)