1from gi.repository import GLib 2from gi.repository import Gtk as gtk 3from gi.repository import Atk as atk 4 5import os 6from xml.dom.minidom import getDOMImplementation, parse, Element 7from .i18n import _ 8from pyatspi import getPath 9from random import random 10import random 11from . import ui_manager 12 13COL_NAME = 0 14COL_APP = 1 15COL_PATH = 2 16_BM_ATTRIBS = ['title', 'app', 'path'] 17 18BOOKMARKS_PATH = os.path.join(GLib.get_user_config_dir(), 'accerciser') 19BOOKMARKS_FILE = 'bookmarks.xml' 20 21class BookmarkStore(gtk.ListStore): 22 ''' 23 Bookmark manager class. Does three things: 24 1. Stores bookmarks. 25 2. Persists bookmark changes to disk. 26 3. Keeps bookmarks submenu up to date. 27 28 @ivar _bookmarks_action_group: Bookmarks' action group. 29 @type _bookmarks_action_group: gtk.ActionGroup 30 @ivar node: Main application's node. 31 @type node: L{Node} 32 @ivar _xmldoc: XML documenr object. 33 @type _xmldoc: xml.dom.DOMImplementation 34 ''' 35 def __init__(self, node, window): 36 ''' 37 Initialize bookmark manager. Load saved bookmarks from disk. 38 39 @param node: Main application's node. 40 @type node: L{Node> 41 ''' 42 gtk.ListStore.__init__(self, object) 43 self._bookmarks_action_group = gtk.ActionGroup.new('BookmarkActions') 44 ui_manager.uimanager.insert_action_group(self._bookmarks_action_group, 0) 45 self._buildMenuUI() 46 self.node = node 47 self.parent_window = window 48 bookmarks_fn = os.path.join(BOOKMARKS_PATH, BOOKMARKS_FILE) 49 try: 50 self._xmldoc = parse(bookmarks_fn) 51 except: 52 impl = getDOMImplementation() 53 self._xmldoc = impl.createDocument(None, "bookmarks", None) 54 self._cleanDoc() 55 self._populateModel() 56 self.connect('row-changed', self._onRowChanged) 57 self.connect('row-deleted', self._onRowDeleted) 58 self.connect('row-inserted', self._onRowInserted) 59 60 def _buildMenuUI(self): 61 ''' 62 Build's the initial submenu with functionality menu items. 63 ''' 64 self._bookmarks_action_group.add_actions( 65 [('AddBookmark', gtk.STOCK_ADD, 66 _('_Add Bookmark…'), '<Control>d', 67 _('Bookmark selected accessible.'), self._onAddBookmark), 68 ('EditBookmarks', gtk.STOCK_EDIT, 69 _('_Edit Bookmarks…'), None, 70 _('Manage bookmarks.'), self._onEditBookmarks)]) 71 72 73 for action in self._bookmarks_action_group.list_actions(): 74 merge_id = ui_manager.uimanager.new_merge_id() 75 action_name = action.get_name() 76 ui_manager.uimanager.add_ui(merge_id, ui_manager.BOOKMARKS_MENU_PATH, 77 action_name, action_name, 78 gtk.UIManagerItemType.MENUITEM, False) 79 80 ui_manager.uimanager.add_ui(ui_manager.uimanager.new_merge_id(), 81 ui_manager.BOOKMARKS_MENU_PATH, 82 'sep', None, 83 gtk.UIManagerItemType.SEPARATOR, False) 84 85 def _onAddBookmark(self, action, data=None): 86 ''' 87 Callback for AddBookmark action. 88 89 @param action: Action that emitted this signal. 90 @type action: gtk.Action 91 ''' 92 iter = self.bookmarkCurrent() 93 if not iter: return 94 bookmark = self[iter][0] 95 dialog = self._NewBookmarkDialog(bookmark, self.parent_window) 96 response_id = dialog.run() 97 if response_id == gtk.ResponseType.OK: 98 bookmark.title, bookmark.app, bookmark.path = dialog.getFields() 99 else: 100 self.remove(iter) 101 dialog.destroy() 102 103 def _onEditBookmarks(self, action, data=None): 104 ''' 105 Callback for EditBookmark action. 106 107 @param action: Action that emitted this signal. 108 @type action: gtk.Action 109 ''' 110 dialog = self._EditDialog(self) 111 dialog.show() 112 113 def _cleanDoc(self): 114 ''' 115 Clean up whitespace in XML doc. 116 ''' 117 elements = self._getElements() 118 for node in self._xmldoc.documentElement.childNodes: 119 if node not in elements: 120 self._xmldoc.documentElement.removeChild(node) 121 122 def _populateModel(self): 123 ''' 124 Populate model with stored bookmarks. 125 ''' 126 for node in self._getElements(): 127 title = node.getAttribute('title') 128 app = node.getAttribute('app') 129 path = node.getAttribute('path') 130 self.addBookmark(title, app, path) 131 132 def addBookmark(self, title, app, path): 133 ''' 134 Add a bookmark to the maanger. 135 136 @param title: Title of bookmark 137 @type title: string 138 @param app: Application name of bookmark. 139 @type app: string 140 @param path: Path of bookmarks. 141 @type path: string 142 143 @return: Tree iter of new bookmark. 144 @rtype: gtk.TreeIter 145 ''' 146 iter = self.append([None]) 147 merge_id = ui_manager.uimanager.new_merge_id() 148 name = 'Bookmark%s' % merge_id 149 bookmark = self._Bookmark(name, title, app, path, merge_id) 150 bookmark.connect('activate', self._onBookmarkActivate) 151 bookmark.connect('notify', self._onBookmarkChanged) 152 self._bookmarks_action_group.add_action(bookmark) 153 ui_manager.uimanager.add_ui(merge_id, 154 '/MainMenuBar/Bookmarks', name, name, 155 gtk.UIManagerItemType.MENUITEM, False) 156 self[iter][0] = bookmark 157 return iter 158 159 def removeBookmark(self, bookmark): 160 ''' 161 Remove bookmark from manager. 162 163 @param bookmark: Bookmark to remove. 164 @type bookmark: BookmarkStore._Bookmark 165 ''' 166 self._bookmarks_action_group.remove_action(bookmark) 167 ui_manager.uimanager.remove_ui(bookmark.merge_id) 168 for row in self: 169 if row[0] == bookmark: 170 self.remove(row.iter) 171 172 def _onBookmarkChanged(self, bookmark, property): 173 ''' 174 Emit a 'row-changed' signal when bookmark's properties emit a 'notify' event. 175 176 @param bookmark: Bookmark that emitted 'notify' event. 177 @type bookmark: L{BookmarkStore._Bookmark} 178 @param property: Property that changed, ignored because we emit dummy signals. 179 @type property: Property 180 ''' 181 for row in self: 182 if row[0] == bookmark: 183 self.row_changed(row.path, row.iter) 184 185 def _getElements(self): 186 ''' 187 Get a list of elements from XML doc. Filter out strings. 188 189 @return: list of elements. 190 @rtype: list of Element 191 ''' 192 return [x for x in self._xmldoc.documentElement.childNodes if isinstance(x, Element)] 193 194 def _onRowChanged(self, model, tree_path, iter): 195 ''' 196 Callback for row changes. Persist changes to disk. 197 198 @param model: Model that emitted signal 199 @type model: L{BookmarkStore} 200 @param path: Path of row that changed. 201 @type path: tuple 202 @param iter: Iter of row that changed. 203 @type iter: gtk.TreeIter 204 ''' 205 path = tuple(tree_path.get_indices()) 206 node = self._getElements()[path[0]] 207 bookmark = model[iter][0] 208 if bookmark is None: return 209 for attr in _BM_ATTRIBS: 210 if getattr(bookmark, attr) is None: continue 211 node.setAttribute(attr, getattr(bookmark, attr)) 212 self._persist() 213 214 def _onRowDeleted(self, model, tree_path): 215 ''' 216 Callback for row deletions. Persist changes to disk, and update UI. 217 218 @param model: Model that emitted signal 219 @type model: L{BookmarkStore} 220 @param path: Path of row that got deleted. 221 @type path: tuple 222 ''' 223 path = tuple(tree_path.get_indices()) 224 node = self._getElements()[path[0]] 225 self._xmldoc.documentElement.removeChild(node) 226 self._persist() 227 228 def _onRowInserted(self, model, path, iter): 229 ''' 230 Callback for row insertions. Persist changes to disk. 231 232 @param model: Model that emitted signal 233 @type model: L{BookmarkStore} 234 @param path: Path of row that is inserted. 235 @type path: tuple 236 @param iter: Iter of row that is inserted. 237 @type iter: gtk.TreeIter 238 ''' 239 node = self._xmldoc.createElement('bookmark') 240 self._xmldoc.documentElement.appendChild(node) 241 self._persist() 242 243 def _persist(self): 244 ''' 245 Persist DOM to disk. 246 ''' 247 bookmarks_fn = os.path.join(BOOKMARKS_PATH, BOOKMARKS_FILE) 248 try: 249 if not os.path.exists(os.path.dirname(bookmarks_fn)): 250 os.mkdir(os.path.dirname(bookmarks_fn)) 251 f = open(bookmarks_fn, 'w') 252 except: 253 return 254 self._xmldoc.writexml(f, '', ' ', '\n') 255 f.close() 256 257 def _onBookmarkActivate(self, bookmark): 258 ''' 259 Bookmark activation callback 260 261 @param bookmark: Bookmark that was activated. 262 @type bookmark: L{BookmarkStore._Bookmark} 263 ''' 264 self.jumpTo(bookmark) 265 266 def jumpTo(self, bookmark): 267 ''' 268 Go to bookmarks. 269 270 @param bookmark: Bookmark to go to. 271 @type bookmark: L{BookmarkStore._Bookmark} 272 ''' 273 if '' == bookmark.path: 274 path = () 275 else: 276 path = list(map(int, bookmark.path.split(','))) 277 self.node.updateToPath(bookmark.app, path) 278 279 def bookmarkCurrent(self): 280 ''' 281 Bookmark the currently selected application-wide node. 282 283 @return: Tree iter of new bookmark. 284 @rtype: gtk.TreeIter 285 ''' 286 if self.node.acc in (self.node.desktop, None): return None 287 if self.node.tree_path is not None: 288 path = ','.join(map(str, self.node.tree_path)) 289 else: 290 path = ','.join(map(str, getPath(self.node.acc))) 291 app = self.node.acc.getApplication() 292 role = self.node.acc.getLocalizedRoleName() 293 first_bm_name = '%s in %s' % (self.node.acc.name or role, app.name) 294 bm_name = first_bm_name 295 i = 1 296 while self._nameIsTaken(bm_name): 297 bm_name = '%s (%d)' % (first_bm_name, i) 298 i += 1 299 return self.addBookmark(bm_name, app.name, path) 300 301 def _nameIsTaken(self, name): 302 ''' 303 Check if label text is already in use. 304 305 @param name: Name to check. 306 @type name: string 307 308 @return: True is name is taken. 309 @rtype: boolean 310 ''' 311 for row in self: 312 bookmark = row[0] 313 if bookmark.get_property('label') == name: 314 return True 315 return False 316 317 class _EditDialog(gtk.Dialog): 318 ''' 319 Dialog for editing and managing bookmarks. 320 ''' 321 def __init__(self, bookmarks_store): 322 ''' 323 Initialize dialog. 324 325 @param bookmarks_store: Bookmarks manager. 326 @type bookmarks_store: L{BookmarkStore} 327 ''' 328 gtk.Dialog.__init__(self, name=_('Edit Bookmarks…')) 329 self.add_buttons(gtk.STOCK_CLOSE, gtk.ResponseType.CLOSE) 330 self.set_default_size(480, 240) 331 self.connect('response', self._onResponse) 332 vbox = self.get_children()[0] 333 hbox = gtk.HBox() 334 hbox.set_spacing(3) 335 tv = self._createTreeView(bookmarks_store) 336 sw = gtk.ScrolledWindow() 337 sw.set_policy(gtk.PolicyType.AUTOMATIC, gtk.PolicyType.AUTOMATIC) 338 sw.set_shadow_type(gtk.ShadowType.IN) 339 sw.add(tv) 340 hbox.pack_start(sw, True, True, 0) 341 button_vbox = gtk.VBox() 342 hbox.pack_start(button_vbox, False, False, 0) 343 add_button = gtk.Button.new_from_stock('gtk-add') 344 add_button.set_use_stock(True) 345 add_button.connect('clicked', self._onAddClicked, tv) 346 remove_button = gtk.Button.new_from_stock('gtk-remove') 347 remove_button.set_use_stock(True) 348 remove_button.connect('clicked', self._onRemoveClicked, tv) 349 jump_button = gtk.Button.new_from_stock('gtk-jump-to') 350 jump_button.set_use_stock(True) 351 jump_button.connect('clicked', self._onJumpToClicked, tv) 352 button_vbox.pack_start(add_button, False, False, 0) 353 button_vbox.pack_start(remove_button, False, False, 0) 354 button_vbox.pack_start(jump_button, False, False, 0) 355 vbox.add(hbox) 356 hbox.set_border_width(3) 357 self.set_transient_for(bookmarks_store.parent_window) 358 self.show_all() 359 360 def _onAddClicked(self, button, tv): 361 ''' 362 Callback for add button. Add a bookmark. 363 364 @param button: Add button 365 @type button: gtk.Button 366 @param tv: Treeview of dialog. 367 @type tv: gtk.TreeView 368 ''' 369 model = tv.get_model() 370 iter = model.bookmarkCurrent() 371 if not iter: return 372 selection = tv.get_selection() 373 selection.select_iter(iter) 374 375 def _onRemoveClicked(self, button, tv): 376 ''' 377 Callback for remove button. Remove a bookmark. 378 379 @param button: Remove button 380 @type button: gtk.Button 381 @param tv: Treeview of dialog. 382 @type tv: gtk.TreeView 383 ''' 384 selection = tv.get_selection() 385 model, iter = selection.get_selected() 386 path = model.get_path(iter) 387 if iter: 388 bookmark = model[iter][0] 389 model.removeBookmark(bookmark) 390 selection.select_path(0) 391 392 393 def _onJumpToClicked(self, button, tv): 394 ''' 395 Callback for "jump to" button. Go to bookmark. 396 397 @param button: "jump to" button 398 @type button: gtk.Button 399 @param tv: Treeview of dialog. 400 @type tv: gtk.TreeView 401 ''' 402 selection = tv.get_selection() 403 model, iter = selection.get_selected() 404 bookmark = model[iter][0] 405 model.jumpTo(bookmark) 406 407 def _onResponse(self, dialog, response): 408 ''' 409 Callback for dialog response. 410 411 @param dialog: Dialog. 412 @type dialog: gtk.Dialog 413 @param response: response ID. 414 @type response: integer 415 ''' 416 self.destroy() 417 418 def _createTreeView(self, model): 419 ''' 420 Create dialog's tree view. 421 422 @param model: Data model for view. 423 @type model: L{BookmarkStore} 424 425 @return: The new tree view. 426 @rtype: gtk.TreeView 427 ''' 428 tv = gtk.TreeView() 429 tv.set_model(model) 430 431 crt = gtk.CellRendererText() 432 crt.set_property('editable', True) 433 crt.connect('edited', self._onCellEdited, model, COL_NAME) 434 tvc = gtk.TreeViewColumn(_('Title')) 435 tvc.pack_start(crt, True) 436 tvc.set_cell_data_func(crt, self._cellDataFunc, COL_NAME) 437 tv.append_column(tvc) 438 439 crt = gtk.CellRendererText() 440 crt.set_property('editable', True) 441 crt.connect('edited', self._onCellEdited, model, COL_APP) 442 tvc = gtk.TreeViewColumn(_('Application')) 443 tvc.pack_start(crt, True) 444 tvc.set_cell_data_func(crt, self._cellDataFunc, COL_APP) 445 tv.append_column(tvc) 446 447 crt = gtk.CellRendererText() 448 crt.set_property('editable', True) 449 crt.connect('edited', self._onCellEdited, model, COL_PATH) 450 tvc = gtk.TreeViewColumn(_('Path')) 451 tvc.pack_start(crt, True) 452 tvc.set_cell_data_func(crt, self._cellDataFunc, COL_PATH) 453 tv.append_column(tvc) 454 455 return tv 456 457 def _onCellEdited(self, cellrenderer, path, new_text, model, col_id): 458 ''' 459 Callback for cell editing. Blocks unallowed input. 460 461 @param cellrenderer: Cellrenderer that is being edited. 462 @type cellrenderer: gtk.CellRendererText 463 @param path: Path of tree node. 464 @type path: tuple 465 @param new_text: New text that was entered. 466 @type new_text: string. 467 @param model: Model of tree view. 468 @type model: L{BookmarkStore} 469 @param col_id: Column ID of change. 470 @type col_id: integer 471 ''' 472 if col_id == COL_NAME and new_text == '': 473 return 474 if col_id == COL_PATH: 475 try: 476 int_path = list(map(int, new_text.split(','))) 477 except ValueError: 478 return 479 bookmark = model[path][0] 480 setattr(bookmark, _BM_ATTRIBS[col_id], new_text) 481 482 def _cellDataFunc(self, column, cell, model, iter, col_id): 483 ''' 484 Cell renderer display function. 485 486 @param column: Tree view column 487 @type column: gtk.TreeViewColumn 488 @param cell: Cell renderer. 489 @type cell: gtk.CellRendererText 490 @param model: Data model. 491 @type model: L{BookmarkStore} 492 @param iter: Tree iter. 493 @type iter: gtk.TreeIter 494 @param col_id: Column ID. 495 @type col_id: integer 496 ''' 497 bookmark = model[iter][0] 498 cell.set_property('text', 499 getattr(bookmark, _BM_ATTRIBS[col_id], '')) 500 501 class _NewBookmarkDialog(gtk.Dialog): 502 ''' 503 New bookmark entry dialog. 504 505 @ivar _title_entry: Title entry widget 506 @type _title_entry: gtk.Entry 507 @ivar _app_entry: Application name entry widget 508 @type _app_entry: gtk.Entry 509 @ivar _path_entry: Path entry widget 510 @type _path_entry: gtk.Entry 511 ''' 512 def __init__(self, bookmark, parent_window): 513 ''' 514 Initialize the dialog. 515 516 @param bookmark: New bookmark to edit. 517 @type bookmark: L{BookmarkStore._Bookmark} 518 ''' 519 gtk.Dialog.__init__(self, _('Add Bookmark…')) 520 self.add_button(gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL) 521 ok_button = self.add_button(gtk.STOCK_ADD, gtk.ResponseType.OK) 522 ok_button.set_sensitive(False) 523 self.set_default_response(gtk.ResponseType.OK) 524 table = gtk.Table.new(3, 2, False) 525 table.set_row_spacings(3) 526 table.set_col_spacings(3) 527 vbox = self.get_children()[0] 528 vbox.add(table) 529 self._title_entry = gtk.Entry() 530 self._title_entry.connect('changed', self._onChanged, ok_button) 531 self._app_entry = gtk.Entry() 532 self._path_entry = gtk.Entry() 533 for i, label_entry_pair in enumerate([(_('Title:'), 534 bookmark.title, 535 self._title_entry), 536 (_('Application:'), 537 bookmark.app, 538 self._app_entry), 539 (_('Path:'), 540 bookmark.path, 541 self._path_entry)]): 542 label, value, entry = label_entry_pair 543 entry.set_text(value) 544 entry.connect('activate', self._onEnter, ok_button) 545 label_widget = gtk.Label.new(label) 546 label_widget.set_alignment(0.0, 0.5) 547 label_acc = label_widget.get_accessible() 548 entry_acc = entry.get_accessible() 549 label_acc.add_relationship(atk.RelationType.LABEL_FOR, entry_acc) 550 entry_acc.add_relationship(atk.RelationType.LABELLED_BY, label_acc) 551 table.attach(gtk.Label.new(label), 0, 1, i, i+1, gtk.AttachOptions.FILL, 0) 552 table.attach(entry, 1, 2, i, i+1) 553 self.set_transient_for(parent_window) 554 self.show_all() 555 556 def _onChanged(self, entry, button): 557 ''' 558 Title entry changed callback. If title entry is empty, disable "add" button. 559 560 @param entry: Entry widget that changed. 561 @type entry: gtk.Entry 562 @param button: Add button. 563 @type button: gtk.Button 564 ''' 565 text = entry.get_text() 566 not_empty = bool(text) 567 button.set_sensitive(not_empty) 568 569 def _onEnter(self, entry, button): 570 ''' 571 Finish dialog when enter is pressed. 572 573 @param entry: Entry widget that changed. 574 @type entry: gtk.Entry 575 @param button: Add button. 576 @type button: gtk.Button 577 ''' 578 button.clicked() 579 580 def getFields(self): 581 ''' 582 Return value of all fields. 583 584 @return: title, app name, and path. 585 @rtype: tuple 586 ''' 587 return \ 588 self._title_entry.get_text(), \ 589 self._app_entry.get_text(), \ 590 self._path_entry.get_text() 591 592 class _Bookmark(gtk.Action): 593 ''' 594 Bookmark object. 595 596 @ivar title: Bookmark title (and label). 597 @type title: string 598 @ivar app: Application name. 599 @type app: string 600 @ivar path: Accessible path. 601 @type path: string 602 @ivar merge_id: Merge id of UIManager. 603 @type merge_id: integer 604 ''' 605 def __init__(self, name, title, app, path, merge_id): 606 ''' 607 Initialize bookmark. 608 609 @param name: Action name 610 @type name: string 611 @param title: Bookmark title (and label). 612 @type title: string 613 @param app: Application name. 614 @type app: string 615 @param path: Accessible path. 616 @type path: string 617 @param merge_id: Merge id of UIManager. 618 @type merge_id: integer 619 ''' 620 gtk.Action.__init__(self, name, title, None, None) 621 self._title = title 622 self._app = app 623 self._path = path 624 self.merge_id = merge_id 625 626 def _getTitle(self): 627 return self._title 628 def _setTitle(self, title): 629 self._title = title 630 self.set_property('label', title) 631 title = property(_getTitle, _setTitle) 632 633 def _getApp(self): 634 return self._app 635 def _setApp(self, app): 636 self._app = app 637 self.notify('name') 638 app = property(_getApp, _setApp) 639 640 def _getPath(self): 641 return self._path 642 def _setPath(self, path): 643 self._path = path 644 self.notify('name') 645 path = property(_getPath, _setPath) 646