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