1''' 2Defines behavior of the accessible tree view widget. 3 4@author: Peter Parente 5@author: Eitan Isaacson 6@organization: IBM Corporation 7@copyright: Copyright (c) 2006, 2007 IBM Corporation 8@license: BSD 9 10All rights reserved. This program and the accompanying materials are made 11available under the terms of the BSD which accompanies this distribution, and 12is available at U{http://www.opensource.org/licenses/bsd-license.php} 13''' 14 15from gi.repository import Gtk as gtk 16from gi.repository import Gdk as gdk 17from gi.repository import GdkPixbuf 18from gi.repository import GLib 19from gi.repository import GObject 20 21import pyatspi 22import os 23from . import ui_manager 24from time import sleep 25from .icons import getIcon 26from .node import Node 27from .tools import ToolsAccessor, getTreePathBoundingBox 28from .i18n import _ 29 30COL_ICON = 0 31COL_NAME = 1 32COL_ROLE = 2 33COL_CHILDCOUNT = 3 34COL_FILLED = 4 35COL_DUMMY = 5 36COL_ACC = 6 37 38class AccessibleModel(gtk.TreeStore, ToolsAccessor): 39 ''' 40 Stores the desktop accessible tree. Only populates sections of the tree 41 that are being viewed. This cuts short on a lot of potential overhead. 42 43 @ivar desktop: The desktop accessible. It holds references to all the 44 application L{Accessibility.Accessible}s 45 @type desktop: L{Accessibility.Accessible} 46 @ivar acc_cache: A list of L{Accessibility.Accessible}s that are currently 47 resident in the model. This helps with faster searching. 48 @type acc_cache: list 49 ''' 50 __gsignals__ = {'row-filled' : 51 (GObject.SignalFlags.RUN_FIRST, 52 None, 53 (GObject.TYPE_PYOBJECT,)), 54 'start-populating' : 55 (GObject.SignalFlags.RUN_FIRST, 56 None, 57 ()), 58 'end-populating' : 59 (GObject.SignalFlags.RUN_FIRST, 60 None, 61 ())} 62 63 def __init__(self, desktop_acc): 64 ''' 65 Initializes the L{AccessibleModel} and all of the needed instant variables. 66 Connects required signals. 67 ''' 68 self.acc_cache = [desktop_acc] 69 gtk.TreeStore.__init__(self, GdkPixbuf.Pixbuf, str, str, int, bool, bool, object) 70 self.connect('row-changed', self._onRowChanged) 71 self.connect('row-filled', self._onRowFilled) 72 self.desktop = desktop_acc 73 self._path_to_populate = None 74 self._populating_tasks = 0 75 76 def _onRowChanged(self, model, path, iter): 77 ''' 78 A callback on "row-changed" that pre-populates a given row when it changes. 79 80 @param model: This model. 81 @type model: L{AccessibleModel} 82 @param path: The path to the row that changed. 83 @type path: tuple 84 @param iter: the iter of the row that changed. 85 @type iter: L{gtk.TreeIter} 86 ''' 87 self._prepopLevel(model[iter][COL_ACC], iter) 88 89 def insert(self, iter, index, row): 90 ''' 91 A method that overrides the L{gtk.TreeStore} insert method. 92 Pre-populate and add the new accessible to acc_cache. 93 94 @param iter: The parent iter of the newly inserted row. 95 @type iter: L{gtk.TreeIter} 96 @param index: The place in which to insert the new row. 97 @type index: integer 98 @param row: A list of columns o insert into the row, 99 @type row: list 100 101 @return: The newly created iter of the inserted row. 102 @rtype: L{gtk.TreeIter} 103 ''' 104 acc = row[COL_ACC] 105 if acc: 106 self.acc_cache.append(acc) 107 new_iter = gtk.TreeStore.insert(self, iter, index, row) 108 self._prepopLevel(acc, new_iter) 109 110 def append(self, iter, row): 111 ''' 112 A method that overrides the L{gtk.TreeStore} append method. 113 Pre-populate and add the new accessible to acc_cache. 114 115 @param iter: The parent iter of the newly inseted row. 116 @type iter: L{gtk.TreeIter} 117 @param row: A list of columns o insert into the row, 118 @type row: list 119 120 @return: The newly created iter of the inserted row. 121 @rtype: L{gtk.TreeIter} 122 ''' 123 self.insert(iter, -1, row) 124 125 def remove(self, iter): 126 ''' 127 A method that overrides the L{gtk.TreeStore} remove method. 128 Remove the row's accessible from acc_cache. 129 130 @param iter: The parent iter of the newly inserted row. 131 @type iter: L{gtk.TreeIter} 132 133 @return: True if L{iter} is still valid. 134 @rtype: boolean 135 ''' 136 if self[iter][COL_ACC]: 137 self.acc_cache.remove(self[iter][COL_ACC]) 138 return gtk.TreeStore.remove(self, iter) 139 140 def isInModel(self, acc): 141 ''' 142 Checks if the given L{Accessibility.Accessible} is resident in the model. 143 144 @param acc: The L{Accessibility.Accessible} to check. 145 @type acc: L{Accessibility.Accessible} 146 147 @return: True if it is in the model. 148 @rtype: boolean 149 ''' 150 return acc in self.acc_cache 151 152 def getChildrenAccs(self, iter): 153 ''' 154 Get list of accessible children of a given row. 155 156 I{Note:} This method returns the accessible's children as they currently exist in 157 the model. The list of accessibles is not necessarily identical to the actual 158 children of the accessible at the given row. 159 160 @param iter: Th iter of the row that we want it's children 161 @type iter: L{gtk.TreeIter} 162 163 @return: List of children 164 @rtype: list 165 ''' 166 if iter: 167 return [row[COL_ACC] for row in self[iter].iterchildren()] 168 else: 169 return [row[COL_ACC] for row in self] 170 171 def _prepopLevel(self, parent, iter): 172 ''' 173 Pre-populate a row. If a L{Accessibility.Accessible} of the given row has children, 174 we need to add to it one dummy child row so that the expander will show and 175 enable the user to expand it. We populate the children rows at expansion time. 176 177 @param parent: The given row's accessible. 178 @type parent: L{Accessibility.Accessible} 179 @param iter: Th iter of the row that needs to be pre-populated 180 @type iter: L{gtk.TreeIter} 181 ''' 182 if (parent and self.children_number(parent) > 0 and 183 not self.isMyApp(parent) and self.iter_n_children(iter) == 0): 184 row = self._buildRow(None, True) 185 self.append(iter, row) 186 187 def popLevel(self, iter): 188 ''' 189 Populate a row with children rows, according to the row's accessible's children. 190 191 @param iter: Th iter of the row that needs to be populated 192 @type iter: L{gtk.TreeIter} 193 ''' 194 if iter: 195 tree_path = self.get_path(iter) 196 path = tuple(tree_path.get_indices()) 197 198 row_reference = gtk.TreeRowReference.new(self, tree_path) 199 #row_reference = gtk.TreeRowReference() 200 else: 201 row_reference = None 202 self._populating_tasks += 1 203 if self._populating_tasks == 1: 204 self.emit('start-populating') 205 GLib.idle_add(self._popOnIdle, row_reference) 206 207 def _childrenIndexesInParent(self, accessible): 208 ''' 209 Gets ids from children (either all of them or only those that are not leaves). 210 211 @param accessible: An accessible from which we want to get children ids. 212 @type accessible: L{Accessibility.Accessible} 213 214 @return: A list with children ids. 215 @rtype: list 216 ''' 217 return [i for i in range(accessible.childCount)] 218 219 def _selectChild(self, parent, index): 220 ''' 221 Returns a child id. 222 223 @param parent: An accessible from which we want to select a child id. 224 @type parent: L{Accessibility.Accessible} 225 @param index: An index to retrieve the child id. In some cases, it is equal to the id. 226 @type index: integer 227 228 @return: A child id. 229 @rtype: integer 230 ''' 231 return index 232 233 def children_number(self, parent): 234 ''' 235 Returns how many children an accessible has (either all of them or only those that are not leaves). 236 237 @param parent: An accessible from which we want to get the number of children. 238 @type parent: L{Accessibility.Accessible} 239 240 @return: The number of children (including leaves or not) an accessible has. 241 @rtype: integer 242 ''' 243 return parent.childCount 244 245 def _popOnIdle(self, row_reference): 246 ''' 247 Idle callback for populating the children of a given row reference. 248 249 @param row_reference: The parent row reference. 250 @type row_reference: gtk.TreeRowReference 251 252 @return: False if task is done (to stop handler). 253 @rtype: boolean 254 ''' 255 remove_iter = None 256 iter = None 257 parent = self.desktop 258 259 if row_reference: 260 if not row_reference.valid(): 261 self._endPopTask() 262 return False 263 iter = self.get_iter(row_reference.get_path()) 264 parent = self[iter][COL_ACC] 265 tree_path = row_reference.get_path() 266 path = tuple(tree_path.get_indices()) 267 if self[path+(0,)][COL_DUMMY]: 268 remove_iter = self.iter_children(iter) 269 270 already_populated_num = self.iter_n_children(iter) 271 272 if already_populated_num >= self.children_number(parent) and \ 273 not remove_iter: 274 if iter: 275 self[iter][COL_FILLED] = True 276 self.emit('row-filled', iter) 277 self._endPopTask() 278 return False 279 elif remove_iter: 280 already_populated_num -= 1 281 try: 282 index = self._selectChild(parent, already_populated_num) 283 child = parent.getChildAtIndex(index) 284 except LookupError: 285 child = None 286 287 row = self._buildRow(child) 288 self.append(iter, row) 289 290 if remove_iter: 291 self.remove(remove_iter) 292 return True 293 294 def _endPopTask(self): 295 ''' 296 Convinience function for stopping a populating task. 297 ''' 298 self._populating_tasks -= 1 299 if self._populating_tasks == 0: 300 self.emit('end-populating') 301 302 def popToPath(self, path): 303 ''' 304 Populate the model with accessible nodes up to given path. 305 306 @param path: Path to populate model to. 307 @type path: tuple 308 ''' 309 if not self._walkThroughFilled(path): 310 self._path_to_populate = path 311 else: 312 self.emit('row-filled', self.get_iter(path)) 313 self._path_to_populate = None 314 315 def _walkThroughFilled(self, path): 316 ''' 317 Reach node in path that is not populated yet, and populate it. 318 319 @param path: Path to fill. 320 @type path: tuple 321 322 @return: True if entire path is populated. 323 @rtype: boolean 324 ''' 325 for i in range(1, len(path)): 326 if not self[path[:i]][COL_FILLED]: 327 self.popLevel(self.get_iter(path[:i])) 328 return False 329 return True 330 331 def _onRowFilled(self, model, iter): 332 ''' 333 Callback for "row-filled" signal. If there is a specific path we need to populate, 334 we continue populating the next node. 335 336 @param model: Model that emitted signal (self). 337 @type model: L{AccessibleModel} 338 @param iter: Iter of row that has been populated. 339 @type iter: gtk.TreeIter 340 ''' 341 if iter and self._path_to_populate: 342 tree_path = self.get_path(iter) 343 path = tuple(tree_path.get_indices()) 344 if self._path_to_populate[:len(path)] == path: 345 if self._walkThroughFilled(self._path_to_populate): 346 self._path_to_populate = None 347 348 def getIndexInParent(self, child): 349 ''' 350 Returns a position of a child in its parent. 351 352 @param child: Accessible for which we want to determine the index. 353 @type child: L{Accessibility.Accessible} 354 355 @return: The child's index or -1 if it wasn't found 356 @rtype: integer 357 ''' 358 return child.getIndexInParent() 359 360 def getAccPath(self, acc): 361 ''' 362 Get the tree path that a given accessible should have. 363 364 I{Note:} The accessible does not necessarily need to be resident in the model. 365 366 @param acc: The accessible we want a path of. 367 @type acc: L{Accessibility.Accessible} 368 369 @return: The path to the accessible. 370 @rtype: tuple 371 ''' 372 path = () 373 child = acc 374 while child.get_parent(): 375 try: 376 index_in_parent = self.getIndexInParent(child) 377 if index_in_parent < 0: 378 break 379 path = (index_in_parent,) + path 380 except Exception as e: 381 return None 382 child = child.get_parent() 383 384 try: 385 path = (list(self.desktop).index(child),) + path 386 except Exception as e: 387 return None 388 389 return path 390 391 def _buildRow(self, accessible, dummy=False): 392 ''' 393 Wrapper for building a row in the tree. Use this method instead of trying 394 to construct the row by hand as it will be synced with the design of the 395 model fields. 396 397 @param accessible: Accessible object 398 @type accessible: L{Accessibility.Accessible} 399 @param dummy: Is this a dummy row? 400 @type dummy: boolean 401 ''' 402 if accessible is not None: 403 icon = getIcon(accessible) 404 if self.isMyApp(accessible): 405 name = "Accerciser" 406 role = "accerciser" 407 count = 0 408 else: 409 name = accessible.name 410 role = accessible.getLocalizedRoleName() 411 count = accessible.childCount 412 else: 413 icon = None 414 if not dummy: 415 name = _('<dead>') 416 else: 417 name = None 418 role = None 419 count = 0 420 return [icon, name, role, count, False, dummy, accessible] 421 422class AccessibleTreeView(gtk.TreeView, ToolsAccessor): 423 ''' 424 The treeview for the desktop's accessibles. The treeview's model (L{AccessibleModel} 425 is only populated when the treeview is traversed and nodes are expanded. This class 426 listens for 'row-expanded' events in order to have a tree node populated. Nodes that 427 are selected are updated into the L{Node} instance variable. This treeview also 428 updates automatically in response to at-spi children change events. 429 430 @ivar desktop: The desktop accessible. It holds references to all the 431 application L{Accessibility.Accessible}s 432 @type desktop: L{Accessibility.Accessible} 433 @ivar node: An object with a reference to the currently selected accessible. 434 @type node: L{Node} 435 @ivar model: The data model of this treeview. 436 @type model: L{AccessibleModel} 437 ''' 438 def __init__(self, node): 439 ''' 440 Initialize the treeview. Build the proper columns. 441 Connect all of the proper signal handlers and at-spi event handlers. 442 443 @param node: The main application node. 444 @type node: L{Node} 445 ''' 446 gtk.TreeView.__init__(self) 447 448 self.desktop = pyatspi.Registry.getDesktop(0) 449 self.node = node 450 self.node.update(self.desktop) 451 self._changed_handler = self.node.connect('accessible_changed', 452 self._onAccChanged) 453 self.connect('row-activated', self._onRowActivated) 454 455 self.model = AccessibleModel(self.desktop) 456 self.filter = self.model.filter_new() 457 self.filter.set_visible_func(self._filterNoChildApps, data=None) 458 self.set_model(self.filter) 459 460 crt = gtk.CellRendererText() 461 crp = gtk.CellRendererPixbuf() 462 tvc = gtk.TreeViewColumn(_('Name')) 463 tvc.pack_start(crp, False) 464 tvc.pack_start(crt, True) 465 tvc.add_attribute(crp, 'pixbuf', COL_ICON) 466 tvc.add_attribute(crt, 'text', COL_NAME) 467 tvc.set_resizable(True) 468 self.append_column(tvc) 469 470 crt= gtk.CellRendererText() 471 tvc = gtk.TreeViewColumn(_('Role')) 472 tvc.pack_start(crt, True) 473 tvc.add_attribute(crt, 'text', COL_ROLE) 474 tvc.set_resizable(True) 475 self.append_column(tvc) 476 477 crt = gtk.CellRendererText() 478 tvc = gtk.TreeViewColumn(_('Children')) 479 tvc.pack_start(crt, True) 480 tvc.add_attribute(crt, 'text', COL_CHILDCOUNT) 481 tvc.set_resizable(True) 482 self.append_column(tvc) 483 484 self.model.connect('row-filled', self._onRowFilled) 485 self.model.connect('start-populating', self._onStartPop) 486 self.model.connect('end-populating', self._onEndPop) 487 self._path_to_expand = None 488 489 self._refreshTopLevel() 490 491 selection = self.get_selection() 492 selection.unselect_all() 493 selection.connect('changed', self._onSelectionChanged) 494 self.connect('row-expanded', self._onExpanded) 495 496 pyatspi.Registry.registerEventListener(self._accEventChildChanged, 497 'object:children-changed') 498 499 pyatspi.Registry.registerEventListener( 500 self._accEventNameChanged, 501 'object:property-change:accessible-name') 502 503 self._hide_leaves = True 504 self.action_group = gtk.ActionGroup.new('TreeActions') 505 self.action_group.add_toggle_actions( 506 [('HideShowLeaves', None, _('_Show Applications without children'), None, 507 None, self._onHideShowLeaves, False)]) 508 self.action_group.add_actions([ 509 ('RefreshAll', gtk.STOCK_REFRESH, _('_Refresh Registry'), 510 # Translators: Appears as tooltip 511 # 512 None, _('Refresh all'), self._refreshTopLevel), 513 # Translators: Refresh current tree node's children. 514 # 515 ('RefreshCurrent', gtk.STOCK_JUMP_TO, _('Refresh _Node'), 516 # Translators: Appears as tooltip 517 # 518 None, _('Refresh selected node’s children'), 519 self._refreshCurrentLevel)]) 520 521 self.refresh_current_action = self.action_group.get_action('RefreshCurrent') 522 self.refresh_current_action.set_sensitive(False) 523 524 self.connect('popup-menu', self._onPopup) 525 self.connect('button-press-event', self._onPopup) 526 self.connect('key-press-event', self._onKeyPress) 527 528 self.connect('cursor-changed', self._onCursorChanged) 529 530 def _filterNoChildApps(self, model, iter, data): 531 ''' 532 Filter all rows of applications without children 533 ''' 534 return not self._hide_leaves or model.iter_parent(iter) != None \ 535 or model[iter][COL_CHILDCOUNT] != 0 or self.isMyApp(model[iter][COL_ACC]) 536 537 def _onCursorChanged(self, tree): 538 ''' 539 Set sensitivity of refresh function only if the tree cursor is 540 on an accessible. 541 ''' 542 path = self.get_cursor()[0] 543 self.refresh_current_action.set_sensitive(path is not None) 544 545 def _onKeyPress(self, w, event): 546 ''' 547 Expand or collapse a row on Left/Right key-press 548 ''' 549 if event.state & \ 550 (gdk.ModifierType.SHIFT_MASK 551 |gdk.ModifierType.CONTROL_MASK 552 |gdk.ModifierType.MOD1_MASK) == 0: 553 path, col = self.get_cursor() 554 if path is not None: 555 if event.keyval == gdk.KEY_Left: 556 if not self.collapse_row(path): 557 # if we ccouldn't collapse the current row, go to the parent 558 if path.up(): 559 self.set_cursor(path) 560 return True 561 elif event.keyval == gdk.KEY_Right: 562 self.expand_row(path, False) 563 return True 564 return False 565 566 def _onPopup(self, w, event=None): 567 ''' 568 Callback for popup button or right mouse button. Brings up a context 569 menu. 570 ''' 571 if event: 572 if event.button != 3: 573 return False 574 info = self.get_path_at_pos(int(event.x), int(event.y)) 575 if info is None: 576 return False 577 path = info[0] 578 if self.isMyApp(self.filter[self.filter.get_iter(path)][COL_ACC]): 579 return False 580 selection = self.get_selection() 581 selection.set_mode(gtk.SelectionMode.NONE) 582 self.set_cursor(path, None, False) 583 selection.set_mode(gtk.SelectionMode.SINGLE) 584 time = event.time 585 button = event.button 586 func = None 587 extra_data = None 588 else: 589 path, col= self.get_cursor() 590 time = gtk.get_current_event_time() 591 button = 0 592 extra_data = getTreePathBoundingBox(self, path, col) 593 func = lambda m, b: (b.x, b.y + (b.height/2), True) 594 595 menu = ui_manager.uimanager.get_widget(ui_manager.POPUP_MENU_PATH) 596 menu.popup(None, None, func, extra_data, button, time) 597 return True 598 599 def _refreshTopLevel(self, action=None, data=None): 600 ''' 601 Refreshes the entire tree at the desktop level. 602 603 @param action: Action object that emitted this signal, if any. 604 @type: gtk.Action 605 ''' 606 self.model.clear() 607 self.model.popLevel(None) 608 # iter over all apps in the desktop too 609 610 def _refreshCurrentLevel(self, action, data=None): 611 ''' 612 Refreshes the current level. Selects and expands the parent of the level. 613 614 @param action: Action object that emitted this signal, if any. 615 @type: gtk.Action 616 ''' 617 path = self.get_cursor()[0] 618 if path == None: 619 return 620 filter_iter = self.filter.get_iter(path) 621 if self.isMyApp(self.filter[filter_iter][COL_ACC]): 622 return 623 is_expanded = self.row_expanded(path) 624 self._refreshChildren(self.filter.convert_iter_to_child_iter(filter_iter)) 625 if is_expanded: 626 self.expand_row(path, False) 627 self._onExpanded(self, self.filter.get_iter(path), path) 628 629 def _onHideShowLeaves(self, option): 630 ''' 631 Hides/Shows all leaves (accessibles with no children) from the accessible treeview. 632 ''' 633 self._hide_leaves = not self._hide_leaves 634 self.filter.refilter() 635 636 def _onExpanded(self, treeview, iter, path): 637 ''' 638 Populates a level when it is expanded. Removes the previously added dummy 639 node. 640 641 @param treeview: The L{AccessibleTreeView} that emitted the signal. 642 @type treeview: L{AccessibleTreeView} 643 @param iter: The iter that has been expanded. 644 @type iter: L{gtk.TreeIter} 645 @param path: The path to the row that has been expanded. 646 @type path: tuple 647 ''' 648 # don't repopulate if it has been filled before 649 if self.filter[iter][COL_FILLED]: 650 return 651 # populate this level 652 self.model.popLevel(self.filter.convert_iter_to_child_iter(iter)) 653 654 def _accEventNameChanged(self, event): 655 ''' 656 Event handler for "object:property-change:accessible-name". 657 Updates the treeview accordingly. 658 659 @param event: The event which triggered this handler. 660 @type event: L{pyatspi.event.Event} 661 ''' 662 if self.isMyApp(event.source) or event.source == self.desktop: 663 # Bad karma 664 return 665 if self.model.isInModel(event.source): 666 try: 667 path = self.model.getAccPath(event.source) 668 iter = self.model.get_iter(path) 669 except: 670 pass 671 else: 672 self.model[iter][COL_NAME] = event.source.name 673 674 def _accEventChildChanged(self, event): 675 ''' 676 Event handler for "object:children-changed". Updates the treeview accordingly. 677 678 @param event: The event which triggered this handler. 679 @type event: L{pyatspi.event.Event} 680 ''' 681 if self.isMyApp(event.source): 682 # Bad karma 683 return 684 if self.model.isInModel(event.source): 685 try: 686 path = self.model.getAccPath(event.source) 687 iter = self.model.get_iter(path) 688 except: 689 iter = None 690 path = None 691 if (event.source == self.desktop) or \ 692 (path and self.model[path][COL_FILLED]): 693 if event.type.minor == 'add': 694 self._addChild(iter, event.source) 695 elif event.type.minor == 'remove': 696 self._removeChild(iter, event.source) 697 if iter and self.model.iter_is_valid(iter): 698 self.model[iter][COL_CHILDCOUNT] = event.source.childCount 699 700 def _addChild(self, iter, parent): 701 ''' 702 Add the new child to the given accessible. 703 704 @param iter: Th iter of the row that needs a child added. 705 @type iter: L{gtk.TreeIter} 706 @param parent: The given row's accessible. 707 @type parent: L{Accessibility.Accessible} 708 ''' 709 old_children = set(self.model.getChildrenAccs(iter)) 710 new_children = set(list(parent)) 711 712 added = new_children.difference(old_children) 713 try: 714 new_child = added.pop() 715 except KeyError: 716 return 717 row = self.model._buildRow(new_child) 718 if new_child is None: 719 self.model.append(iter, row) 720 else: 721 self.model.insert(iter, self.model.getIndexInParent(new_child), row) 722 # We do this because an application won't have an icon loaded in 723 # the window manager when it is first registered to at-spi 724 if new_child == new_child.getApplication(): 725 GLib.timeout_add(1000, self._refreshIcon, new_child) 726 727 def _refreshIcon(self, app): 728 ''' 729 Refresh the icon of a given application's accessible. This is done because it 730 takes wnck a while to load an application's icon at application startup. 731 732 @param app: The given application's accessible. 733 @type app: L{Accessibility.Accessible} 734 ''' 735 path = self.model.getAccPath(app) 736 try: 737 self.model[path][COL_ICON] = getIcon(app) 738 except: 739 pass 740 return False 741 742 def _removeChild(self, parent_iter, parent): 743 ''' 744 Remove a child from the given accessible node. 745 746 @param parent_iter: Th iter of the row that needs a child removed. 747 @type parent_iter: L{gtk.TreeIter} 748 @param parent: The given row's accessible. 749 @type parent: L{Accessibility.Accessible} 750 ''' 751 if parent_iter: 752 iter = self.model.iter_children(parent_iter) 753 else: 754 iter = self.model.get_iter_first() 755 756 while iter: 757 if self.model[iter][COL_ACC] not in parent: 758 cursor_path = self.get_cursor()[0] 759 if cursor_path != None: 760 (res, filter_iter) = self.filter.convert_child_iter_to_iter(iter) 761 if res: 762 filter_path = self.filter.get_path(filter_iter) 763 if filter_path.is_ancestor(cursor_path): 764 cursor_path = filter_path 765 if 0 == filter_path.compare(cursor_path): 766 if filter_path.prev() or filter_path.up(): 767 self.set_cursor(filter_path, None, False) 768 if not self.model.remove(iter): 769 break 770 else: 771 iter = self.model.iter_next(iter) 772 773 def _refreshChildren(self, iter): 774 ''' 775 Remove all of a given node's children from the model. 776 777 @param iter: The parent node. 778 @type iter: L{gtk.TreeIter} 779 ''' 780 if not iter: 781 self._refreshTopLevel() 782 return 783 child_iter = self.model.iter_children(iter) 784 while child_iter: 785 if not self.model.remove(child_iter): 786 break 787 acc = self.model[iter][COL_ACC] 788 self.model[iter][COL_CHILDCOUNT] = acc.childCount 789 self.model[iter][COL_FILLED] = False 790 791 def refreshSelected(self): 792 ''' 793 Manually refresh the selected node. 794 ''' 795 selection = self.get_selection() 796 model, iter = selection.get_selected() 797 if not self.isMyApp(model[iter][COL_ACC]): 798 self._refreshChildren(iter) 799 800 def _onSelectionChanged(self, selection): 801 ''' 802 Update the accessible according to the selected row. 803 804 @param selection: The selection object that emitted the the 'selection-changed' 805 signal. 806 @type selection: L{gtk.TreeSelection} 807 ''' 808 self._path_to_expand = None 809 model, iter = selection.get_selected() 810 if iter: 811 new_acc = model[iter][COL_ACC] 812 else: 813 new_acc = self.desktop 814 if new_acc == self.node.acc: 815 return 816 self.node.handler_block(self._changed_handler) 817 self.node.update(new_acc) 818 self.node.handler_unblock(self._changed_handler) 819 if iter: 820 tree_path = model.get_path(iter) 821 path = tuple(tree_path.get_indices()) 822 823 self.node.tree_path = list(path[1:]) 824 825 def _onAccChanged(self, node, acc): 826 ''' 827 Change the treeview's selection to the updated accessible in the L{Node}. 828 829 @param node: The L{node} that emitted the signal. 830 @type node: L{Node} 831 @param acc: The new accessible in the node. 832 @type acc: L{Accessibility.Accessible} 833 ''' 834 if self.isMyApp(acc): 835 # Bad karma 836 return 837 path = self.model.getAccPath(acc) 838 if not path: 839 return 840 if len(path) >= 1: 841 self.selectNodeAtPath(path) 842 self.node.tree_path = list(path[1:]) 843 844 def selectNodeAtPath(self, path): 845 ''' 846 Select the node at the current path. The path does not need to exist in the model, 847 only in the accessibles tree. The model will get populated accordingly. 848 849 @param path: The path to select. 850 @type path: tuple 851 ''' 852 try: 853 dummy = self.model[path][COL_DUMMY] 854 except: 855 dummy = True 856 if dummy: 857 self._path_to_expand = path 858 self.model.popToPath(path) 859 else: 860 self._selectExistingPath(path) 861 862 def _onRowFilled(self, model, iter): 863 ''' 864 Callback for "row-filled" (populated) signal. Used for selecting a child node in 865 the given iter if L{selectNodeAtPath} was called on one of the given 866 iter's children. 867 868 @param model: Model that emitted this signal. 869 @type model: L{AccessibleModel} 870 @param iter: Iter of row that was populated with children. 871 @type iter: gtk.TreeIter 872 ''' 873 if iter and self._path_to_expand and \ 874 self._path_to_expand[:-1] == model.get_path(iter).get_indices(): 875 self._selectExistingPath(self._path_to_expand) 876 self._path_to_expand = None 877 878 def _selectExistingPath(self, path): 879 ''' 880 Select a path that already exists. Expand, scroll, and select. 881 882 @param path: Path to select. 883 @type path: tuple 884 ''' 885 tree_path = gtk.TreePath(path[:-1]) 886 if len(path) > 1: 887 self.expand_to_path(tree_path) 888 self.scroll_to_cell(path) 889 selection = self.get_selection() 890 selection.select_path(path) 891 892 893 def _onStartPop(self, model): 894 ''' 895 Callback for when the model is populating, changes the cursor to a watch. 896 897 @param model: Model that emitted the signal. 898 @type model: L{AccessibleModel} 899 ''' 900 if self.get_window(): 901 window = self.get_window() 902 window.set_cursor(gdk.Cursor(gdk.CursorType.WATCH)) 903 904 def _onEndPop(self, model): 905 ''' 906 Callback for when the model stops populating, changes the cursor to an arrow. 907 908 @param model: Model that emitted the signal. 909 @type model: L{AccessibleModel} 910 ''' 911 if self.get_window(): 912 window = self.get_window() 913 window.set_cursor(gdk.Cursor(gdk.CursorType.TOP_LEFT_ARROW)) 914 915 def _onRowActivated(self, treeview, path, view_column): 916 ''' 917 When the row is activated (double clicked, or enter), blink the selected 918 accessible, if possible. 919 920 @param treeview: The L{AccessibleTreeView} that emitted the signal. 921 @type treeview: L{AccessibleTreeView} 922 @param path: The path to the selected row. 923 @type path: tuple 924 @param view_column: The column in the activated row. 925 @type view_column: L{gtk.TreeViewColumn} 926 ''' 927 if not self.isMyApp(self.filter[self.filter.get_iter(path)][COL_ACC]): 928 self.node.highlight() 929