| 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::{ActionHandler, NodeId, Role, TreeUpdate}; |
| 12 | use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler, TreeState}; |
| 13 | use atspi_common::{InterfaceSet, Live, State}; |
| 14 | use std::{ |
| 15 | collections::HashSet, |
| 16 | sync::{ |
| 17 | atomic::{AtomicUsize, Ordering}, |
| 18 | Arc, RwLock, |
| 19 | }, |
| 20 | }; |
| 21 | |
| 22 | use crate::{ |
| 23 | context::{ActionHandlerNoMut, ActionHandlerWrapper, AppContext, Context}, |
| 24 | filters::filter, |
| 25 | node::{NodeIdOrRoot, NodeWrapper, PlatformNode, PlatformRoot}, |
| 26 | util::WindowBounds, |
| 27 | AdapterCallback, Event, ObjectEvent, WindowEvent, |
| 28 | }; |
| 29 | |
| 30 | struct AdapterChangeHandler<'a> { |
| 31 | adapter: &'a Adapter, |
| 32 | added_nodes: HashSet<NodeId>, |
| 33 | removed_nodes: HashSet<NodeId>, |
| 34 | checked_text_change: HashSet<NodeId>, |
| 35 | } |
| 36 | |
| 37 | impl<'a> AdapterChangeHandler<'a> { |
| 38 | fn new(adapter: &'a Adapter) -> Self { |
| 39 | Self { |
| 40 | adapter, |
| 41 | added_nodes: HashSet::new(), |
| 42 | removed_nodes: HashSet::new(), |
| 43 | checked_text_change: HashSet::new(), |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | fn add_node(&mut self, node: &Node) { |
| 48 | let id = node.id(); |
| 49 | if self.added_nodes.contains(&id) { |
| 50 | return; |
| 51 | } |
| 52 | self.added_nodes.insert(id); |
| 53 | |
| 54 | let role = node.role(); |
| 55 | let is_root = node.is_root(); |
| 56 | let node = NodeWrapper(node); |
| 57 | let interfaces = node.interfaces(); |
| 58 | self.adapter.register_interfaces(node.id(), interfaces); |
| 59 | if is_root && role == Role::Window { |
| 60 | let adapter_index = self |
| 61 | .adapter |
| 62 | .context |
| 63 | .read_app_context() |
| 64 | .adapter_index(self.adapter.id) |
| 65 | .unwrap(); |
| 66 | self.adapter.window_created(adapter_index, node.id()); |
| 67 | } |
| 68 | |
| 69 | let live = node.live(); |
| 70 | if live != Live::None { |
| 71 | if let Some(name) = node.name() { |
| 72 | self.adapter |
| 73 | .emit_object_event(node.id(), ObjectEvent::Announcement(name, live)); |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | fn add_subtree(&mut self, node: &Node) { |
| 79 | self.add_node(node); |
| 80 | for child in node.filtered_children(&filter) { |
| 81 | self.add_subtree(&child); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | fn remove_node(&mut self, node: &Node) { |
| 86 | let id = node.id(); |
| 87 | if self.removed_nodes.contains(&id) { |
| 88 | return; |
| 89 | } |
| 90 | self.removed_nodes.insert(id); |
| 91 | |
| 92 | let role = node.role(); |
| 93 | let is_root = node.is_root(); |
| 94 | let node = NodeWrapper(node); |
| 95 | if is_root && role == Role::Window { |
| 96 | self.adapter.window_destroyed(node.id()); |
| 97 | } |
| 98 | self.adapter |
| 99 | .emit_object_event(node.id(), ObjectEvent::StateChanged(State::Defunct, true)); |
| 100 | self.adapter |
| 101 | .unregister_interfaces(node.id(), node.interfaces()); |
| 102 | } |
| 103 | |
| 104 | fn remove_subtree(&mut self, node: &Node) { |
| 105 | for child in node.filtered_children(&filter) { |
| 106 | self.remove_subtree(&child); |
| 107 | } |
| 108 | self.remove_node(node); |
| 109 | } |
| 110 | |
| 111 | fn emit_text_change_if_needed_parent(&mut self, old_node: &Node, new_node: &Node) { |
| 112 | if !new_node.supports_text_ranges() || !old_node.supports_text_ranges() { |
| 113 | return; |
| 114 | } |
| 115 | let id = new_node.id(); |
| 116 | if self.checked_text_change.contains(&id) { |
| 117 | return; |
| 118 | } |
| 119 | self.checked_text_change.insert(id); |
| 120 | let old_text = old_node.document_range().text(); |
| 121 | let new_text = new_node.document_range().text(); |
| 122 | |
| 123 | let mut old_chars = old_text.chars(); |
| 124 | let mut new_chars = new_text.chars(); |
| 125 | let mut prefix_usv_count = 0; |
| 126 | let mut prefix_byte_count = 0; |
| 127 | loop { |
| 128 | match (old_chars.next(), new_chars.next()) { |
| 129 | (Some(old_char), Some(new_char)) if old_char == new_char => { |
| 130 | prefix_usv_count += 1; |
| 131 | prefix_byte_count += new_char.len_utf8(); |
| 132 | } |
| 133 | (None, None) => return, |
| 134 | _ => break, |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | let suffix_byte_count = old_text[prefix_byte_count..] |
| 139 | .chars() |
| 140 | .rev() |
| 141 | .zip(new_text[prefix_byte_count..].chars().rev()) |
| 142 | .take_while(|(old_char, new_char)| old_char == new_char) |
| 143 | .fold(0, |count, (c, _)| count + c.len_utf8()); |
| 144 | |
| 145 | let old_content = &old_text[prefix_byte_count..old_text.len() - suffix_byte_count]; |
| 146 | if let Ok(length) = old_content.chars().count().try_into() { |
| 147 | if length > 0 { |
| 148 | self.adapter.emit_object_event( |
| 149 | id, |
| 150 | ObjectEvent::TextRemoved { |
| 151 | start_index: prefix_usv_count, |
| 152 | length, |
| 153 | content: old_content.to_string(), |
| 154 | }, |
| 155 | ); |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | let new_content = &new_text[prefix_byte_count..new_text.len() - suffix_byte_count]; |
| 160 | if let Ok(length) = new_content.chars().count().try_into() { |
| 161 | if length > 0 { |
| 162 | self.adapter.emit_object_event( |
| 163 | id, |
| 164 | ObjectEvent::TextInserted { |
| 165 | start_index: prefix_usv_count, |
| 166 | length, |
| 167 | content: new_content.to_string(), |
| 168 | }, |
| 169 | ); |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | fn emit_text_change_if_needed(&mut self, old_node: &Node, new_node: &Node) { |
| 175 | if let Role::TextRun | Role::GenericContainer = new_node.role() { |
| 176 | if let (Some(old_parent), Some(new_parent)) = ( |
| 177 | old_node.filtered_parent(&filter), |
| 178 | new_node.filtered_parent(&filter), |
| 179 | ) { |
| 180 | self.emit_text_change_if_needed_parent(&old_parent, &new_parent); |
| 181 | } |
| 182 | } else { |
| 183 | self.emit_text_change_if_needed_parent(old_node, new_node); |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | fn emit_text_selection_change(&self, old_node: Option<&Node>, new_node: &Node) { |
| 188 | if !new_node.supports_text_ranges() { |
| 189 | return; |
| 190 | } |
| 191 | let Some(old_node) = old_node else { |
| 192 | if let Some(selection) = new_node.text_selection() { |
| 193 | if !selection.is_degenerate() { |
| 194 | self.adapter |
| 195 | .emit_object_event(new_node.id(), ObjectEvent::TextSelectionChanged); |
| 196 | } |
| 197 | } |
| 198 | if let Some(selection_focus) = new_node.text_selection_focus() { |
| 199 | if let Ok(offset) = selection_focus.to_global_usv_index().try_into() { |
| 200 | self.adapter |
| 201 | .emit_object_event(new_node.id(), ObjectEvent::CaretMoved(offset)); |
| 202 | } |
| 203 | } |
| 204 | return; |
| 205 | }; |
| 206 | if !old_node.is_focused() || new_node.raw_text_selection() == old_node.raw_text_selection() |
| 207 | { |
| 208 | return; |
| 209 | } |
| 210 | |
| 211 | if let Some(selection) = new_node.text_selection() { |
| 212 | if !selection.is_degenerate() |
| 213 | || old_node |
| 214 | .text_selection() |
| 215 | .map(|selection| !selection.is_degenerate()) |
| 216 | .unwrap_or(false) |
| 217 | { |
| 218 | self.adapter |
| 219 | .emit_object_event(new_node.id(), ObjectEvent::TextSelectionChanged); |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | let old_caret_position = old_node |
| 224 | .raw_text_selection() |
| 225 | .map(|selection| selection.focus); |
| 226 | let new_caret_position = new_node |
| 227 | .raw_text_selection() |
| 228 | .map(|selection| selection.focus); |
| 229 | if old_caret_position != new_caret_position { |
| 230 | if let Some(selection_focus) = new_node.text_selection_focus() { |
| 231 | if let Ok(offset) = selection_focus.to_global_usv_index().try_into() { |
| 232 | self.adapter |
| 233 | .emit_object_event(new_node.id(), ObjectEvent::CaretMoved(offset)); |
| 234 | } |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | impl TreeChangeHandler for AdapterChangeHandler<'_> { |
| 241 | fn node_added(&mut self, node: &Node) { |
| 242 | if filter(node) == FilterResult::Include { |
| 243 | self.add_node(node); |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | fn node_updated(&mut self, old_node: &Node, new_node: &Node) { |
| 248 | self.emit_text_change_if_needed(old_node, new_node); |
| 249 | let filter_old = filter(old_node); |
| 250 | let filter_new = filter(new_node); |
| 251 | if filter_new != filter_old { |
| 252 | if filter_new == FilterResult::Include { |
| 253 | if filter_old == FilterResult::ExcludeSubtree { |
| 254 | self.add_subtree(new_node); |
| 255 | } else { |
| 256 | self.add_node(new_node); |
| 257 | } |
| 258 | } else if filter_old == FilterResult::Include { |
| 259 | if filter_new == FilterResult::ExcludeSubtree { |
| 260 | self.remove_subtree(old_node); |
| 261 | } else { |
| 262 | self.remove_node(old_node); |
| 263 | } |
| 264 | } |
| 265 | } else if filter_new == FilterResult::Include { |
| 266 | let old_wrapper = NodeWrapper(old_node); |
| 267 | let new_wrapper = NodeWrapper(new_node); |
| 268 | let old_interfaces = old_wrapper.interfaces(); |
| 269 | let new_interfaces = new_wrapper.interfaces(); |
| 270 | let kept_interfaces = old_interfaces & new_interfaces; |
| 271 | self.adapter |
| 272 | .unregister_interfaces(new_wrapper.id(), old_interfaces ^ kept_interfaces); |
| 273 | self.adapter |
| 274 | .register_interfaces(new_node.id(), new_interfaces ^ kept_interfaces); |
| 275 | let bounds = *self.adapter.context.read_root_window_bounds(); |
| 276 | new_wrapper.notify_changes(&bounds, self.adapter, &old_wrapper); |
| 277 | self.emit_text_selection_change(Some(old_node), new_node); |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | fn focus_moved(&mut self, old_node: Option<&Node>, new_node: Option<&Node>) { |
| 282 | if let (None, Some(new_node)) = (old_node, new_node) { |
| 283 | if let Some(root_window) = root_window(new_node.tree_state) { |
| 284 | self.adapter.window_activated(&NodeWrapper(&root_window)); |
| 285 | } |
| 286 | } else if let (Some(old_node), None) = (old_node, new_node) { |
| 287 | if let Some(root_window) = root_window(old_node.tree_state) { |
| 288 | self.adapter.window_deactivated(&NodeWrapper(&root_window)); |
| 289 | } |
| 290 | } |
| 291 | if let Some(node) = new_node { |
| 292 | self.adapter |
| 293 | .emit_object_event(node.id(), ObjectEvent::StateChanged(State::Focused, true)); |
| 294 | self.emit_text_selection_change(None, node); |
| 295 | } |
| 296 | if let Some(node) = old_node { |
| 297 | self.adapter |
| 298 | .emit_object_event(node.id(), ObjectEvent::StateChanged(State::Focused, false)); |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | fn node_removed(&mut self, node: &Node) { |
| 303 | if filter(node) == FilterResult::Include { |
| 304 | self.remove_node(node); |
| 305 | } |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | static NEXT_ADAPTER_ID: AtomicUsize = AtomicUsize::new(0); |
| 310 | |
| 311 | /// If you use this function, you must ensure that only one adapter at a time |
| 312 | /// has a given ID. |
| 313 | pub fn next_adapter_id() -> usize { |
| 314 | NEXT_ADAPTER_ID.fetch_add(val:1, order:Ordering::Relaxed) |
| 315 | } |
| 316 | |
| 317 | pub struct Adapter { |
| 318 | id: usize, |
| 319 | callback: Box<dyn AdapterCallback + Send + Sync>, |
| 320 | context: Arc<Context>, |
| 321 | } |
| 322 | |
| 323 | impl Adapter { |
| 324 | pub fn new( |
| 325 | app_context: &Arc<RwLock<AppContext>>, |
| 326 | callback: impl 'static + AdapterCallback + Send + Sync, |
| 327 | initial_state: TreeUpdate, |
| 328 | is_window_focused: bool, |
| 329 | root_window_bounds: WindowBounds, |
| 330 | action_handler: impl 'static + ActionHandler + Send, |
| 331 | ) -> Self { |
| 332 | let id = next_adapter_id(); |
| 333 | Self::with_id( |
| 334 | id, |
| 335 | app_context, |
| 336 | callback, |
| 337 | initial_state, |
| 338 | is_window_focused, |
| 339 | root_window_bounds, |
| 340 | action_handler, |
| 341 | ) |
| 342 | } |
| 343 | |
| 344 | pub fn with_id( |
| 345 | id: usize, |
| 346 | app_context: &Arc<RwLock<AppContext>>, |
| 347 | callback: impl 'static + AdapterCallback + Send + Sync, |
| 348 | initial_state: TreeUpdate, |
| 349 | is_window_focused: bool, |
| 350 | root_window_bounds: WindowBounds, |
| 351 | action_handler: impl 'static + ActionHandler + Send, |
| 352 | ) -> Self { |
| 353 | Self::with_wrapped_action_handler( |
| 354 | id, |
| 355 | app_context, |
| 356 | callback, |
| 357 | initial_state, |
| 358 | is_window_focused, |
| 359 | root_window_bounds, |
| 360 | Arc::new(ActionHandlerWrapper::new(action_handler)), |
| 361 | ) |
| 362 | } |
| 363 | |
| 364 | /// This is an implementation detail of `accesskit_unix`, required for |
| 365 | /// robust state transitions with minimal overhead. |
| 366 | pub fn with_wrapped_action_handler( |
| 367 | id: usize, |
| 368 | app_context: &Arc<RwLock<AppContext>>, |
| 369 | callback: impl 'static + AdapterCallback + Send + Sync, |
| 370 | initial_state: TreeUpdate, |
| 371 | is_window_focused: bool, |
| 372 | root_window_bounds: WindowBounds, |
| 373 | action_handler: Arc<dyn ActionHandlerNoMut + Send + Sync>, |
| 374 | ) -> Self { |
| 375 | let tree = Tree::new(initial_state, is_window_focused); |
| 376 | let focus_id = tree.state().focus_id(); |
| 377 | let context = Context::new(app_context, tree, action_handler, root_window_bounds); |
| 378 | context.write_app_context().push_adapter(id, &context); |
| 379 | let adapter = Self { |
| 380 | id, |
| 381 | callback: Box::new(callback), |
| 382 | context, |
| 383 | }; |
| 384 | adapter.register_tree(); |
| 385 | if let Some(id) = focus_id { |
| 386 | adapter.emit_object_event(id, ObjectEvent::StateChanged(State::Focused, true)); |
| 387 | } |
| 388 | adapter |
| 389 | } |
| 390 | |
| 391 | fn register_tree(&self) { |
| 392 | fn add_children(node: Node<'_>, to_add: &mut Vec<(NodeId, InterfaceSet)>) { |
| 393 | for child in node.filtered_children(&filter) { |
| 394 | let child_id = child.id(); |
| 395 | let wrapper = NodeWrapper(&child); |
| 396 | let interfaces = wrapper.interfaces(); |
| 397 | to_add.push((child_id, interfaces)); |
| 398 | add_children(child, to_add); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | let mut objects_to_add = Vec::new(); |
| 403 | |
| 404 | let (adapter_index, root_id) = { |
| 405 | let tree = self.context.read_tree(); |
| 406 | let tree_state = tree.state(); |
| 407 | let mut app_context = self.context.write_app_context(); |
| 408 | app_context.name = tree_state.app_name(); |
| 409 | app_context.toolkit_name = tree_state.toolkit_name(); |
| 410 | app_context.toolkit_version = tree_state.toolkit_version(); |
| 411 | let adapter_index = app_context.adapter_index(self.id).unwrap(); |
| 412 | let root = tree_state.root(); |
| 413 | let root_id = root.id(); |
| 414 | let wrapper = NodeWrapper(&root); |
| 415 | objects_to_add.push((root_id, wrapper.interfaces())); |
| 416 | add_children(root, &mut objects_to_add); |
| 417 | (adapter_index, root_id) |
| 418 | }; |
| 419 | |
| 420 | for (id, interfaces) in objects_to_add { |
| 421 | self.register_interfaces(id, interfaces); |
| 422 | if id == root_id { |
| 423 | self.window_created(adapter_index, id); |
| 424 | } |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | pub fn platform_node(&self, id: NodeId) -> PlatformNode { |
| 429 | PlatformNode::new(&self.context, self.id, id) |
| 430 | } |
| 431 | |
| 432 | pub fn root_id(&self) -> NodeId { |
| 433 | self.context.read_tree().state().root_id() |
| 434 | } |
| 435 | |
| 436 | pub fn platform_root(&self) -> PlatformRoot { |
| 437 | PlatformRoot::new(&self.context.app_context) |
| 438 | } |
| 439 | |
| 440 | fn register_interfaces(&self, id: NodeId, new_interfaces: InterfaceSet) { |
| 441 | self.callback.register_interfaces(self, id, new_interfaces); |
| 442 | } |
| 443 | |
| 444 | fn unregister_interfaces(&self, id: NodeId, old_interfaces: InterfaceSet) { |
| 445 | self.callback |
| 446 | .unregister_interfaces(self, id, old_interfaces); |
| 447 | } |
| 448 | |
| 449 | pub(crate) fn emit_object_event(&self, target: NodeId, event: ObjectEvent) { |
| 450 | let target = NodeIdOrRoot::Node(target); |
| 451 | self.callback |
| 452 | .emit_event(self, Event::Object { target, event }); |
| 453 | } |
| 454 | |
| 455 | fn emit_root_object_event(&self, event: ObjectEvent) { |
| 456 | let target = NodeIdOrRoot::Root; |
| 457 | self.callback |
| 458 | .emit_event(self, Event::Object { target, event }); |
| 459 | } |
| 460 | |
| 461 | pub fn set_root_window_bounds(&mut self, new_bounds: WindowBounds) { |
| 462 | let mut bounds = self.context.root_window_bounds.write().unwrap(); |
| 463 | *bounds = new_bounds; |
| 464 | } |
| 465 | |
| 466 | pub fn update(&mut self, update: TreeUpdate) { |
| 467 | let mut handler = AdapterChangeHandler::new(self); |
| 468 | let mut tree = self.context.tree.write().unwrap(); |
| 469 | tree.update_and_process_changes(update, &mut handler); |
| 470 | } |
| 471 | |
| 472 | pub fn update_window_focus_state(&mut self, is_focused: bool) { |
| 473 | let mut handler = AdapterChangeHandler::new(self); |
| 474 | let mut tree = self.context.tree.write().unwrap(); |
| 475 | tree.update_host_focus_state_and_process_changes(is_focused, &mut handler); |
| 476 | } |
| 477 | |
| 478 | fn window_created(&self, adapter_index: usize, window: NodeId) { |
| 479 | self.emit_root_object_event(ObjectEvent::ChildAdded(adapter_index, window)); |
| 480 | } |
| 481 | |
| 482 | fn window_activated(&self, window: &NodeWrapper<'_>) { |
| 483 | self.callback.emit_event( |
| 484 | self, |
| 485 | Event::Window { |
| 486 | target: window.id(), |
| 487 | name: window.name().unwrap_or_default(), |
| 488 | event: WindowEvent::Activated, |
| 489 | }, |
| 490 | ); |
| 491 | self.emit_object_event(window.id(), ObjectEvent::StateChanged(State::Active, true)); |
| 492 | self.emit_root_object_event(ObjectEvent::ActiveDescendantChanged(window.id())); |
| 493 | } |
| 494 | |
| 495 | fn window_deactivated(&self, window: &NodeWrapper<'_>) { |
| 496 | self.callback.emit_event( |
| 497 | self, |
| 498 | Event::Window { |
| 499 | target: window.id(), |
| 500 | name: window.name().unwrap_or_default(), |
| 501 | event: WindowEvent::Deactivated, |
| 502 | }, |
| 503 | ); |
| 504 | self.emit_object_event(window.id(), ObjectEvent::StateChanged(State::Active, false)); |
| 505 | } |
| 506 | |
| 507 | fn window_destroyed(&self, window: NodeId) { |
| 508 | self.emit_root_object_event(ObjectEvent::ChildRemoved(window)); |
| 509 | } |
| 510 | |
| 511 | pub fn id(&self) -> usize { |
| 512 | self.id |
| 513 | } |
| 514 | |
| 515 | pub fn is_window_focused(&self) -> bool { |
| 516 | self.context.read_tree().state().is_host_focused() |
| 517 | } |
| 518 | |
| 519 | pub fn root_window_bounds(&self) -> WindowBounds { |
| 520 | *self.context.read_root_window_bounds() |
| 521 | } |
| 522 | |
| 523 | /// This is an implementation detail of `accesskit_unix`, required for |
| 524 | /// robust state transitions with minimal overhead. |
| 525 | pub fn wrapped_action_handler(&self) -> Arc<dyn ActionHandlerNoMut + Send + Sync> { |
| 526 | Arc::clone(&self.context.action_handler) |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | fn root_window(current_state: &TreeState) -> Option<Node> { |
| 531 | const WINDOW_ROLES: &[Role] = &[Role::AlertDialog, Role::Dialog, Role::Window]; |
| 532 | let root: Node<'_> = current_state.root(); |
| 533 | if WINDOW_ROLES.contains(&root.role()) { |
| 534 | Some(root) |
| 535 | } else { |
| 536 | None |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | impl Drop for Adapter { |
| 541 | fn drop(&mut self) { |
| 542 | let root_id: NodeId = self.context.read_tree().state().root_id(); |
| 543 | self.window_destroyed(window:root_id); |
| 544 | // Note: We deliberately do the following here, not in a Drop |
| 545 | // implementation on context, because AppContext owns a second |
| 546 | // strong reference to Context, and we need that to be released. |
| 547 | self.context.write_app_context().remove_adapter(self.id); |
| 548 | } |
| 549 | } |
| 550 | |