| 1 | // Copyright 2022 The AccessKit Authors. All rights reserved. |
| 2 | // Licensed under the Apache License, Version 2.0 (found in |
| 3 | // the LICENSE-APACHE file) or the MIT license (found in |
| 4 | // the LICENSE-MIT file), at your option. |
| 5 | |
| 6 | // Derived from Chromium's accessibility abstraction. |
| 7 | // Copyright 2017 The Chromium Authors. All rights reserved. |
| 8 | // Use of this source code is governed by a BSD-style license that can be |
| 9 | // found in the LICENSE.chromium file. |
| 10 | |
| 11 | use accesskit::{ |
| 12 | Action, ActionData, ActionRequest, Affine, Live, NodeId, Orientation, Point, Rect, Role, |
| 13 | Toggled, |
| 14 | }; |
| 15 | use accesskit_consumer::{FilterResult, Node, TreeState}; |
| 16 | use atspi_common::{ |
| 17 | CoordType, Granularity, Interface, InterfaceSet, Layer, Live as AtspiLive, Role as AtspiRole, |
| 18 | ScrollType, State, StateSet, |
| 19 | }; |
| 20 | use std::{ |
| 21 | collections::HashMap, |
| 22 | hash::{Hash, Hasher}, |
| 23 | iter::FusedIterator, |
| 24 | sync::{Arc, RwLock, RwLockReadGuard, Weak}, |
| 25 | }; |
| 26 | |
| 27 | use crate::{ |
| 28 | adapter::Adapter, |
| 29 | context::{AppContext, Context}, |
| 30 | filters::filter, |
| 31 | util::*, |
| 32 | Action as AtspiAction, Error, ObjectEvent, Property, Rect as AtspiRect, Result, |
| 33 | }; |
| 34 | |
| 35 | pub(crate) struct NodeWrapper<'a>(pub(crate) &'a Node<'a>); |
| 36 | |
| 37 | impl<'a> NodeWrapper<'a> { |
| 38 | pub(crate) fn name(&self) -> Option<String> { |
| 39 | if self.0.label_comes_from_value() { |
| 40 | self.0.value() |
| 41 | } else { |
| 42 | self.0.label() |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | pub(crate) fn description(&self) -> Option<String> { |
| 47 | self.0.description() |
| 48 | } |
| 49 | |
| 50 | pub(crate) fn parent_id(&self) -> Option<NodeId> { |
| 51 | self.0.parent_id() |
| 52 | } |
| 53 | |
| 54 | pub(crate) fn id(&self) -> NodeId { |
| 55 | self.0.id() |
| 56 | } |
| 57 | |
| 58 | fn filtered_child_ids( |
| 59 | &self, |
| 60 | ) -> impl DoubleEndedIterator<Item = NodeId> + FusedIterator<Item = NodeId> + '_ { |
| 61 | self.0.filtered_children(&filter).map(|child| child.id()) |
| 62 | } |
| 63 | |
| 64 | pub(crate) fn role(&self) -> AtspiRole { |
| 65 | if self.0.has_role_description() { |
| 66 | return AtspiRole::Extended; |
| 67 | } |
| 68 | |
| 69 | match self.0.role() { |
| 70 | Role::Alert => AtspiRole::Notification, |
| 71 | Role::AlertDialog => AtspiRole::Alert, |
| 72 | Role::Comment | Role::Suggestion => AtspiRole::Section, |
| 73 | // TODO: See how to represent ARIA role="application" |
| 74 | Role::Application => AtspiRole::Embedded, |
| 75 | Role::Article => AtspiRole::Article, |
| 76 | Role::Audio => AtspiRole::Audio, |
| 77 | Role::Banner | Role::Header => AtspiRole::Landmark, |
| 78 | Role::Blockquote => AtspiRole::BlockQuote, |
| 79 | Role::Caret => AtspiRole::Unknown, |
| 80 | Role::Button => { |
| 81 | if self.0.toggled().is_some() { |
| 82 | AtspiRole::ToggleButton |
| 83 | } else { |
| 84 | AtspiRole::PushButton |
| 85 | } |
| 86 | } |
| 87 | Role::DefaultButton => AtspiRole::PushButton, |
| 88 | Role::Canvas => AtspiRole::Canvas, |
| 89 | Role::Caption => AtspiRole::Caption, |
| 90 | Role::Cell => AtspiRole::TableCell, |
| 91 | Role::CheckBox => AtspiRole::CheckBox, |
| 92 | Role::Switch => AtspiRole::ToggleButton, |
| 93 | Role::ColorWell => AtspiRole::PushButton, |
| 94 | Role::ColumnHeader => AtspiRole::ColumnHeader, |
| 95 | Role::ComboBox | Role::EditableComboBox => AtspiRole::ComboBox, |
| 96 | Role::Complementary => AtspiRole::Landmark, |
| 97 | Role::ContentDeletion => AtspiRole::ContentDeletion, |
| 98 | Role::ContentInsertion => AtspiRole::ContentInsertion, |
| 99 | Role::ContentInfo | Role::Footer => AtspiRole::Landmark, |
| 100 | Role::Definition | Role::DescriptionListDetail => AtspiRole::DescriptionValue, |
| 101 | Role::DescriptionList => AtspiRole::DescriptionList, |
| 102 | Role::DescriptionListTerm => AtspiRole::DescriptionTerm, |
| 103 | Role::Details => AtspiRole::Panel, |
| 104 | Role::Dialog => AtspiRole::Dialog, |
| 105 | Role::Directory => AtspiRole::List, |
| 106 | Role::DisclosureTriangle => AtspiRole::ToggleButton, |
| 107 | Role::DocCover => AtspiRole::Image, |
| 108 | Role::DocBackLink | Role::DocBiblioRef | Role::DocGlossRef | Role::DocNoteRef => { |
| 109 | AtspiRole::Link |
| 110 | } |
| 111 | Role::DocBiblioEntry | Role::DocEndnote => AtspiRole::ListItem, |
| 112 | Role::DocNotice | Role::DocTip => AtspiRole::Comment, |
| 113 | Role::DocFootnote => AtspiRole::Footnote, |
| 114 | Role::DocPageBreak => AtspiRole::Separator, |
| 115 | Role::DocPageFooter => AtspiRole::Footer, |
| 116 | Role::DocPageHeader => AtspiRole::Header, |
| 117 | Role::DocAcknowledgements |
| 118 | | Role::DocAfterword |
| 119 | | Role::DocAppendix |
| 120 | | Role::DocBibliography |
| 121 | | Role::DocChapter |
| 122 | | Role::DocConclusion |
| 123 | | Role::DocCredits |
| 124 | | Role::DocEndnotes |
| 125 | | Role::DocEpilogue |
| 126 | | Role::DocErrata |
| 127 | | Role::DocForeword |
| 128 | | Role::DocGlossary |
| 129 | | Role::DocIndex |
| 130 | | Role::DocIntroduction |
| 131 | | Role::DocPageList |
| 132 | | Role::DocPart |
| 133 | | Role::DocPreface |
| 134 | | Role::DocPrologue |
| 135 | | Role::DocToc => AtspiRole::Landmark, |
| 136 | Role::DocAbstract |
| 137 | | Role::DocColophon |
| 138 | | Role::DocCredit |
| 139 | | Role::DocDedication |
| 140 | | Role::DocEpigraph |
| 141 | | Role::DocExample |
| 142 | | Role::DocPullquote |
| 143 | | Role::DocQna => AtspiRole::Section, |
| 144 | Role::DocSubtitle => AtspiRole::Heading, |
| 145 | Role::Document => AtspiRole::DocumentFrame, |
| 146 | Role::EmbeddedObject => AtspiRole::Embedded, |
| 147 | // TODO: Forms which lack an accessible name are no longer |
| 148 | // exposed as forms. Forms which have accessible |
| 149 | // names should be exposed as `AtspiRole::Landmark` according to Core AAM. |
| 150 | Role::Form => AtspiRole::Form, |
| 151 | Role::Figure | Role::Feed => AtspiRole::Panel, |
| 152 | Role::GenericContainer |
| 153 | | Role::FooterAsNonLandmark |
| 154 | | Role::HeaderAsNonLandmark |
| 155 | | Role::Ruby => AtspiRole::Section, |
| 156 | Role::GraphicsDocument => AtspiRole::DocumentFrame, |
| 157 | Role::GraphicsObject => AtspiRole::Panel, |
| 158 | Role::GraphicsSymbol => AtspiRole::Image, |
| 159 | Role::Grid => AtspiRole::Table, |
| 160 | Role::Group => AtspiRole::Panel, |
| 161 | Role::Heading => AtspiRole::Heading, |
| 162 | Role::Iframe | Role::IframePresentational => AtspiRole::InternalFrame, |
| 163 | // TODO: If there are unignored children, then it should be AtspiRole::ImageMap. |
| 164 | Role::Image => AtspiRole::Image, |
| 165 | Role::TextRun => AtspiRole::Static, |
| 166 | Role::Legend => AtspiRole::Label, |
| 167 | // Layout table objects are treated the same as `Role::GenericContainer`. |
| 168 | Role::LayoutTable => AtspiRole::Section, |
| 169 | Role::LayoutTableCell => AtspiRole::Section, |
| 170 | Role::LayoutTableRow => AtspiRole::Section, |
| 171 | // TODO: Having a separate accessible object for line breaks |
| 172 | // is inconsistent with other implementations. |
| 173 | Role::LineBreak => AtspiRole::Static, |
| 174 | Role::Link => AtspiRole::Link, |
| 175 | Role::List => AtspiRole::List, |
| 176 | Role::ListBox => AtspiRole::ListBox, |
| 177 | // TODO: Use `AtspiRole::MenuItem' inside a combo box. |
| 178 | Role::ListBoxOption => AtspiRole::ListItem, |
| 179 | Role::ListGrid => AtspiRole::Table, |
| 180 | Role::ListItem => AtspiRole::ListItem, |
| 181 | // Regular list markers only expose their alternative text, but do not |
| 182 | // expose their descendants, and the descendants should be ignored. This |
| 183 | // is because the alternative text depends on the counter style and can |
| 184 | // be different from the actual (visual) marker text, and hence, |
| 185 | // inconsistent with the descendants. We treat a list marker as non-text |
| 186 | // only if it still has non-ignored descendants, which happens only when => |
| 187 | // - The list marker itself is ignored but the descendants are not |
| 188 | // - Or the list marker contains images |
| 189 | Role::ListMarker => AtspiRole::Static, |
| 190 | Role::Log => AtspiRole::Log, |
| 191 | Role::Main => AtspiRole::Landmark, |
| 192 | Role::Mark => AtspiRole::Static, |
| 193 | Role::Math => AtspiRole::Math, |
| 194 | Role::Marquee => AtspiRole::Marquee, |
| 195 | Role::Menu | Role::MenuListPopup => AtspiRole::Menu, |
| 196 | Role::MenuBar => AtspiRole::MenuBar, |
| 197 | Role::MenuItem | Role::MenuListOption => AtspiRole::MenuItem, |
| 198 | Role::MenuItemCheckBox => AtspiRole::CheckMenuItem, |
| 199 | Role::MenuItemRadio => AtspiRole::RadioMenuItem, |
| 200 | Role::Meter => AtspiRole::LevelBar, |
| 201 | Role::Navigation => AtspiRole::Landmark, |
| 202 | Role::Note => AtspiRole::Comment, |
| 203 | Role::Pane | Role::ScrollView => AtspiRole::Panel, |
| 204 | Role::Paragraph => AtspiRole::Paragraph, |
| 205 | Role::PdfActionableHighlight => AtspiRole::PushButton, |
| 206 | Role::PdfRoot => AtspiRole::DocumentFrame, |
| 207 | Role::PluginObject => AtspiRole::Embedded, |
| 208 | Role::Portal => AtspiRole::PushButton, |
| 209 | Role::Pre => AtspiRole::Section, |
| 210 | Role::ProgressIndicator => AtspiRole::ProgressBar, |
| 211 | Role::RadioButton => AtspiRole::RadioButton, |
| 212 | Role::RadioGroup => AtspiRole::Panel, |
| 213 | Role::Region => AtspiRole::Landmark, |
| 214 | Role::RootWebArea => AtspiRole::DocumentWeb, |
| 215 | Role::Row => AtspiRole::TableRow, |
| 216 | Role::RowGroup => AtspiRole::Panel, |
| 217 | Role::RowHeader => AtspiRole::RowHeader, |
| 218 | // TODO: Generally exposed as description on <ruby> (`Role::Ruby`) element, not |
| 219 | // as its own object in the tree. |
| 220 | // However, it's possible to make a `Role::RubyAnnotation` element show up in the |
| 221 | // tree, for example by adding tabindex="0" to the source <rp> or <rt> |
| 222 | // element or making the source element the target of an aria-owns. |
| 223 | // Therefore, we need to gracefully handle it if it actually |
| 224 | // shows up in the tree. |
| 225 | Role::RubyAnnotation => AtspiRole::Static, |
| 226 | Role::Section => AtspiRole::Section, |
| 227 | Role::ScrollBar => AtspiRole::ScrollBar, |
| 228 | Role::Search => AtspiRole::Landmark, |
| 229 | Role::Slider => AtspiRole::Slider, |
| 230 | Role::SpinButton => AtspiRole::SpinButton, |
| 231 | Role::Splitter => AtspiRole::Separator, |
| 232 | Role::Label => AtspiRole::Label, |
| 233 | Role::Status => AtspiRole::StatusBar, |
| 234 | Role::SvgRoot => AtspiRole::DocumentFrame, |
| 235 | Role::Tab => AtspiRole::PageTab, |
| 236 | Role::Table => AtspiRole::Table, |
| 237 | Role::TabList => AtspiRole::PageTabList, |
| 238 | Role::TabPanel => AtspiRole::ScrollPane, |
| 239 | // TODO: This mapping should also be applied to the dfn |
| 240 | // element. |
| 241 | Role::Term => AtspiRole::DescriptionTerm, |
| 242 | Role::TitleBar => AtspiRole::TitleBar, |
| 243 | Role::TextInput |
| 244 | | Role::MultilineTextInput |
| 245 | | Role::SearchInput |
| 246 | | Role::EmailInput |
| 247 | | Role::NumberInput |
| 248 | | Role::PhoneNumberInput |
| 249 | | Role::UrlInput => AtspiRole::Entry, |
| 250 | Role::DateInput |
| 251 | | Role::DateTimeInput |
| 252 | | Role::WeekInput |
| 253 | | Role::MonthInput |
| 254 | | Role::TimeInput => AtspiRole::DateEditor, |
| 255 | Role::PasswordInput => AtspiRole::PasswordText, |
| 256 | Role::Abbr | Role::Code | Role::Emphasis | Role::Strong | Role::Time => { |
| 257 | AtspiRole::Static |
| 258 | } |
| 259 | Role::Timer => AtspiRole::Timer, |
| 260 | Role::Toolbar => AtspiRole::ToolBar, |
| 261 | Role::Tooltip => AtspiRole::ToolTip, |
| 262 | Role::Tree => AtspiRole::Tree, |
| 263 | Role::TreeItem => AtspiRole::TreeItem, |
| 264 | Role::TreeGrid => AtspiRole::TreeTable, |
| 265 | Role::Video => AtspiRole::Video, |
| 266 | // In AT-SPI, elements with `AtspiRole::Frame` are windows with titles and |
| 267 | // buttons, while those with `AtspiRole::Window` are windows without those |
| 268 | // elements. |
| 269 | Role::Window => AtspiRole::Frame, |
| 270 | Role::WebView => AtspiRole::Panel, |
| 271 | Role::FigureCaption => AtspiRole::Caption, |
| 272 | // TODO: Are there special cases to consider? |
| 273 | Role::Unknown => AtspiRole::Unknown, |
| 274 | Role::ImeCandidate | Role::Keyboard => AtspiRole::RedundantObject, |
| 275 | Role::Terminal => AtspiRole::Terminal, |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | fn is_focused(&self) -> bool { |
| 280 | self.0.is_focused() |
| 281 | } |
| 282 | |
| 283 | pub(crate) fn state(&self, is_window_focused: bool) -> StateSet { |
| 284 | let state = self.0; |
| 285 | let atspi_role = self.role(); |
| 286 | let mut atspi_state = StateSet::empty(); |
| 287 | if state.parent_id().is_none() && state.role() == Role::Window && is_window_focused { |
| 288 | atspi_state.insert(State::Active); |
| 289 | } |
| 290 | if state.is_text_input() && !state.is_read_only() { |
| 291 | atspi_state.insert(State::Editable); |
| 292 | } |
| 293 | // TODO: Focus and selection. |
| 294 | if state.is_focusable() { |
| 295 | atspi_state.insert(State::Focusable); |
| 296 | } |
| 297 | if let Some(orientation) = state.orientation() { |
| 298 | atspi_state.insert(if orientation == Orientation::Horizontal { |
| 299 | State::Horizontal |
| 300 | } else { |
| 301 | State::Vertical |
| 302 | }); |
| 303 | } |
| 304 | let filter_result = filter(self.0); |
| 305 | if filter_result == FilterResult::Include { |
| 306 | atspi_state.insert(State::Visible | State::Showing); |
| 307 | } |
| 308 | if atspi_role != AtspiRole::ToggleButton && state.toggled().is_some() { |
| 309 | atspi_state.insert(State::Checkable); |
| 310 | } |
| 311 | if let Some(selected) = state.is_selected() { |
| 312 | if !state.is_disabled() { |
| 313 | atspi_state.insert(State::Selectable); |
| 314 | } |
| 315 | if selected { |
| 316 | atspi_state.insert(State::Selected); |
| 317 | } |
| 318 | } |
| 319 | if state.is_text_input() { |
| 320 | atspi_state.insert(State::SelectableText); |
| 321 | atspi_state.insert(match state.is_multiline() { |
| 322 | true => State::MultiLine, |
| 323 | false => State::SingleLine, |
| 324 | }); |
| 325 | } |
| 326 | |
| 327 | // Special case for indeterminate progressbar. |
| 328 | if state.role() == Role::ProgressIndicator && state.numeric_value().is_none() { |
| 329 | atspi_state.insert(State::Indeterminate); |
| 330 | } |
| 331 | |
| 332 | // Toggled state |
| 333 | match state.toggled() { |
| 334 | Some(Toggled::Mixed) => atspi_state.insert(State::Indeterminate), |
| 335 | Some(Toggled::True) if atspi_role == AtspiRole::ToggleButton => { |
| 336 | atspi_state.insert(State::Pressed) |
| 337 | } |
| 338 | Some(Toggled::True) => atspi_state.insert(State::Checked), |
| 339 | _ => {} |
| 340 | } |
| 341 | |
| 342 | if state.is_read_only_supported() && state.is_read_only_or_disabled() { |
| 343 | atspi_state.insert(State::ReadOnly); |
| 344 | } else { |
| 345 | atspi_state.insert(State::Enabled | State::Sensitive); |
| 346 | } |
| 347 | |
| 348 | if self.is_focused() { |
| 349 | atspi_state.insert(State::Focused); |
| 350 | } |
| 351 | |
| 352 | atspi_state |
| 353 | } |
| 354 | |
| 355 | fn attributes(&self) -> HashMap<&'static str, String> { |
| 356 | let mut attributes = HashMap::new(); |
| 357 | if let Some(placeholder) = self.0.placeholder() { |
| 358 | attributes.insert("placeholder-text" , placeholder); |
| 359 | } |
| 360 | attributes |
| 361 | } |
| 362 | |
| 363 | fn is_root(&self) -> bool { |
| 364 | self.0.is_root() |
| 365 | } |
| 366 | |
| 367 | fn supports_action(&self) -> bool { |
| 368 | self.0.is_clickable() |
| 369 | } |
| 370 | |
| 371 | fn supports_component(&self) -> bool { |
| 372 | self.0.raw_bounds().is_some() || self.is_root() |
| 373 | } |
| 374 | |
| 375 | fn supports_text(&self) -> bool { |
| 376 | self.0.supports_text_ranges() |
| 377 | } |
| 378 | |
| 379 | fn supports_value(&self) -> bool { |
| 380 | self.current_value().is_some() |
| 381 | } |
| 382 | |
| 383 | pub(crate) fn interfaces(&self) -> InterfaceSet { |
| 384 | let mut interfaces = InterfaceSet::new(Interface::Accessible); |
| 385 | if self.supports_action() { |
| 386 | interfaces.insert(Interface::Action); |
| 387 | } |
| 388 | if self.supports_component() { |
| 389 | interfaces.insert(Interface::Component); |
| 390 | } |
| 391 | if self.supports_text() { |
| 392 | interfaces.insert(Interface::Text); |
| 393 | } |
| 394 | if self.supports_value() { |
| 395 | interfaces.insert(Interface::Value); |
| 396 | } |
| 397 | interfaces |
| 398 | } |
| 399 | |
| 400 | pub(crate) fn live(&self) -> AtspiLive { |
| 401 | let live = self.0.live(); |
| 402 | match live { |
| 403 | Live::Off => AtspiLive::None, |
| 404 | Live::Polite => AtspiLive::Polite, |
| 405 | Live::Assertive => AtspiLive::Assertive, |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | fn n_actions(&self) -> i32 { |
| 410 | if self.0.is_clickable() { |
| 411 | 1 |
| 412 | } else { |
| 413 | 0 |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | fn get_action_name(&self, index: i32) -> String { |
| 418 | if index != 0 { |
| 419 | return String::new(); |
| 420 | } |
| 421 | String::from(if self.0.is_clickable() { "click" } else { "" }) |
| 422 | } |
| 423 | |
| 424 | fn raw_bounds_and_transform(&self) -> (Option<Rect>, Affine) { |
| 425 | let state = self.0; |
| 426 | (state.raw_bounds(), state.direct_transform()) |
| 427 | } |
| 428 | |
| 429 | fn extents(&self, window_bounds: &WindowBounds, coord_type: CoordType) -> Option<Rect> { |
| 430 | let mut bounds = self.0.bounding_box(); |
| 431 | if self.is_root() { |
| 432 | let window_bounds = window_bounds.inner.with_origin(Point::ZERO); |
| 433 | if !window_bounds.is_empty() { |
| 434 | if let Some(bounds) = &mut bounds { |
| 435 | bounds.x0 = bounds.x0.min(window_bounds.x1); |
| 436 | bounds.y0 = bounds.y0.min(window_bounds.y1); |
| 437 | bounds.x1 = bounds.x1.min(window_bounds.x1); |
| 438 | bounds.y1 = bounds.y1.min(window_bounds.y1); |
| 439 | } else { |
| 440 | bounds = Some(window_bounds); |
| 441 | } |
| 442 | } |
| 443 | } |
| 444 | bounds.map(|bounds| { |
| 445 | let new_origin = window_bounds.accesskit_point_to_atspi_point( |
| 446 | bounds.origin(), |
| 447 | self.0.filtered_parent(&filter), |
| 448 | coord_type, |
| 449 | ); |
| 450 | bounds.with_origin(new_origin) |
| 451 | }) |
| 452 | } |
| 453 | |
| 454 | fn current_value(&self) -> Option<f64> { |
| 455 | self.0.numeric_value() |
| 456 | } |
| 457 | |
| 458 | pub(crate) fn notify_changes( |
| 459 | &self, |
| 460 | window_bounds: &WindowBounds, |
| 461 | adapter: &Adapter, |
| 462 | old: &NodeWrapper<'_>, |
| 463 | ) { |
| 464 | self.notify_state_changes(adapter, old); |
| 465 | self.notify_property_changes(adapter, old); |
| 466 | self.notify_bounds_changes(window_bounds, adapter, old); |
| 467 | self.notify_children_changes(adapter, old); |
| 468 | } |
| 469 | |
| 470 | fn notify_state_changes(&self, adapter: &Adapter, old: &NodeWrapper<'_>) { |
| 471 | let old_state = old.state(true); |
| 472 | let new_state = self.state(true); |
| 473 | let changed_states = old_state ^ new_state; |
| 474 | for state in changed_states.iter() { |
| 475 | if state == State::Focused { |
| 476 | // This is handled specially in `focus_moved`. |
| 477 | continue; |
| 478 | } |
| 479 | adapter.emit_object_event( |
| 480 | self.id(), |
| 481 | ObjectEvent::StateChanged(state, new_state.contains(state)), |
| 482 | ); |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | fn notify_property_changes(&self, adapter: &Adapter, old: &NodeWrapper<'_>) { |
| 487 | let name = self.name(); |
| 488 | if name != old.name() { |
| 489 | let name = name.unwrap_or_default(); |
| 490 | adapter.emit_object_event( |
| 491 | self.id(), |
| 492 | ObjectEvent::PropertyChanged(Property::Name(name.clone())), |
| 493 | ); |
| 494 | |
| 495 | let live = self.live(); |
| 496 | if live != AtspiLive::None { |
| 497 | adapter.emit_object_event(self.id(), ObjectEvent::Announcement(name, live)); |
| 498 | } |
| 499 | } |
| 500 | let description = self.description(); |
| 501 | if description != old.description() { |
| 502 | adapter.emit_object_event( |
| 503 | self.id(), |
| 504 | ObjectEvent::PropertyChanged(Property::Description( |
| 505 | description.unwrap_or_default(), |
| 506 | )), |
| 507 | ); |
| 508 | } |
| 509 | let parent_id = self.parent_id(); |
| 510 | if parent_id != old.parent_id() { |
| 511 | let parent = self |
| 512 | .0 |
| 513 | .filtered_parent(&filter) |
| 514 | .map_or(NodeIdOrRoot::Root, |node| NodeIdOrRoot::Node(node.id())); |
| 515 | adapter.emit_object_event( |
| 516 | self.id(), |
| 517 | ObjectEvent::PropertyChanged(Property::Parent(parent)), |
| 518 | ); |
| 519 | } |
| 520 | let role = self.role(); |
| 521 | if role != old.role() { |
| 522 | adapter.emit_object_event( |
| 523 | self.id(), |
| 524 | ObjectEvent::PropertyChanged(Property::Role(role)), |
| 525 | ); |
| 526 | } |
| 527 | if let Some(value) = self.current_value() { |
| 528 | if Some(value) != old.current_value() { |
| 529 | adapter.emit_object_event( |
| 530 | self.id(), |
| 531 | ObjectEvent::PropertyChanged(Property::Value(value)), |
| 532 | ); |
| 533 | } |
| 534 | } |
| 535 | } |
| 536 | |
| 537 | fn notify_bounds_changes( |
| 538 | &self, |
| 539 | window_bounds: &WindowBounds, |
| 540 | adapter: &Adapter, |
| 541 | old: &NodeWrapper<'_>, |
| 542 | ) { |
| 543 | if self.raw_bounds_and_transform() != old.raw_bounds_and_transform() { |
| 544 | if let Some(extents) = self.extents(window_bounds, CoordType::Window) { |
| 545 | adapter.emit_object_event(self.id(), ObjectEvent::BoundsChanged(extents.into())); |
| 546 | } |
| 547 | } |
| 548 | } |
| 549 | |
| 550 | fn notify_children_changes(&self, adapter: &Adapter, old: &NodeWrapper<'_>) { |
| 551 | let old_filtered_children = old.filtered_child_ids().collect::<Vec<NodeId>>(); |
| 552 | let new_filtered_children = self.filtered_child_ids().collect::<Vec<NodeId>>(); |
| 553 | for (index, child) in new_filtered_children.iter().enumerate() { |
| 554 | if !old_filtered_children.contains(child) { |
| 555 | adapter.emit_object_event(self.id(), ObjectEvent::ChildAdded(index, *child)); |
| 556 | } |
| 557 | } |
| 558 | for child in old_filtered_children.into_iter() { |
| 559 | if !new_filtered_children.contains(&child) { |
| 560 | adapter.emit_object_event(self.id(), ObjectEvent::ChildRemoved(child)); |
| 561 | } |
| 562 | } |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | #[derive (Clone)] |
| 567 | pub struct PlatformNode { |
| 568 | context: Weak<Context>, |
| 569 | adapter_id: usize, |
| 570 | id: NodeId, |
| 571 | } |
| 572 | |
| 573 | impl PlatformNode { |
| 574 | pub(crate) fn new(context: &Arc<Context>, adapter_id: usize, id: NodeId) -> Self { |
| 575 | Self { |
| 576 | context: Arc::downgrade(context), |
| 577 | adapter_id, |
| 578 | id, |
| 579 | } |
| 580 | } |
| 581 | |
| 582 | fn from_adapter_root(adapter_id_and_context: &(usize, Arc<Context>)) -> Self { |
| 583 | let (adapter_id, context) = adapter_id_and_context; |
| 584 | Self::new(context, *adapter_id, context.read_tree().state().root_id()) |
| 585 | } |
| 586 | |
| 587 | fn upgrade_context(&self) -> Result<Arc<Context>> { |
| 588 | if let Some(context) = self.context.upgrade() { |
| 589 | Ok(context) |
| 590 | } else { |
| 591 | Err(Error::Defunct) |
| 592 | } |
| 593 | } |
| 594 | |
| 595 | fn with_tree_state<F, T>(&self, f: F) -> Result<T> |
| 596 | where |
| 597 | F: FnOnce(&TreeState) -> Result<T>, |
| 598 | { |
| 599 | let context = self.upgrade_context()?; |
| 600 | let tree = context.read_tree(); |
| 601 | f(tree.state()) |
| 602 | } |
| 603 | |
| 604 | fn with_tree_state_and_context<F, T>(&self, f: F) -> Result<T> |
| 605 | where |
| 606 | F: FnOnce(&TreeState, &Context) -> Result<T>, |
| 607 | { |
| 608 | let context = self.upgrade_context()?; |
| 609 | let tree = context.read_tree(); |
| 610 | f(tree.state(), &context) |
| 611 | } |
| 612 | |
| 613 | fn resolve_with_context<F, T>(&self, f: F) -> Result<T> |
| 614 | where |
| 615 | for<'a> F: FnOnce(Node<'a>, &Context) -> Result<T>, |
| 616 | { |
| 617 | self.with_tree_state_and_context(|state, context| { |
| 618 | if let Some(node) = state.node_by_id(self.id) { |
| 619 | f(node, context) |
| 620 | } else { |
| 621 | Err(Error::Defunct) |
| 622 | } |
| 623 | }) |
| 624 | } |
| 625 | |
| 626 | fn resolve_for_text_with_context<F, T>(&self, f: F) -> Result<T> |
| 627 | where |
| 628 | for<'a> F: FnOnce(Node<'a>, &Context) -> Result<T>, |
| 629 | { |
| 630 | self.resolve_with_context(|node, context| { |
| 631 | let wrapper = NodeWrapper(&node); |
| 632 | if wrapper.supports_text() { |
| 633 | f(node, context) |
| 634 | } else { |
| 635 | Err(Error::UnsupportedInterface) |
| 636 | } |
| 637 | }) |
| 638 | } |
| 639 | |
| 640 | fn resolve<F, T>(&self, f: F) -> Result<T> |
| 641 | where |
| 642 | for<'a> F: FnOnce(Node<'a>) -> Result<T>, |
| 643 | { |
| 644 | self.resolve_with_context(|node, _| f(node)) |
| 645 | } |
| 646 | |
| 647 | fn resolve_for_text<F, T>(&self, f: F) -> Result<T> |
| 648 | where |
| 649 | for<'a> F: FnOnce(Node<'a>) -> Result<T>, |
| 650 | { |
| 651 | self.resolve_for_text_with_context(|node, _| f(node)) |
| 652 | } |
| 653 | |
| 654 | fn do_action_internal<F>(&self, f: F) -> Result<()> |
| 655 | where |
| 656 | F: FnOnce(&TreeState, &Context) -> ActionRequest, |
| 657 | { |
| 658 | let context = self.upgrade_context()?; |
| 659 | let tree = context.read_tree(); |
| 660 | if tree.state().has_node(self.id) { |
| 661 | let request = f(tree.state(), &context); |
| 662 | drop(tree); |
| 663 | context.do_action(request); |
| 664 | Ok(()) |
| 665 | } else { |
| 666 | Err(Error::Defunct) |
| 667 | } |
| 668 | } |
| 669 | |
| 670 | pub fn name(&self) -> Result<String> { |
| 671 | self.resolve(|node| { |
| 672 | let wrapper = NodeWrapper(&node); |
| 673 | Ok(wrapper.name().unwrap_or_default()) |
| 674 | }) |
| 675 | } |
| 676 | |
| 677 | pub fn description(&self) -> Result<String> { |
| 678 | self.resolve(|node| { |
| 679 | let wrapper = NodeWrapper(&node); |
| 680 | Ok(wrapper.description().unwrap_or_default()) |
| 681 | }) |
| 682 | } |
| 683 | |
| 684 | pub fn relative(&self, id: NodeId) -> Self { |
| 685 | Self { |
| 686 | context: self.context.clone(), |
| 687 | adapter_id: self.adapter_id, |
| 688 | id, |
| 689 | } |
| 690 | } |
| 691 | |
| 692 | pub fn root(&self) -> Result<PlatformRoot> { |
| 693 | let context = self.upgrade_context()?; |
| 694 | Ok(PlatformRoot::new(&context.app_context)) |
| 695 | } |
| 696 | |
| 697 | pub fn toolkit_name(&self) -> Result<String> { |
| 698 | self.with_tree_state(|state| Ok(state.toolkit_name().unwrap_or_default())) |
| 699 | } |
| 700 | |
| 701 | pub fn toolkit_version(&self) -> Result<String> { |
| 702 | self.with_tree_state(|state| Ok(state.toolkit_version().unwrap_or_default())) |
| 703 | } |
| 704 | |
| 705 | pub fn parent(&self) -> Result<NodeIdOrRoot> { |
| 706 | self.resolve(|node| { |
| 707 | let parent = node |
| 708 | .filtered_parent(&filter) |
| 709 | .map_or(NodeIdOrRoot::Root, |node| NodeIdOrRoot::Node(node.id())); |
| 710 | Ok(parent) |
| 711 | }) |
| 712 | } |
| 713 | |
| 714 | pub fn child_count(&self) -> Result<i32> { |
| 715 | self.resolve(|node| { |
| 716 | i32::try_from(node.filtered_children(&filter).count()) |
| 717 | .map_err(|_| Error::TooManyChildren) |
| 718 | }) |
| 719 | } |
| 720 | |
| 721 | pub fn adapter_id(&self) -> usize { |
| 722 | self.adapter_id |
| 723 | } |
| 724 | |
| 725 | pub fn id(&self) -> NodeId { |
| 726 | self.id |
| 727 | } |
| 728 | |
| 729 | pub fn accessible_id(&self) -> Result<String> { |
| 730 | self.resolve(|node| { |
| 731 | if let Some(author_id) = node.author_id() { |
| 732 | Ok(author_id.to_string()) |
| 733 | } else { |
| 734 | Ok(String::new()) |
| 735 | } |
| 736 | }) |
| 737 | } |
| 738 | |
| 739 | pub fn child_at_index(&self, index: usize) -> Result<Option<NodeId>> { |
| 740 | self.resolve(|node| { |
| 741 | let child = node |
| 742 | .filtered_children(&filter) |
| 743 | .nth(index) |
| 744 | .map(|child| child.id()); |
| 745 | Ok(child) |
| 746 | }) |
| 747 | } |
| 748 | |
| 749 | pub fn map_children<T, I>(&self, f: impl Fn(NodeId) -> I) -> Result<T> |
| 750 | where |
| 751 | T: FromIterator<I>, |
| 752 | { |
| 753 | self.resolve(|node| { |
| 754 | let children = node |
| 755 | .filtered_children(&filter) |
| 756 | .map(|child| child.id()) |
| 757 | .map(f) |
| 758 | .collect(); |
| 759 | Ok(children) |
| 760 | }) |
| 761 | } |
| 762 | |
| 763 | pub fn index_in_parent(&self) -> Result<i32> { |
| 764 | self.resolve(|node| { |
| 765 | i32::try_from(node.preceding_filtered_siblings(&filter).count()) |
| 766 | .map_err(|_| Error::IndexOutOfRange) |
| 767 | }) |
| 768 | } |
| 769 | |
| 770 | pub fn role(&self) -> Result<AtspiRole> { |
| 771 | self.resolve(|node| { |
| 772 | let wrapper = NodeWrapper(&node); |
| 773 | Ok(wrapper.role()) |
| 774 | }) |
| 775 | } |
| 776 | |
| 777 | pub fn localized_role_name(&self) -> Result<String> { |
| 778 | self.resolve(|node| Ok(node.role_description().unwrap_or_default())) |
| 779 | } |
| 780 | |
| 781 | pub fn state(&self) -> StateSet { |
| 782 | self.resolve_with_context(|node, context| { |
| 783 | let wrapper = NodeWrapper(&node); |
| 784 | Ok(wrapper.state(context.read_tree().state().focus_id().is_some())) |
| 785 | }) |
| 786 | .unwrap_or(State::Defunct.into()) |
| 787 | } |
| 788 | |
| 789 | pub fn attributes(&self) -> Result<HashMap<&'static str, String>> { |
| 790 | self.resolve(|node| { |
| 791 | let wrapper = NodeWrapper(&node); |
| 792 | Ok(wrapper.attributes()) |
| 793 | }) |
| 794 | } |
| 795 | |
| 796 | pub fn supports_action(&self) -> Result<bool> { |
| 797 | self.resolve(|node| { |
| 798 | let wrapper = NodeWrapper(&node); |
| 799 | Ok(wrapper.supports_action()) |
| 800 | }) |
| 801 | } |
| 802 | |
| 803 | pub fn supports_component(&self) -> Result<bool> { |
| 804 | self.resolve(|node| { |
| 805 | let wrapper = NodeWrapper(&node); |
| 806 | Ok(wrapper.supports_component()) |
| 807 | }) |
| 808 | } |
| 809 | |
| 810 | pub fn supports_text(&self) -> Result<bool> { |
| 811 | self.resolve(|node| { |
| 812 | let wrapper = NodeWrapper(&node); |
| 813 | Ok(wrapper.supports_text()) |
| 814 | }) |
| 815 | } |
| 816 | |
| 817 | pub fn supports_value(&self) -> Result<bool> { |
| 818 | self.resolve(|node| { |
| 819 | let wrapper = NodeWrapper(&node); |
| 820 | Ok(wrapper.supports_value()) |
| 821 | }) |
| 822 | } |
| 823 | |
| 824 | pub fn interfaces(&self) -> Result<InterfaceSet> { |
| 825 | self.resolve(|node| { |
| 826 | let wrapper = NodeWrapper(&node); |
| 827 | Ok(wrapper.interfaces()) |
| 828 | }) |
| 829 | } |
| 830 | |
| 831 | pub fn n_actions(&self) -> Result<i32> { |
| 832 | self.resolve(|node| { |
| 833 | let wrapper = NodeWrapper(&node); |
| 834 | Ok(wrapper.n_actions()) |
| 835 | }) |
| 836 | } |
| 837 | |
| 838 | pub fn action_name(&self, index: i32) -> Result<String> { |
| 839 | self.resolve(|node| { |
| 840 | let wrapper = NodeWrapper(&node); |
| 841 | Ok(wrapper.get_action_name(index)) |
| 842 | }) |
| 843 | } |
| 844 | |
| 845 | pub fn actions(&self) -> Result<Vec<AtspiAction>> { |
| 846 | self.resolve(|node| { |
| 847 | let wrapper = NodeWrapper(&node); |
| 848 | let n_actions = wrapper.n_actions() as usize; |
| 849 | let mut actions = Vec::with_capacity(n_actions); |
| 850 | for i in 0..n_actions { |
| 851 | actions.push(AtspiAction { |
| 852 | localized_name: wrapper.get_action_name(i as i32), |
| 853 | description: "" .into(), |
| 854 | key_binding: "" .into(), |
| 855 | }); |
| 856 | } |
| 857 | Ok(actions) |
| 858 | }) |
| 859 | } |
| 860 | |
| 861 | pub fn do_action(&self, index: i32) -> Result<bool> { |
| 862 | if index != 0 { |
| 863 | return Ok(false); |
| 864 | } |
| 865 | self.do_action_internal(|_, _| ActionRequest { |
| 866 | action: Action::Click, |
| 867 | target: self.id, |
| 868 | data: None, |
| 869 | })?; |
| 870 | Ok(true) |
| 871 | } |
| 872 | |
| 873 | pub fn contains(&self, x: i32, y: i32, coord_type: CoordType) -> Result<bool> { |
| 874 | self.resolve_with_context(|node, context| { |
| 875 | let window_bounds = context.read_root_window_bounds(); |
| 876 | let wrapper = NodeWrapper(&node); |
| 877 | if let Some(extents) = wrapper.extents(&window_bounds, coord_type) { |
| 878 | Ok(extents.contains(Point::new(x.into(), y.into()))) |
| 879 | } else { |
| 880 | Ok(false) |
| 881 | } |
| 882 | }) |
| 883 | } |
| 884 | |
| 885 | pub fn accessible_at_point( |
| 886 | &self, |
| 887 | x: i32, |
| 888 | y: i32, |
| 889 | coord_type: CoordType, |
| 890 | ) -> Result<Option<NodeId>> { |
| 891 | self.resolve_with_context(|node, context| { |
| 892 | let window_bounds = context.read_root_window_bounds(); |
| 893 | let point = window_bounds.atspi_point_to_accesskit_point( |
| 894 | Point::new(x.into(), y.into()), |
| 895 | Some(node), |
| 896 | coord_type, |
| 897 | ); |
| 898 | let point = node.transform().inverse() * point; |
| 899 | Ok(node.node_at_point(point, &filter).map(|node| node.id())) |
| 900 | }) |
| 901 | } |
| 902 | |
| 903 | pub fn extents(&self, coord_type: CoordType) -> Result<AtspiRect> { |
| 904 | self.resolve_with_context(|node, context| { |
| 905 | let window_bounds = context.read_root_window_bounds(); |
| 906 | let wrapper = NodeWrapper(&node); |
| 907 | Ok(wrapper |
| 908 | .extents(&window_bounds, coord_type) |
| 909 | .map_or(AtspiRect::INVALID, AtspiRect::from)) |
| 910 | }) |
| 911 | } |
| 912 | |
| 913 | pub fn layer(&self) -> Result<Layer> { |
| 914 | self.resolve(|node| { |
| 915 | let wrapper = NodeWrapper(&node); |
| 916 | if wrapper.role() == AtspiRole::Window { |
| 917 | Ok(Layer::Window) |
| 918 | } else { |
| 919 | Ok(Layer::Widget) |
| 920 | } |
| 921 | }) |
| 922 | } |
| 923 | |
| 924 | pub fn grab_focus(&self) -> Result<bool> { |
| 925 | self.do_action_internal(|_, _| ActionRequest { |
| 926 | action: Action::Focus, |
| 927 | target: self.id, |
| 928 | data: None, |
| 929 | })?; |
| 930 | Ok(true) |
| 931 | } |
| 932 | |
| 933 | pub fn scroll_to_point(&self, coord_type: CoordType, x: i32, y: i32) -> Result<bool> { |
| 934 | self.resolve_with_context(|node, context| { |
| 935 | let window_bounds = context.read_root_window_bounds(); |
| 936 | let point = window_bounds.atspi_point_to_accesskit_point( |
| 937 | Point::new(x.into(), y.into()), |
| 938 | node.filtered_parent(&filter), |
| 939 | coord_type, |
| 940 | ); |
| 941 | context.do_action(ActionRequest { |
| 942 | action: Action::ScrollToPoint, |
| 943 | target: self.id, |
| 944 | data: Some(ActionData::ScrollToPoint(point)), |
| 945 | }); |
| 946 | Ok(()) |
| 947 | })?; |
| 948 | Ok(true) |
| 949 | } |
| 950 | |
| 951 | pub fn character_count(&self) -> Result<i32> { |
| 952 | self.resolve_for_text(|node| { |
| 953 | node.document_range() |
| 954 | .end() |
| 955 | .to_global_usv_index() |
| 956 | .try_into() |
| 957 | .map_err(|_| Error::TooManyCharacters) |
| 958 | }) |
| 959 | } |
| 960 | |
| 961 | pub fn caret_offset(&self) -> Result<i32> { |
| 962 | self.resolve_for_text(|node| { |
| 963 | node.text_selection_focus().map_or(Ok(-1), |focus| { |
| 964 | focus |
| 965 | .to_global_usv_index() |
| 966 | .try_into() |
| 967 | .map_err(|_| Error::TooManyCharacters) |
| 968 | }) |
| 969 | }) |
| 970 | } |
| 971 | |
| 972 | pub fn string_at_offset( |
| 973 | &self, |
| 974 | offset: i32, |
| 975 | granularity: Granularity, |
| 976 | ) -> Result<(String, i32, i32)> { |
| 977 | self.resolve_for_text(|node| { |
| 978 | let range = text_range_from_offset(&node, offset, granularity)?; |
| 979 | let text = range.text(); |
| 980 | let start = range |
| 981 | .start() |
| 982 | .to_global_usv_index() |
| 983 | .try_into() |
| 984 | .map_err(|_| Error::TooManyCharacters)?; |
| 985 | let end = range |
| 986 | .end() |
| 987 | .to_global_usv_index() |
| 988 | .try_into() |
| 989 | .map_err(|_| Error::TooManyCharacters)?; |
| 990 | |
| 991 | Ok((text, start, end)) |
| 992 | }) |
| 993 | } |
| 994 | |
| 995 | pub fn text(&self, start_offset: i32, end_offset: i32) -> Result<String> { |
| 996 | self.resolve_for_text(|node| { |
| 997 | let range = text_range_from_offsets(&node, start_offset, end_offset) |
| 998 | .ok_or(Error::IndexOutOfRange)?; |
| 999 | Ok(range.text()) |
| 1000 | }) |
| 1001 | } |
| 1002 | |
| 1003 | pub fn set_caret_offset(&self, offset: i32) -> Result<bool> { |
| 1004 | self.resolve_for_text_with_context(|node, context| { |
| 1005 | let offset = text_position_from_offset(&node, offset).ok_or(Error::IndexOutOfRange)?; |
| 1006 | context.do_action(ActionRequest { |
| 1007 | action: Action::SetTextSelection, |
| 1008 | target: node.id(), |
| 1009 | data: Some(ActionData::SetTextSelection( |
| 1010 | offset.to_degenerate_range().to_text_selection(), |
| 1011 | )), |
| 1012 | }); |
| 1013 | Ok(true) |
| 1014 | }) |
| 1015 | } |
| 1016 | |
| 1017 | pub fn text_attribute_value(&self, _offset: i32, _attribute_name: &str) -> Result<String> { |
| 1018 | // TODO: Implement rich text. |
| 1019 | Err(Error::UnsupportedInterface) |
| 1020 | } |
| 1021 | |
| 1022 | pub fn text_attributes(&self, _offset: i32) -> Result<(HashMap<String, String>, i32, i32)> { |
| 1023 | // TODO: Implement rich text. |
| 1024 | Err(Error::UnsupportedInterface) |
| 1025 | } |
| 1026 | |
| 1027 | pub fn default_text_attributes(&self) -> Result<HashMap<String, String>> { |
| 1028 | // TODO: Implement rich text. |
| 1029 | Err(Error::UnsupportedInterface) |
| 1030 | } |
| 1031 | |
| 1032 | pub fn character_extents(&self, offset: i32, coord_type: CoordType) -> Result<AtspiRect> { |
| 1033 | self.resolve_for_text_with_context(|node, context| { |
| 1034 | let range = text_range_from_offset(&node, offset, Granularity::Char)?; |
| 1035 | if let Some(bounds) = range.bounding_boxes().first() { |
| 1036 | let window_bounds = context.read_root_window_bounds(); |
| 1037 | let new_origin = window_bounds.accesskit_point_to_atspi_point( |
| 1038 | bounds.origin(), |
| 1039 | Some(node), |
| 1040 | coord_type, |
| 1041 | ); |
| 1042 | Ok(bounds.with_origin(new_origin).into()) |
| 1043 | } else { |
| 1044 | Ok(AtspiRect::INVALID) |
| 1045 | } |
| 1046 | }) |
| 1047 | } |
| 1048 | |
| 1049 | pub fn offset_at_point(&self, x: i32, y: i32, coord_type: CoordType) -> Result<i32> { |
| 1050 | self.resolve_for_text_with_context(|node, context| { |
| 1051 | let window_bounds = context.read_root_window_bounds(); |
| 1052 | let point = window_bounds.atspi_point_to_accesskit_point( |
| 1053 | Point::new(x.into(), y.into()), |
| 1054 | Some(node), |
| 1055 | coord_type, |
| 1056 | ); |
| 1057 | let point = node.transform().inverse() * point; |
| 1058 | node.text_position_at_point(point) |
| 1059 | .to_global_usv_index() |
| 1060 | .try_into() |
| 1061 | .map_err(|_| Error::TooManyCharacters) |
| 1062 | }) |
| 1063 | } |
| 1064 | |
| 1065 | pub fn n_selections(&self) -> Result<i32> { |
| 1066 | self.resolve_for_text(|node| { |
| 1067 | match node.text_selection().filter(|range| !range.is_degenerate()) { |
| 1068 | Some(_) => Ok(1), |
| 1069 | None => Ok(0), |
| 1070 | } |
| 1071 | }) |
| 1072 | } |
| 1073 | |
| 1074 | pub fn selection(&self, selection_num: i32) -> Result<(i32, i32)> { |
| 1075 | if selection_num != 0 { |
| 1076 | return Ok((-1, -1)); |
| 1077 | } |
| 1078 | |
| 1079 | self.resolve_for_text(|node| { |
| 1080 | node.text_selection() |
| 1081 | .filter(|range| !range.is_degenerate()) |
| 1082 | .map_or(Ok((-1, -1)), |range| { |
| 1083 | let start = range |
| 1084 | .start() |
| 1085 | .to_global_usv_index() |
| 1086 | .try_into() |
| 1087 | .map_err(|_| Error::TooManyCharacters)?; |
| 1088 | let end = range |
| 1089 | .end() |
| 1090 | .to_global_usv_index() |
| 1091 | .try_into() |
| 1092 | .map_err(|_| Error::TooManyCharacters)?; |
| 1093 | |
| 1094 | Ok((start, end)) |
| 1095 | }) |
| 1096 | }) |
| 1097 | } |
| 1098 | |
| 1099 | pub fn add_selection(&self, start_offset: i32, end_offset: i32) -> Result<bool> { |
| 1100 | // We only support one selection. |
| 1101 | self.set_selection(0, start_offset, end_offset) |
| 1102 | } |
| 1103 | |
| 1104 | pub fn remove_selection(&self, selection_num: i32) -> Result<bool> { |
| 1105 | if selection_num != 0 { |
| 1106 | return Ok(false); |
| 1107 | } |
| 1108 | |
| 1109 | self.resolve_for_text_with_context(|node, context| { |
| 1110 | // Simply collapse the selection to the position of the caret if a caret is |
| 1111 | // visible, otherwise set the selection to 0. |
| 1112 | let selection_end = node |
| 1113 | .text_selection_focus() |
| 1114 | .unwrap_or_else(|| node.document_range().start()); |
| 1115 | context.do_action(ActionRequest { |
| 1116 | action: Action::SetTextSelection, |
| 1117 | target: node.id(), |
| 1118 | data: Some(ActionData::SetTextSelection( |
| 1119 | selection_end.to_degenerate_range().to_text_selection(), |
| 1120 | )), |
| 1121 | }); |
| 1122 | Ok(true) |
| 1123 | }) |
| 1124 | } |
| 1125 | |
| 1126 | pub fn set_selection( |
| 1127 | &self, |
| 1128 | selection_num: i32, |
| 1129 | start_offset: i32, |
| 1130 | end_offset: i32, |
| 1131 | ) -> Result<bool> { |
| 1132 | if selection_num != 0 { |
| 1133 | return Ok(false); |
| 1134 | } |
| 1135 | |
| 1136 | self.resolve_for_text_with_context(|node, context| { |
| 1137 | let range = text_range_from_offsets(&node, start_offset, end_offset) |
| 1138 | .ok_or(Error::IndexOutOfRange)?; |
| 1139 | context.do_action(ActionRequest { |
| 1140 | action: Action::SetTextSelection, |
| 1141 | target: node.id(), |
| 1142 | data: Some(ActionData::SetTextSelection(range.to_text_selection())), |
| 1143 | }); |
| 1144 | Ok(true) |
| 1145 | }) |
| 1146 | } |
| 1147 | |
| 1148 | pub fn range_extents( |
| 1149 | &self, |
| 1150 | start_offset: i32, |
| 1151 | end_offset: i32, |
| 1152 | coord_type: CoordType, |
| 1153 | ) -> Result<AtspiRect> { |
| 1154 | self.resolve_for_text_with_context(|node, context| { |
| 1155 | if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { |
| 1156 | let window_bounds = context.read_root_window_bounds(); |
| 1157 | let new_origin = window_bounds.accesskit_point_to_atspi_point( |
| 1158 | rect.origin(), |
| 1159 | Some(node), |
| 1160 | coord_type, |
| 1161 | ); |
| 1162 | Ok(rect.with_origin(new_origin).into()) |
| 1163 | } else { |
| 1164 | Ok(AtspiRect::INVALID) |
| 1165 | } |
| 1166 | }) |
| 1167 | } |
| 1168 | |
| 1169 | pub fn text_attribute_run( |
| 1170 | &self, |
| 1171 | _offset: i32, |
| 1172 | _include_defaults: bool, |
| 1173 | ) -> Result<(HashMap<String, String>, i32, i32)> { |
| 1174 | // TODO: Implement rich text. |
| 1175 | // For now, just report a range spanning the entire text with no attributes, |
| 1176 | // this is required by Orca to announce selection content and caret movements. |
| 1177 | let character_count = self.character_count()?; |
| 1178 | Ok((HashMap::new(), 0, character_count)) |
| 1179 | } |
| 1180 | |
| 1181 | pub fn scroll_substring_to( |
| 1182 | &self, |
| 1183 | start_offset: i32, |
| 1184 | end_offset: i32, |
| 1185 | _: ScrollType, |
| 1186 | ) -> Result<bool> { |
| 1187 | self.resolve_for_text_with_context(|node, context| { |
| 1188 | if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { |
| 1189 | context.do_action(ActionRequest { |
| 1190 | action: Action::ScrollIntoView, |
| 1191 | target: node.id(), |
| 1192 | data: Some(ActionData::ScrollTargetRect(rect)), |
| 1193 | }); |
| 1194 | Ok(true) |
| 1195 | } else { |
| 1196 | Ok(false) |
| 1197 | } |
| 1198 | }) |
| 1199 | } |
| 1200 | |
| 1201 | pub fn scroll_substring_to_point( |
| 1202 | &self, |
| 1203 | start_offset: i32, |
| 1204 | end_offset: i32, |
| 1205 | coord_type: CoordType, |
| 1206 | x: i32, |
| 1207 | y: i32, |
| 1208 | ) -> Result<bool> { |
| 1209 | self.resolve_for_text_with_context(|node, context| { |
| 1210 | let window_bounds = context.read_root_window_bounds(); |
| 1211 | let target_point = window_bounds.atspi_point_to_accesskit_point( |
| 1212 | Point::new(x.into(), y.into()), |
| 1213 | Some(node), |
| 1214 | coord_type, |
| 1215 | ); |
| 1216 | |
| 1217 | if let Some(rect) = text_range_bounds_from_offsets(&node, start_offset, end_offset) { |
| 1218 | let point = Point::new(target_point.x - rect.x0, target_point.y - rect.y0); |
| 1219 | context.do_action(ActionRequest { |
| 1220 | action: Action::ScrollToPoint, |
| 1221 | target: node.id(), |
| 1222 | data: Some(ActionData::ScrollToPoint(point)), |
| 1223 | }); |
| 1224 | return Ok(true); |
| 1225 | } |
| 1226 | Ok(false) |
| 1227 | }) |
| 1228 | } |
| 1229 | |
| 1230 | pub fn minimum_value(&self) -> Result<f64> { |
| 1231 | self.resolve(|node| Ok(node.min_numeric_value().unwrap_or(f64::MIN))) |
| 1232 | } |
| 1233 | |
| 1234 | pub fn maximum_value(&self) -> Result<f64> { |
| 1235 | self.resolve(|node| Ok(node.max_numeric_value().unwrap_or(f64::MAX))) |
| 1236 | } |
| 1237 | |
| 1238 | pub fn minimum_increment(&self) -> Result<f64> { |
| 1239 | self.resolve(|node| Ok(node.numeric_value_step().unwrap_or(0.0))) |
| 1240 | } |
| 1241 | |
| 1242 | pub fn current_value(&self) -> Result<f64> { |
| 1243 | self.resolve(|node| { |
| 1244 | let wrapper = NodeWrapper(&node); |
| 1245 | Ok(wrapper.current_value().unwrap_or(0.0)) |
| 1246 | }) |
| 1247 | } |
| 1248 | |
| 1249 | pub fn set_current_value(&self, value: f64) -> Result<()> { |
| 1250 | self.do_action_internal(|_, _| ActionRequest { |
| 1251 | action: Action::SetValue, |
| 1252 | target: self.id, |
| 1253 | data: Some(ActionData::NumericValue(value)), |
| 1254 | }) |
| 1255 | } |
| 1256 | } |
| 1257 | |
| 1258 | impl PartialEq for PlatformNode { |
| 1259 | fn eq(&self, other: &Self) -> bool { |
| 1260 | self.adapter_id == other.adapter_id && self.id == other.id |
| 1261 | } |
| 1262 | } |
| 1263 | |
| 1264 | impl Eq for PlatformNode {} |
| 1265 | |
| 1266 | impl Hash for PlatformNode { |
| 1267 | fn hash<H: Hasher>(&self, state: &mut H) { |
| 1268 | self.adapter_id.hash(state); |
| 1269 | self.id.hash(state); |
| 1270 | } |
| 1271 | } |
| 1272 | |
| 1273 | #[derive (Clone)] |
| 1274 | pub struct PlatformRoot { |
| 1275 | app_context: Weak<RwLock<AppContext>>, |
| 1276 | } |
| 1277 | |
| 1278 | impl PlatformRoot { |
| 1279 | pub fn new(app_context: &Arc<RwLock<AppContext>>) -> Self { |
| 1280 | Self { |
| 1281 | app_context: Arc::downgrade(app_context), |
| 1282 | } |
| 1283 | } |
| 1284 | |
| 1285 | fn resolve_app_context<F, T>(&self, f: F) -> Result<T> |
| 1286 | where |
| 1287 | for<'a> F: FnOnce(RwLockReadGuard<'a, AppContext>) -> Result<T>, |
| 1288 | { |
| 1289 | let app_context = match self.app_context.upgrade() { |
| 1290 | Some(context) => context, |
| 1291 | None => return Err(Error::Defunct), |
| 1292 | }; |
| 1293 | let app_context = app_context.read().unwrap(); |
| 1294 | f(app_context) |
| 1295 | } |
| 1296 | |
| 1297 | pub fn name(&self) -> Result<String> { |
| 1298 | self.resolve_app_context(|context| Ok(context.name.clone().unwrap_or_default())) |
| 1299 | } |
| 1300 | |
| 1301 | pub fn child_count(&self) -> Result<i32> { |
| 1302 | self.resolve_app_context(|context| { |
| 1303 | i32::try_from(context.adapters.len()).map_err(|_| Error::TooManyChildren) |
| 1304 | }) |
| 1305 | } |
| 1306 | |
| 1307 | pub fn child_at_index(&self, index: usize) -> Result<Option<PlatformNode>> { |
| 1308 | self.resolve_app_context(|context| { |
| 1309 | let child = context |
| 1310 | .adapters |
| 1311 | .get(index) |
| 1312 | .map(PlatformNode::from_adapter_root); |
| 1313 | Ok(child) |
| 1314 | }) |
| 1315 | } |
| 1316 | |
| 1317 | pub fn child_id_at_index(&self, index: usize) -> Result<Option<(usize, NodeId)>> { |
| 1318 | self.resolve_app_context(|context| { |
| 1319 | let child = context |
| 1320 | .adapters |
| 1321 | .get(index) |
| 1322 | .map(|(adapter_id, context)| (*adapter_id, context.read_tree().state().root_id())); |
| 1323 | Ok(child) |
| 1324 | }) |
| 1325 | } |
| 1326 | |
| 1327 | pub fn map_children<T, I>(&self, f: impl Fn(PlatformNode) -> I) -> Result<T> |
| 1328 | where |
| 1329 | T: FromIterator<I>, |
| 1330 | { |
| 1331 | self.resolve_app_context(|context| { |
| 1332 | let children = context |
| 1333 | .adapters |
| 1334 | .iter() |
| 1335 | .map(PlatformNode::from_adapter_root) |
| 1336 | .map(f) |
| 1337 | .collect(); |
| 1338 | Ok(children) |
| 1339 | }) |
| 1340 | } |
| 1341 | |
| 1342 | pub fn map_child_ids<T, I>(&self, f: impl Fn((usize, NodeId)) -> I) -> Result<T> |
| 1343 | where |
| 1344 | T: FromIterator<I>, |
| 1345 | { |
| 1346 | self.resolve_app_context(|context| { |
| 1347 | let children = context |
| 1348 | .adapters |
| 1349 | .iter() |
| 1350 | .map(|(adapter_id, context)| (*adapter_id, context.read_tree().state().root_id())) |
| 1351 | .map(f) |
| 1352 | .collect(); |
| 1353 | Ok(children) |
| 1354 | }) |
| 1355 | } |
| 1356 | |
| 1357 | pub fn toolkit_name(&self) -> Result<String> { |
| 1358 | self.resolve_app_context(|context| Ok(context.toolkit_name.clone().unwrap_or_default())) |
| 1359 | } |
| 1360 | |
| 1361 | pub fn toolkit_version(&self) -> Result<String> { |
| 1362 | self.resolve_app_context(|context| Ok(context.toolkit_version.clone().unwrap_or_default())) |
| 1363 | } |
| 1364 | |
| 1365 | pub fn id(&self) -> Result<i32> { |
| 1366 | self.resolve_app_context(|context| Ok(context.id.unwrap_or(-1))) |
| 1367 | } |
| 1368 | |
| 1369 | pub fn set_id(&mut self, id: i32) -> Result<()> { |
| 1370 | let app_context = match self.app_context.upgrade() { |
| 1371 | Some(context) => context, |
| 1372 | None => return Err(Error::Defunct), |
| 1373 | }; |
| 1374 | let mut app_context = app_context.write().unwrap(); |
| 1375 | app_context.id = Some(id); |
| 1376 | Ok(()) |
| 1377 | } |
| 1378 | } |
| 1379 | |
| 1380 | impl PartialEq for PlatformRoot { |
| 1381 | fn eq(&self, other: &Self) -> bool { |
| 1382 | self.app_context.ptr_eq(&other.app_context) |
| 1383 | } |
| 1384 | } |
| 1385 | |
| 1386 | impl Hash for PlatformRoot { |
| 1387 | fn hash<H: Hasher>(&self, state: &mut H) { |
| 1388 | self.app_context.as_ptr().hash(state); |
| 1389 | } |
| 1390 | } |
| 1391 | |
| 1392 | #[derive (Clone, Copy, Debug, PartialEq, Eq)] |
| 1393 | pub enum NodeIdOrRoot { |
| 1394 | Node(NodeId), |
| 1395 | Root, |
| 1396 | } |
| 1397 | |