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