diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/a11y.rs | 72 | ||||
| -rw-r--r-- | src/ui/mru.rs | 10 |
2 files changed, 81 insertions, 1 deletions
diff --git a/src/a11y.rs b/src/a11y.rs index f6553138..76d66f0f 100644 --- a/src/a11y.rs +++ b/src/a11y.rs @@ -7,9 +7,12 @@ use accesskit::{ }; use accesskit_unix::Adapter; use calloop::LoopHandle; +use niri_config::MruScope; use crate::layout::workspace::WorkspaceId; use crate::niri::{KeyboardFocus, Niri, State}; +use crate::utils::with_toplevel_role; +use crate::window::mapped::MappedId; const ID_ROOT: NodeId = NodeId(0); const ID_ANNOUNCEMENT: NodeId = NodeId(1); @@ -22,6 +25,9 @@ pub struct A11y { event_loop: LoopHandle<'static, State>, focus: NodeId, workspace_id: Option<WorkspaceId>, + mru_selection: Option<MappedId>, + mru_scope: Option<MruScope>, + last_mru_title: String, last_announcement: String, to_accesskit: Option<mpsc::SyncSender<TreeUpdate>>, } @@ -38,6 +44,9 @@ impl A11y { event_loop, focus: ID_ROOT, workspace_id: None, + mru_selection: None, + mru_scope: None, + last_mru_title: String::new(), last_announcement: String::new(), to_accesskit: None, } @@ -129,9 +138,30 @@ impl Niri { self.a11y.workspace_id = ws_id; let focus = self.a11y_focus(); + + // Check if the MRU selection changed. + let mut update_mru_selection = false; + if focus == ID_MRU { + let current = self.window_mru_ui.current_window_id(); + if self.a11y.mru_selection != current { + update_mru_selection = true; + self.a11y.mru_selection = current; + } + + // If there's no window title to announce, check if there's a scope change. + let scope = self.window_mru_ui.scope(); + if !update_mru_selection && self.a11y.mru_scope != Some(scope) { + announcement = Some(self.window_mru_ui.a11y_scope_text()); + } + self.a11y.mru_scope = Some(scope); + } else { + self.a11y.mru_scope = None; + self.a11y.mru_selection = None; + } + let update_focus = self.a11y.focus != focus; - if !(announcement.is_some() || update_focus) { + if !(announcement.is_some() || update_focus || update_mru_selection) { return; } @@ -150,6 +180,43 @@ impl Niri { nodes.push((ID_ANNOUNCEMENT, node)); } + if focus == ID_MRU { + // Ideally MRU would be a Group with a child Button for a window, but I've no idea how + // to make it work reliably. When I did it that way, there were two issues: + // + // 1. Alt-Tab would always start reading from "Recent windows grouping" instead of the + // window title. + // 2. When Alt-Tab became empty (e.g. switching scope to something empty), Orca would + // completely stop reading any child buttons for the remainder of the session. + // + // I've no idea what to do about these and where they even come from. So, just flip the + // MRU node between Group and Button, which seems to work fine. + if update_mru_selection { + if let Some(id) = self.a11y.mru_selection { + if let Some((_, mapped)) = self.layout.windows().find(|(_, m)| m.id() == id) { + with_toplevel_role(mapped.toplevel(), |role| { + let mut title = role.title.as_deref().unwrap_or("Unknown").to_owned(); + // Change title on match to ensure we announce same-titled windows. + if self.a11y.last_mru_title == title { + title.push(' '); + } + self.a11y.last_mru_title = title; + + let mut mru = Node::new(Role::Button); + mru.set_label(&*self.a11y.last_mru_title); + nodes.push((ID_MRU, mru)); + }); + } + } else { + let mut mru = Node::new(Role::Group); + // Announce the current scope in the empty text to make it clear. + let scope = self.window_mru_ui.a11y_scope_text(); + mru.set_label(format!("Recent windows empty, {scope}")); + nodes.push((ID_MRU, mru)); + } + } + } + let update = TreeUpdate { nodes, tree: None, @@ -259,6 +326,9 @@ impl Niri { let focus = self.a11y_focus(); + // NOTE: we don't fill in current MRU selection here to avoid duplicating code; it should + // get updated right away anyway. + TreeUpdate { nodes: vec![ (ID_ROOT, root), diff --git a/src/ui/mru.rs b/src/ui/mru.rs index 5e7e24df..e7fe9227 100644 --- a/src/ui/mru.rs +++ b/src/ui/mru.rs @@ -1224,6 +1224,16 @@ impl WindowMruUi { _ => None, } } + + #[cfg(feature = "dbus")] + pub fn a11y_scope_text(&self) -> String { + let scope = match self.scope() { + MruScope::All => "all", + MruScope::Output => "output", + MruScope::Workspace => "workspace", + }; + format!("Scope {scope}") + } } fn compute_view_offset(cur_x: f64, working_width: f64, new_col_x: f64, new_col_width: f64) -> f64 { |
