1'''
2Defines and manages the multiple plugin views.
3
4@author: Eitan Isaacson
5@organization: Mozilla Foundation
6@copyright: Copyright (c) 2006, 2007 Mozilla Foundation
7@license: BSD
8
9All rights reserved. This program and the accompanying materials are made
10available under the terms of the BSD which accompanies this distribution, and
11is available at U{http://www.opensource.org/licenses/bsd-license.php}
12'''
13from gi.repository import Gtk as gtk
14from gi.repository import Gdk as gdk
15from gi.repository.Gio import Settings as GSettings
16from gi.repository import GObject
17
18from .base_plugin import Plugin
19from accerciser.tools import *
20from .message import MessageManager
21import os
22import sys
23import imp
24from accerciser.i18n import _, N_
25import gc
26from accerciser import ui_manager
27
28
29GSCHEMA = 'org.a11y.Accerciser'
30PLUGVIEWS_GSCHEMA = 'org.a11y.Accerciser.pluginviews'
31NEWPLUGVIEWS_GSCHEMA = 'org.a11y.Accerciser.newpluginviews'
32NEWPLUGVIEWS_PATH = '/org/a11y/accerciser/newpluginviews/'
33
34
35class PluginView(gtk.Notebook):
36  '''
37  Container for multiple plugins. Implemented with a gtk notebook.
38
39  @cvar TARGET_PLUGINVIEW: Drag and drop target ID for another pluginview.
40  @type TARGET_PLUGINVIEW: integer
41  @cvar TARGET_ROOTWIN: Drag and drop target ID for root window.
42  @type TARGET_ROOTWIN: integer
43  @ivar NOTEBOOK_GROUP: Group ID for detachable tabs.
44  @type NOTEBOOK_GROUP: string
45
46  @ivar view_name: Name of view.
47  @type view_name: string
48  '''
49
50  __gsignals__ = {'plugin_drag_end' :
51                  (GObject.SignalFlags.RUN_FIRST,
52                   None,
53                   (GObject.TYPE_OBJECT,)),
54                  'tab_popup_menu' :
55                  (GObject.SignalFlags.RUN_FIRST,
56                   None,
57                   (GObject.TYPE_PYOBJECT,
58                    GObject.TYPE_OBJECT))}
59  TARGET_PLUGINVIEW = 0
60  TARGET_ROOTWIN = 1
61  NOTEBOOK_GROUP = 'NOTEBOOK_GROUP'
62
63  def __init__(self, view_name):
64    '''
65    Initialize a new plugin view.
66
67    @param view_name: The name of the view.
68    @type view_name: string
69    '''
70    gtk.Notebook.__init__(self)
71    self.view_name = view_name
72    self.set_scrollable(True)
73    self.set_group_name(self.NOTEBOOK_GROUP)
74    self.connect('drag-end', self._onDragEnd)
75    self.connect('drag-data-get', self._onDragDataGet)
76    self.connect('key-press-event', self._onKeyPress)
77    self.connect('button-press-event', self._onButtonPress)
78
79    self.dest_type = None
80
81  def _onButtonPress(self, nb, event):
82    '''
83    Callback for button presses, used for tab context menus.
84
85    @param nb: Notebook that emitted signal.
86    @type nb: gtk.Notebook
87    @param event: Event object.
88    @type event: gtk.dk.Event
89    '''
90    plugin = self._getClickedPlugin(event.x_root, event.y_root)
91    if plugin and event.button == 3:
92      self.emit('tab_popup_menu', event, plugin)
93
94  def _onKeyPress(self, nb, event):
95    '''
96    Callback for key presses, used for tab context menus.
97
98    @param nb: Notebook that emitted signal.
99    @type nb: gtk.Notebook
100    @param event: Event object.
101    @type event: gtk.dk.Event
102    '''
103    if event.keyval == gdk.KEY_Menu and \
104          self.get_property('has-focus'):
105      page_num = self.get_current_page()
106      child = self.get_nth_page(page_num)
107      if isinstance(child, Plugin):
108        self.emit('tab_popup_menu', event, self.get_nth_page(page_num))
109
110  def _getClickedPlugin(self, event_x, event_y):
111    '''
112    Determines which plugin's tab was clicked with given coordinates.
113
114    @param event_x: X coordnate of click.
115    @type event_x: integer
116    @param event_y: Y coordnate of click.
117    @type event_y: integer
118
119    @return: Tab's plugin or None if nothing found.
120    @rtype: L{Plugin}
121    '''
122    for child in self.getPlugins():
123      tab = self.get_tab_label(child)
124      # TODO-JH: Don't really know if this is correct
125      if tab != None and tab.get_state_flags() & tab.get_mapped():
126        x, y, w, h = self.getTabAlloc(tab)
127        if event_x >= x and \
128              event_x <= x + w and \
129              event_y >= y and \
130              event_y <= y + h:
131          return child
132    return None
133
134  def getTabAlloc(self, widget):
135    '''
136    Get the screen allocation of the given tab.
137
138    @param widget: The tab widget.
139    @type widget: gtk.Widget
140
141    @return: X, Y, width any height coordinates.
142    @rtype: tuple
143    '''
144    gdk_window = widget.get_window()
145    origin_z, origin_x, origin_y = gdk_window.get_origin()
146    alloc = widget.get_allocation()
147    x, y, width, height = \
148        alloc.x, alloc.y, alloc.width, alloc.height
149    # TODO-JH: Review this conversion
150    has_window = not(widget.get_has_window())
151    if bool(widget.get_state_flags().value_names) & has_window:
152      origin_x += x
153      origin_y += y
154    return origin_x, origin_y, width, height
155
156  def _onDragDataGet(self, widget, context, selection_data, info, time):
157    '''
158    Data transfer function for drag and drop.
159
160    @param widget: Widget that recieved the signal.
161    @type widget: gtk.Widget
162    @param context: Drag context for this operation
163    @type context: gtk.gdk.DragContext
164    @param selection_data: A selection data object
165    @type selection_data: gtk.SelectionData
166    @param info: An ID of the drag
167    @type info: integer
168    @param time: The timestamp of the drag event.
169    @type time: float
170    '''
171    self.dest_type = info
172    selection_data.set(selection_data.get_target(), 8, '')
173
174  def _onDragEnd(self, widget, drag_context):
175    '''
176    Callback for completed drag operation.
177
178    @param widget: Widget that recieved the signal.
179    @type widget: gtk.Widget
180    @param drag_context: Drag context for this operation
181    @type drag_context: gtk.gdk.DragContext
182    '''
183    if self.dest_type == self.TARGET_PLUGINVIEW:
184      return
185    index = self.get_current_page()
186    child = self.get_nth_page(index)
187    self.emit('plugin_drag_end', child)
188
189  def getPlugins(self):
190    '''
191    Return list of plugins in given view. Filter out tabs that are not plugins.
192
193    @return: Plugins in given view.
194    @rtype: List of {Plugin}
195    '''
196    return [x for x in self.get_children() if isinstance(x, Plugin)]
197
198  def insert_page(self, child, tab_label=None, position=-1):
199    '''
200    Override gtk.Notebook's method. Use the plugin's name or widget's name
201    as default tab label. Keep message tab as first tab.
202
203    @param child: Child widget to insert.
204    @type child: gtk.Widget
205    @param tab_label: Label to use. Plugin name will be used by default.
206    @type tab_label: gtk.Widget
207    @param position: Position to insert child.
208    @type position: integer
209    '''
210    if position != -1 and  \
211          isinstance(self.get_nth_page(0), MessageManager.MessageTab):
212      position += 1
213    if tab_label:
214      name = tab_label
215    elif isinstance(child, Plugin):
216      name = getattr(child, 'plugin_name_localized', None) or child.plugin_name
217    elif child.get_name():
218      name = child.get_name()
219    gtk.Notebook.append_page(self, child, None)
220    gtk.Notebook.reorder_child(self, child, position)
221    gtk.Notebook.set_tab_label(self, child, gtk.Label.new(name))
222
223  def append_page(self, child, tab_label=None):
224    '''
225    Override gtk.Notebook's method. Use the plugin's name or widget's name
226    as default tab label. Keep message tab as first tab.
227
228    @param child: Child widget to insert.
229    @type child: gtk.Widget
230    @param tab_label: Label to use. Plugin name will be used by default.
231    @type tab_label: gtk.Widget
232    '''
233    self.insert_page(child, tab_label, -1)
234
235  def prepend_page(self, child, tab_label=None):
236    '''
237    Override gtk.Notebook's method. Use the plugin's name or widget's name
238    as default tab label. Keep message tab as first tab.
239
240    @param child: Child widget to insert.
241    @type child: gtk.Widget
242    @param tab_label: Label to use. Plugin name will be used by default.
243    @type tab_label: gtk.Widget
244    '''
245    self.insert_page(child, tab_label, 0)
246
247  def focusTab(self, tab_num):
248    '''
249    Set focus on given tab number.
250
251    @param tab_num: Index within visible tabs.
252    @type tab_num: integer
253    '''
254    children = self.get_children()
255    shown_children = [x for x in children if x.get_property('visible')]
256    try:
257      child = shown_children[tab_num]
258    except IndexError:
259      return
260    self.set_current_page(children.index(child))
261    self.grab_focus()
262
263  def getNVisiblePages(self):
264    '''
265    Get number of visible children.
266
267    @return: Number of visible children.
268    @rtype: integer
269    '''
270    shown_children = [x for x in self.get_children() if x.get_property('visible')]
271    return len(shown_children)
272
273class PluginViewWindow(gtk.Window, ToolsAccessor):
274  '''
275  Standalone window with a plugin view.
276
277  @ivar plugin_view: Embedded plugin view.
278  @type plugin_view: L{PluginView}
279  '''
280  def __init__(self, view_name):
281    '''
282    Initialize a new plugin view window.
283
284    @param view_name: The name of the view.
285    @type view_name: string
286    '''
287    gtk.Window.__init__(self)
288    self.plugin_view = PluginView(view_name)
289    self.add(self.plugin_view)
290
291    gspath = NEWPLUGVIEWS_PATH + view_name.lower().replace(' ', '-') + '/'
292    gsettings = GSettings.new_with_path(NEWPLUGVIEWS_GSCHEMA, gspath)
293    width = gsettings.get_int('width')
294    height = gsettings.get_int('height')
295    self.set_default_size(width, height)
296    self.connect('key_press_event', self._onKeyPress)
297    self.plugin_view.connect_after('page_removed', self._onPluginRemoved)
298    self.set_title(view_name)
299    self.set_position(gtk.WindowPosition.MOUSE)
300    self.show_all()
301    self.connect('size-allocate', self._onResize)
302
303  def _onResize(self, widget, allocation):
304    '''
305    Callback for window resizing. Used for persisting view sizes across
306    sessions.
307
308    @param widget: Window widget.
309    @type widget: gtk.Widget
310    @param allocation: The new allocation.
311    @type allocation: gtk.gdk.Rectangle
312    '''
313    view_name = self.plugin_view.view_name
314    gspath = NEWPLUGVIEWS_PATH + view_name.lower().replace(' ', '-') + '/'
315    gsettings = GSettings.new_with_path(NEWPLUGVIEWS_GSCHEMA, gspath)
316    gsettings.set_int('width', self.get_allocated_width())
317    gsettings.set_int('height', self.get_allocated_height())
318
319  def _onPluginRemoved(self, pluginview, page, page_num):
320    '''
321    Callback for removed tabs. If there are no plugins in a stand alone view,
322    destroy it.
323
324    @param pluginview: Plugin view that emitted signal.
325    @type pluginview: L{PluginView}
326    @param page: Child that has been removed.
327    @type page: gtk.Widget
328    @param page_num: Tab index of child.
329    @type page_num: integer
330    '''
331    if pluginview.get_n_pages() == 0:
332      self.destroy()
333
334  def _onKeyPress(self, widget, event):
335    '''
336    Callback for keypresses in window. Enables alt-<num> tab switching.
337
338    @param widget: Window widget.
339    @type widget: gtk.Widget
340    @param event: Event object
341    @type event: gtk.gdk.Event
342    '''
343    if event.state & gdk.ModifierType.MOD1_MASK and \
344          event.keyval in range(gdk.keyval_from_name('0'),
345                                 gdk.keyval_from_name('9')):
346      tab_num = event.keyval - gdk.keyval_from_name('0') or 10
347      pages_count = self.plugin_view.get_n_pages()
348      if pages_count >= tab_num:
349        self.plugin_view.focusTab(tab_num - 1)
350
351
352class ViewManager(object):
353  '''
354  Manage plugins and their views.
355  '''
356  def __init__(self, *perm_views):
357    '''
358    Initialize view manager.
359
360    @param perm_views: List of permanent views, at least one is required.
361    @type perm_views: list of {PluginView}
362    '''
363    self._perm_views = perm_views
364    gsettings = GSettings.new(PLUGVIEWS_GSCHEMA)
365    single = gsettings.get_boolean('layout-single')
366    self._initViewModel(single)
367    self._setupActions()
368
369  def _setupActions(self):
370    '''
371    Sets up actions related to plugin layout.
372    '''
373    single = isinstance(self._view_model, SingleViewModel)
374    layout_action_group = gtk.ActionGroup.new('PluginActions')
375    ui_manager.uimanager.insert_action_group(layout_action_group, 0)
376    layout_action_group.add_toggle_actions(
377      [('SingleViewMode', None, _('_Single plugins view'), '<Control>t',
378        None, self._onSingleViewToggled, single)])
379
380    for action in layout_action_group.list_actions():
381      merge_id = ui_manager.uimanager.new_merge_id()
382      action_name = action.get_name()
383      ui_manager.uimanager.add_ui(merge_id, ui_manager.PLUGIN_LAYOUT_PATH,
384                                  action_name, action_name,
385                                  gtk.UIManagerItemType.MENUITEM, True)
386
387
388  def _onSingleViewToggled(self, action, data=None):
389    '''
390    Callback for single view toggle action.
391
392    @param action: Action object that emitted callback.
393    @type action: gtk.ToggleAction
394    '''
395    self.setSingleMode(action.get_active())
396
397  def setSingleMode(self, single):
398    '''
399    Toggle single mode on or off.
400
401    @param single: True if we want single mode.
402    @type single: boolean
403    '''
404    if isinstance(self._view_model, SingleViewModel) == single:
405      return
406    gsettings = GSettings.new(PLUGVIEWS_GSCHEMA)
407    gsettings.set_boolean('layout-single', single)
408    plugins = self._view_model.getViewedPlugins()
409    self._view_model.close()
410    del self._view_model
411    for plugin in plugins:
412      if plugin.get_parent():
413        plugin.get_parent().remove(plugin)
414    self._initViewModel(single)
415    for plugin in plugins:
416      self._view_model.addElement(plugin)
417
418  def _initViewModel(self, single):
419    '''
420    Initialize a view model, either multi view or single view.
421
422    @param single: True if we want single mode.
423    @type single: boolean
424    '''
425    if single:
426      self._view_model = SingleViewModel(*self._perm_views)
427    else:
428      self._view_model = MultiViewModel(*self._perm_views)
429
430  def addElement(self, element):
431    '''
432    Add an element to a plugin view.
433
434    @param element: The element to be added to a view.
435    @type element: gtk.Widget
436    '''
437    self._view_model.addElement(element)
438
439  def close(self):
440    '''
441    Do cleanup.
442    '''
443    self._view_model.close()
444
445  def initialView(self):
446    '''
447    Do something when view/s are first displayed.
448    '''
449    self._view_model.initialView()
450
451  def giveElementFocus(self, element):
452    '''
453    Give focus to given element (ie. a plugin)
454
455    @param element: The element to give focus to.
456    @type element: gtk.Widget
457    '''
458    self._view_model.giveElementFocus(element)
459
460  def changeView(self, plugin, new_view_name):
461    '''
462    Put a plugin instance in a different view.
463
464    @param plugin: Plugin to move.
465    @type plugin: L{Plugin}
466    @param new_view_name: New view name.
467    @type new_view_name: string
468    '''
469    self._view_model.changeView(plugin, new_view_name)
470
471  def getViewNameForPlugin(self, plugin_name):
472    '''
473    Get the view name for a given plugin name.
474
475    @param plugin_name: Plugin's name to lookup view for.
476    @type plugin_name: string
477
478    @return: View name for plugin.
479    @rtype: string
480    '''
481    return self._view_model.getViewNameForPlugin(plugin_name)
482
483  def Menu(self, context_plugin, transient_window):
484    '''
485    Return a context menu for the given plugin with view manipulation options.
486
487    @param context_plugin: Subject plugin of this menu.
488    @type context_plugin: L{Plugin}
489    @param transient_window: Transient parent window. Used for keeping the
490    new view dialog modal.
491    @type transient_window: gtk.Window
492
493    @return: A Menu widget
494    @rtype: gtk.Menu
495    '''
496    return self._view_model.Menu(context_plugin, transient_window)
497
498class BaseViewModel(ToolsAccessor):
499  '''
500  Base class for views model
501
502  @ivar perm_views: List of permanent views.
503  @type perm_views: list of L{PluginView}
504  @ivar main_view: Main view.
505  @type main_view: L{PluginView}
506  '''
507  def __init__(self, *perm_views):
508    '''
509    Initialize view model.
510
511    @param perm_views: List of permanent views, at least one is required.
512    @type perm_views: list of {PluginView}
513    '''
514    if len(perm_views) == 0:
515      raise TypeError('View model needs at least one permanent view')
516    self.perm_views = perm_views
517    self.main_view = perm_views[0]
518
519  def addElement(self, element):
520    '''
521    Add an element to a plugin view. If the element is a message tab, put it as
522    the first tab in the main view. If the element is a plugin, check if it's
523    placement is cached in this instance or read it's position from gsettings.
524    By default a plugin is appended to the main view.
525
526    @param element: The element to be added to a view.
527    @type element: gtk.Widget
528    '''
529    if isinstance(element, Plugin):
530      self.addPlugin(element)
531    elif isinstance(element, MessageManager.MessageTab):
532      element.connect('show', Proxy(self._onMessageTabShow))
533      element.hide()
534      self.main_view.insert_page(element, 0)
535      self.main_view.set_tab_detachable(element, False)
536      self.main_view.set_tab_reorderable(element, False)
537
538  def addPlugin(self, plugin):
539    '''
540    Add a plugin to the view. Check if it's placement is cached in this
541    instance or read it's position from gsettings. By default a plugin is
542    appended to the main view.
543
544    @param plugin: Plugin to add.
545    @type plugin: L{Plugin}
546    '''
547    pass
548
549  def close(self):
550    '''
551    Do cleanup.
552    '''
553    pass
554
555  def initialView(self):
556    '''
557    Do something when view/s are first displayed.
558    '''
559    pass
560
561  def giveElementFocus(self, element):
562    '''
563    Give focus to given element (ie. a plugin)
564
565    @param element: The element to give focus to.
566    @type element: gtk.Widget
567    '''
568    if not getattr(element, 'parent', None):
569      return
570    view = element.parent
571    page_num = view.page_num(element)
572    view.set_current_page(page_num)
573    view.set_focus_child(element)
574
575  def _onMessageTabShow(self, message_tab):
576    '''
577    Callback for when a message tab appears. Give it focus.
578
579    @param message_tab: Message tab that just appeared.
580    @type message_tab: L{MessageManager.MessageTab}
581    '''
582    self.giveElementFocus(message_tab)
583
584  def changeView(self, plugin, new_view_name):
585    '''
586    Put a plugin instance in a different view.
587
588    @param plugin: Plugin to move.
589    @type plugin: L{Plugin}
590    @param new_view_name: New view name.
591    @type new_view_name: string
592    '''
593    pass
594
595  def getViewNameForPlugin(self, plugin_name):
596    '''
597    Get the view name for a given plugin name.
598
599    @param plugin_name: Plugin's name to lookup view for.
600    @type plugin_name: string
601
602    @return: View name for plugin.
603    @rtype: string
604    '''
605    pass
606
607  def getViewedPlugins(self):
608    '''
609    Get a list of all viewed plugins that are in the managed view/s.
610    '''
611    pass
612
613  def Menu(self, context_plugin, transient_window):
614    '''
615    Return a context menu for the given plugin with view manipulation options.
616
617    @param context_plugin: Subject plugin of this menu.
618    @type context_plugin: L{Plugin}
619    @param transient_window: Transient parent window. Used for keeping the
620    new view dialog modal.
621    @type transient_window: gtk.Window
622
623    @return: A Menu widget
624    @rtype: gtk.Menu
625    '''
626    return gtk.Menu()
627
628class SingleViewModel(BaseViewModel):
629  def addPlugin(self, plugin):
630    '''
631    Add a given plugin to our single view in alphabetical order.
632
633    @param plugin: Plugin to add
634    @type plugin: L{Plugin}
635    '''
636    plugin_names = \
637        [p.plugin_name.lower() for p in self.main_view.getPlugins()]
638    plugin_names.append(plugin.plugin_name.lower())
639    plugin_names.sort()
640    index = plugin_names.index(plugin.plugin_name.lower())
641    self.main_view.insert_page(plugin, position=index)
642    self.main_view.set_tab_detachable(plugin, False)
643    self.main_view.set_tab_reorderable(plugin, False)
644    plugin.show_all()
645  def initialView(self):
646    '''
647    Set tab to first tab.
648    '''
649    self.main_view.set_current_page(0)
650  def getViewedPlugins(self):
651    '''
652    Get all managed plugins.
653
654    @return: list of all managed plugins.
655    @rtype: list of PLugin
656    '''
657    return self.main_view.getPlugins()
658
659class MultiViewModel(list, BaseViewModel):
660  '''
661  Manages all plugin views. Implements a gtk.ListStore of all views.
662  Persists plugin view placements across sessions.
663
664  @cvar COL_NAME: View name column ID.
665  @type COL_NAME: integer
666  @cvar COL_INSTANCE: View instance column ID.
667  @type COL_INSTANCE: integer
668
669  @ivar perm_views: List of permanent views.
670  @type perm_views: list of L{PluginView}
671  @ivar main_view: Main view.
672  @type main_view: L{PluginView}
673  @ivar _ignore_insertion: A list of tuples with view and plugin names that
674  should be ignored and not go in to gsettings. This is to avoid recursive
675  gsettings modification.
676  @type _ignore_insertion: list of tuples
677  @ivar _placement_cache: A cache of recently disabled plugins with their
678  placement. allowsthem to be enabled in to the same position.
679  @type _placement_cache: dictionary
680  @ivar _closed: Indicator to stop writing plugin remove events to gsettings.
681  @type _closed: boolean
682  '''
683  COL_NAME = 0
684  COL_INSTANCE = 1
685  def __init__(self, *perm_views):
686    '''
687    Initialize view manager.
688
689    @param perm_views: List of permanent views, at least one is required.
690    @type perm_views: list of {PluginView}
691    '''
692    BaseViewModel.__init__(self, *perm_views)
693    for view in self.perm_views:
694      self.append(view)
695      self._connectSignals(view)
696    self._ignore_insertion = []
697    self._placement_cache = {}
698    self._closed = False
699
700  def close(self):
701    '''
702    Stops gsettings maniputaion.
703    '''
704    self._closed = True
705
706  def getViewNameForPlugin(self, plugin_name):
707    '''
708    Get the view name for a given plugin name as defined in gsettings.
709    Or return name of main view.
710
711    @param plugin_name: Plugin's name to lookup view for.
712    @type plugin_name: string
713
714    @return: View name for plugin.
715    @rtype: string
716    '''
717    plugin_layouts = self._getPluginLayouts()
718    for view_name in plugin_layouts:
719      if plugin_name in plugin_layouts[view_name]:
720        return view_name
721    return self.main_view.view_name
722
723  def _getViewByName(self, view_name):
724    '''
725    Return the view instance of the given name.
726
727    @param view_name: Name of view to retrieve.
728    @type view_name: string
729
730    @return: View instance or None
731    @rtype: L{PluginView}
732    '''
733    for view in self:
734      if view.view_name == view_name:
735        return view
736    return None
737
738  def _onPluginDragEnd(self, view, plugin):
739    '''
740    Callback for the end of a drag operation of a plugin. Only is called
741    when the drag ends on the root window.
742
743    @param view: Current plugin's view.
744    @type view: L{PluginView}
745    @param plugin: Plugin that was dragged.
746    @type plugin: L{Plugin}
747    '''
748    new_view = self._newView()
749    view.remove(plugin)
750    new_view.append_page(plugin)
751    new_view.set_tab_detachable(plugin, True)
752    new_view.set_tab_reorderable(plugin, True)
753
754  def _newView(self, view_name=None):
755    '''
756    Creates a new view.
757
758    @param view_name: An optional view name. Gives a more mundane one if no
759    name is provided.
760    @type view_name: string
761
762    @return: New view
763    @rtype: L{PluginView}
764    '''
765    if not view_name:
766      view_name = _('Plugin View')
767      view_num = 2
768      while view_name in self._getViewNames():
769        view_name = _('Plugin View (%d)') % view_num
770        view_num += 1
771    w = PluginViewWindow(view_name)
772    view = w.plugin_view
773    self._connectSignals(view)
774    self.append(view)
775    return view
776
777  def _getViewOrNewView(self, view_name):
778    '''
779    Get an existing or new view with the current name.
780
781    @param view_name: View's name
782    @type view_name: string
783
784    @return: New or existing view.
785    @rtype: L{PluginView}
786    '''
787    view = self._getViewByName(view_name) or self._newView(view_name)
788    return view
789
790  def _onViewDelete(self, view_window, event):
791    '''
792    Callback for a view window's delete event. Puts all orphaned plugins
793    in main view.
794
795    @param view_window: View window that emitted delete event.
796    @type view_window: L{PluginViewWindow}
797    @param event: Event object.
798    @type event: gtk.gdk.Event
799    '''
800    view = view_window.plugin_view
801    for child in view.getPlugins():
802      view.remove(child)
803      self.main_view.append_page(child)
804    self._removeView(view)
805
806  def _removeView(self, view):
807    '''
808    Removes view from model.
809
810    @param view: View to remove.
811    @type view: L{PluginView}
812    '''
813    if view in self.perm_views:
814      return
815    if view in self:
816      self.remove(view)
817
818  def _onTabPopupMenu(self, view, event, plugin):
819    '''
820    Callback for popup menu signal from plugin view. Displays a context menu
821    with available views.
822
823    @param view: Plugin view that emitted this signal.
824    @type view: L{PluginView}
825    @param event: Relevant event object that will be used in popup menu.
826    @type event: gtk.gdk.Event
827    @param plugin: Plugin of tab that was clicked or pressed.
828    @type plugin: L{Plugin}
829    '''
830    menu = self.Menu(plugin, view.get_toplevel())
831    if hasattr(event, 'button'):
832      menu.popup(None, None, None, None, event.button, event.time)
833    else:
834      tab = view.get_tab_label(plugin)
835      x, y, w, h = view.getTabAlloc(tab)
836      rect = gdk.Rectangle(x, y, w, h)
837      menu.popup(None, None,
838                 lambda m, r: (r.x+r.width/2, r.y+r.height/2, True),
839                 rect, 0, event.time)
840
841  def _connectSignals(self, view):
842    '''
843    Convenience function for connecting all needed signal callbacks.
844
845    @param view: Plugin view to connect.
846    @type view: :{PluginView}
847    '''
848    if isinstance(view.get_parent(), PluginViewWindow):
849      view.get_parent().connect('delete-event', Proxy(self._onViewDelete))
850    view.connect('plugin-drag-end', Proxy(self._onPluginDragEnd))
851    view.connect('tab-popup-menu', Proxy(self._onTabPopupMenu))
852    view.connect('page-added', Proxy(self._onViewLayoutChanged), 'added')
853    view.connect('page-removed', Proxy(self._onViewLayoutChanged), 'removed')
854    view.connect('page-reordered', Proxy(self._onViewLayoutChanged), 'reordered')
855
856  def _onViewLayoutChanged(self, view, plugin, page_num, action):
857    '''
858    Callback for all layout changes. Updates gsettings.
859
860    @param view: View that emitted the signal.
861    @type view: L{PluginView}
862    @param plugin: Plugin that moved.
863    @type plugin: L{Plugin}
864    @param page_num: Plugin's position in view.
865    @type page_num: integer
866    @param action: Action that triggered this event.
867    @type action: string
868    '''
869    if self._closed or not isinstance(plugin, Plugin): return
870    if (view.view_name, plugin.plugin_name) in self._ignore_insertion:
871      self._ignore_insertion.remove((view.view_name, plugin.plugin_name))
872      return
873    if plugin.plugin_name in self._placement_cache:
874      self._placement_cache.pop(plugin.plugin_name)
875
876    plugin_layouts = self._getPluginLayouts()
877    try:
878      plugin_layout = plugin_layouts[view.view_name]
879    except KeyError:
880      plugin_layouts[view.view_name] = []
881      plugin_layout = plugin_layouts[view.view_name]
882    if plugin.plugin_name in plugin_layout:
883        plugin_layout.remove(plugin.plugin_name)
884    if action in ('reordered', 'added'):
885      plugin_layout.insert(page_num, plugin.plugin_name)
886    elif action == 'removed':
887      self._placement_cache[plugin.plugin_name] = (view.view_name, page_num)
888    if len(plugin_layout) == 0:
889      self._removeView(view)
890
891    self._setPluginLayouts(plugin_layouts)
892
893  def _setPluginLayouts(self, plugin_layouts):
894    self.plugviews = GSettings.new(PLUGVIEWS_GSCHEMA)
895    self.plugviews.set_strv('top-panel-layout', plugin_layouts.pop('Top panel'))
896    self.plugviews.set_strv('bottom-panel-layout', plugin_layouts.pop('Bottom panel'))
897
898    for plugview in list(plugin_layouts.keys()):
899      gspath = NEWPLUGVIEWS_PATH + plugview.lower().replace(' ', '-') + '/'
900      newview = GSettings.new_with_path(NEWPLUGVIEWS_GSCHEMA, gspath)
901      newview.set_strv('layout', plugin_layouts[plugview])
902      l = self.plugviews.get_strv('available-newviews')
903      l.append(plugview)
904      self.plugviews.set_strv('available-newviews', l)
905
906  def _getPluginLayouts(self):
907    plugin_layouts= {}
908    self.plugviews = GSettings.new(PLUGVIEWS_GSCHEMA)
909    plugin_layouts['Top panel'] = self.plugviews.get_strv('top-panel-layout')
910    plugin_layouts['Bottom panel'] = self.plugviews.get_strv('bottom-panel-layout')
911
912    for plugview in self.plugviews.get_strv('available-newviews'):
913      gspath = NEWPLUGVIEWS_PATH + plugview.lower().replace(' ', '-') + '/'
914      newview = GSettings.new_with_path(NEWPLUGVIEWS_GSCHEMA, gspath)
915      layout = newview.get_strv('layout')
916      if layout:
917        plugin_layouts[plugview] = layout
918      else:
919        l = self.plugviews.get_strv('available-newviews')
920        l.remove(plugview)
921        self.plugviews.set_strv('available-newviews', l)
922    return plugin_layouts
923
924
925  def addPlugin(self, plugin):
926    '''
927    Add a plugin to the view. Check if it's placement is cached in this
928    instance or read it's position from gsettings. By default a plugin is
929    appended to the main view.
930
931    @param plugin: Plugin to add.
932    @type plugin: L{Plugin}
933    '''
934    if plugin.plugin_name in self._placement_cache:
935      view_name, index = self._placement_cache.pop(plugin.plugin_name)
936      view = self._getViewOrNewView(view_name)
937    else:
938      view_name = self.getViewNameForPlugin(plugin.plugin_name)
939      view = self._getViewOrNewView(view_name)
940      plugin_layouts = self._getPluginLayouts()
941      try:
942        plugin_layout = plugin_layouts[view.view_name]
943      except KeyError:
944        plugin_layout = []
945        plugin_layouts[view.view_name] = plugin_layout
946      index = -1
947      if plugin.plugin_name in plugin_layout:
948        # The plugins that have a higher index.
949        successive = plugin_layout[plugin_layout.index(plugin.plugin_name)+1:]
950        for child_index, preceding_plugin in enumerate(view.getPlugins()):
951          if preceding_plugin.plugin_name in successive:
952            # Place new plugin just before the first successive plugin.
953            index = child_index
954            break
955      self._ignore_insertion.append((view.view_name, plugin.plugin_name))
956      self._setPluginLayouts(plugin_layouts)
957
958    view.insert_page(plugin, position=index)
959    view.set_tab_detachable(plugin, True)
960    view.set_tab_reorderable(plugin, True)
961    plugin.show_all()
962
963  def initialView(self):
964    '''
965    Set the current tab of all views to be the first one.
966    Used when Accercier first starts.
967    '''
968    for view in self:
969      view.set_current_page(0)
970
971  def getViewedPlugins(self):
972    '''
973    Get all plugins from all views.
974    '''
975    rv = []
976    for view in self:
977      rv.extend(view.getPlugins())
978    return rv
979
980  def _getViewNames(self):
981    '''
982    Get a list of all managed view names.
983
984    @return: A list of view names.
985    @rtype: list of string
986    '''
987    return [view.view_name for view in self]
988
989  def changeView(self, plugin, new_view_name):
990    '''
991    Put a plugin instance in a different view. If given view name does not
992    exist, create it.
993
994    @param plugin: Plugin to move.
995    @type plugin: L{Plugin}
996    @param new_view_name: New view name.
997    @type new_view_name: string
998    '''
999    if not plugin or not isinstance(plugin, gtk.Widget): return
1000    old_view = plugin.get_parent()
1001    new_view = self._getViewOrNewView(new_view_name)
1002    if old_view is not new_view:
1003      old_view.remove(plugin)
1004      new_view.append_page(plugin)
1005      new_view.set_tab_detachable(plugin, True)
1006      new_view.set_tab_reorderable(plugin, True)
1007
1008  def Menu(self, context_plugin, transient_window):
1009    '''
1010    Helps emulate a non-static inner class. These don't exist in python,
1011    I think.
1012
1013    @param context_plugin: Subject plugin of this menu.
1014    @type context_plugin: L{Plugin}
1015    @param transient_window: Transient parent window. Used for keeping the
1016    new view dialog modal.
1017    @type transient_window: gtk.Window
1018
1019    @return: An inner menu class.
1020    @rtype: L{ViewManager._Menu}
1021    '''
1022    return self._Menu(self, context_plugin, transient_window)
1023
1024  class _Menu(gtk.Menu):
1025    '''
1026    Implements a popup menu for a plugin that will allow putting the plugin in
1027    a different view.
1028
1029    @cvar RADIO_GROUP: Radio menu item's group id.
1030    @type RADIO_GROUP: integer
1031
1032    @ivar view_manager: View manager to use as data model and controller.
1033    @type view_manager: L{ViewManager}
1034    '''
1035    RADIO_GROUP = 13
1036    def __init__(self, view_manager, context_plugin, transient_window):
1037      '''
1038      Initialize menu.
1039
1040      @param view_manager: View manager to use as data model and controller.
1041      @type view_manager: L{ViewManager}
1042      @param context_plugin: Subject plugin of this menu.
1043      @type context_plugin: L{Plugin}
1044      @param transient_window: Transient parent window. Used for keeping the
1045      new view dialog modal.
1046      @type transient_window: gtk.Window
1047      '''
1048      gtk.Menu.__init__(self)
1049      self.view_manager = view_manager
1050      if isinstance(context_plugin, gtk.Widget):
1051        self._buildMenu(context_plugin, transient_window)
1052
1053    def _buildMenu(self, context_plugin, transient_window):
1054      '''
1055      Build the menu according to the view managers model.
1056
1057      @param context_plugin: Subject plugin of this menu.
1058      @type context_plugin: L{Plugin}
1059      @param transient_window: Transient parent window. Used for keeping the
1060      new view dialog modal.
1061      @type transient_window: gtk.Window
1062      '''
1063      menu_item = None
1064      for view in self.view_manager:
1065        menu_item = gtk.RadioMenuItem(label = _(view.view_name))
1066        menu_item.set_name(view.view_name)
1067        menu_item.connect('toggled', self._onItemToggled, view, context_plugin)
1068        menu_item.set_active(view == context_plugin.get_parent())
1069        self.append(menu_item)
1070        menu_item.show()
1071      menu_item = gtk.SeparatorMenuItem()
1072      self.append(menu_item)
1073      menu_item.show()
1074      menu_item = gtk.MenuItem(label="<i>" + _('_New view…') + "</i>")
1075      menu_item.get_child().set_use_markup(True)
1076      menu_item.connect('activate', self._onItemActivated,
1077                        context_plugin, transient_window)
1078      self.append(menu_item)
1079      menu_item.show()
1080
1081    def _onItemToggled(self, menu_item, view, context_plugin):
1082      '''
1083      Callback for radio item toggles. Change the views accordingly.
1084
1085      @param menu_item: Menu item that was toggled
1086      @type menu_item: gtk.RadioMenuItem
1087      @param view: View that was chosen.
1088      @type view: L{PluginView}
1089      @param context_plugin: Subject plugin of this menu.
1090      @type context_plugin: L{Plugin}
1091      '''
1092      self.view_manager.changeView(context_plugin, view.view_name)
1093
1094    def _onItemActivated(self, menu_item, context_plugin, transient_window):
1095      '''
1096      Callback for "new view" menu item. Creates a dialog for
1097      entering a view name.
1098
1099      @param menu_item: Menu item that was activated.
1100      @type menu_item: gtk.MenuItem
1101      @param context_plugin: Subject plugin of this menu.
1102      @type context_plugin: L{Plugin}
1103      @param transient_window: Transient parent window. Used for keeping the
1104      new view dialog modal.
1105      @type transient_window: gtk.Window
1106      '''
1107      new_view_dialog = \
1108          self._NewViewDialog(self.view_manager, transient_window)
1109      response_id = new_view_dialog.run()
1110      plugin_name = new_view_dialog.getEntryText()
1111      if response_id == gtk.ResponseType.OK and plugin_name:
1112        self.view_manager.changeView(context_plugin, plugin_name)
1113      new_view_dialog.destroy()
1114
1115    class _NewViewDialog(gtk.Dialog):
1116      '''
1117      Small dialog that allows entry of a new view name.
1118      '''
1119      def __init__(self, view_manager, transient_window):
1120        '''
1121
1122
1123        @param view_manager: View manager to use as data model and controller.
1124        @type view_manager: L{ViewManager}
1125        @param transient_window: Transient parent window. Used for keeping the
1126        new view dialog modal.
1127        @type transient_window: gtk.Window
1128        '''
1129        self.view_manager = view_manager
1130        gtk.Dialog.__init__(self, _('New View…'), transient_window)
1131        self.add_buttons(gtk.STOCK_OK, gtk.ResponseType.OK,
1132                         gtk.STOCK_CLOSE, gtk.ResponseType.CLOSE)
1133        self.set_default_response(gtk.ResponseType.OK)
1134        completion = gtk.EntryCompletion()
1135        complete_model = gtk.ListStore(str)
1136        for view in self.view_manager:
1137          complete_model.append([view.view_name])
1138        completion.set_model(complete_model)
1139        completion.set_text_column(0)
1140        self.entry = gtk.Entry()
1141        self.entry.set_completion(completion)
1142        self.entry.connect('activate', self._onEntryActivate)
1143        self.box = self.get_children()[0]
1144        self.box.add(self.entry)
1145        self.entry.show()
1146
1147      def getEntryText(self):
1148        '''
1149        Get the contents of the entry widget.
1150
1151        @return: Text in entry box.
1152        @rtype: string
1153        '''
1154        return self.entry.get_text()
1155
1156      def _onEntryActivate(self, entry):
1157        '''
1158        Callback for activation of the entry box. Return an OK response.
1159
1160        @param entry: Entry box that was activated.
1161        @type entry: gtk.Entry
1162        '''
1163        self.response(gtk.ResponseType.OK)
1164
1165