root/gaphor/tags/gaphor-0.12.0/gaphor/ui/mainwindow.py

Revision 2100, 19.7 kB (checked in by arj..@yirdis.nl, 1 year ago)

Allow for creation of top-level packages and diagrams.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 """
2 The main application window.
3 """
4
5 import gobject, gtk
6
7 from zope import interface, component
8 from gaphor.interfaces import IActionProvider
9 from interfaces import IUIComponent
10
11 from gaphor import UML
12 from gaphor.core import _, inject, action, radio_action, build_action_group, transactional
13 from namespace import NamespaceModel, NamespaceView
14 from diagramtab import DiagramTab
15 from toolbox import Toolbox
16 from diagramtoolbox import TOOLBOX_ACTIONS
17 from propertyeditor import PropertyEditor
18 from toplevelwindow import ToplevelWindow
19
20
21 from interfaces import IDiagramSelectionChange
22 from gaphor.interfaces import IServiceEvent, IActionExecutedEvent
23 from event import DiagramSelectionChange
24 from gaphor.application import Application
25 from gaphor.services.filemanager import FileManagerStateChanged
26
27 class MainWindow(ToplevelWindow):
28     """
29     The main window for the application.
30     It contains a Namespace-based tree view and a menu and a statusbar.
31     """
32     interface.implements(IActionProvider)
33
34     properties = inject('properties')
35     element_factory = inject('element_factory')
36     action_manager = inject('action_manager')
37     file_manager = inject('file_manager')
38
39     title = 'Gaphor'
40     size = property(lambda s: s.properties.get('ui.window-size', (760, 580)))
41     menubar_path = '/mainwindow'
42     toolbar_path = '/mainwindow-toolbar'
43
44     menu_xml = """
45       <ui>
46         <menubar name="mainwindow">
47           <menu action="file">
48             <placeholder name="primary" />
49             <separator />
50             <menu action="file-export" />
51             <menu action="file-import" />
52             <separator />
53             <placeholder name="secondary" />
54             <placeholder name="ternary" />
55             <separator />
56             <menuitem action="file-quit" />
57           </menu>
58           <menu action="edit">
59             <placeholder name="primary" />
60             <placeholder name="secondary" />
61             <placeholder name="ternary" />
62           </menu>
63           <menu action="diagram">
64             <menuitem action="tree-view-create-diagram" />
65             <menuitem action="tree-view-create-package" />
66             <separator />
67             <menuitem action="tree-view-delete-diagram" />
68             <menuitem action="tree-view-delete-package" />
69             <separator />
70             <placeholder name="primary" />
71             <placeholder name="secondary" />
72             <placeholder name="ternary" />
73           </menu>
74           <menu action="tools">
75             <placeholder name="primary" />
76             <placeholder name="secondary" />
77             <placeholder name="ternary" />
78           </menu>
79           <menu action="window">
80             <placeholder name="primary" />
81             <placeholder name="secondary" />
82             <placeholder name="ternary" />
83           </menu>
84           <menu action="help">
85             <placeholder name="primary" />
86             <placeholder name="secondary" />
87             <placeholder name="ternary" />
88           </menu>
89         </menubar>
90         <toolbar name='mainwindow-toolbar'>
91         </toolbar>
92         <toolbar action="tools">
93         </toolbar>
94         <popup action="namespace-popup">
95           <menuitem action="tree-view-open" />
96           <menuitem action="tree-view-rename" />
97           <separator />
98           <menuitem action="tree-view-create-diagram" />
99           <menuitem action="tree-view-create-package" />
100           <separator />
101           <menuitem action="tree-view-delete-diagram" />
102           <menuitem action="tree-view-delete-package" />
103           <separator />
104           <menuitem action="tree-view-refresh" />
105         </popup>
106       </ui>
107     """
108
109     def __init__(self):
110         ToplevelWindow.__init__(self)
111         # Map tab contents to DiagramTab
112         self.notebook_map = {}
113         # Tree view:
114         self._tree_view = None
115
116         self.action_group = build_action_group(self)
117         for name, label in (('file', '_File'),
118                              ('file-export', '_Export'),
119                              ('file-import', '_Import'),
120                              ('edit', '_Edit'),
121                              ('diagram', '_Diagram'),
122                              ('tools', '_Tools'),
123                              ('window', '_Window'),
124                              ('help', '_Help')):
125             a = gtk.Action(name, label, None, None)
126             a.set_property('hide-if-empty', False)
127             self.action_group.add_action(a)
128         self._tab_ui_settings = None
129
130     tree_model = property(lambda s: s.tree_view.get_model())
131
132     tree_view = property(lambda s: s._tree_view)
133
134     def get_filename(self):
135         """
136         Return the file name of the currently opened model.
137         """
138         return self.file_manager.filename
139
140     def get_current_diagram_tab(self):
141         """
142         Get the currently opened and viewed DiagramTab, shown on the right
143         side of the main window.
144         See also: get_current_diagram(), get_current_diagram_view().
145         """
146         return self.get_current_tab()
147
148     def get_current_diagram(self):
149         """
150         Return the Diagram associated with the viewed DiagramTab.
151         See also: get_current_diagram_tab(), get_current_diagram_view().
152         """
153         tab = self.get_current_diagram_tab()
154         return tab and tab.get_diagram()
155
156     def get_current_diagram_view(self):
157         """
158         Return the DiagramView associated with the viewed DiagramTab.
159         See also: get_current_diagram_tab(), get_current_diagram().
160         """
161         tab = self.get_current_diagram_tab()
162         return tab and tab.get_view()
163
164     def ask_to_close(self):
165         """
166         Ask user to close window.
167         """
168         dialog = gtk.MessageDialog(self.window,
169             gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
170             gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO,
171             _("Quit Gaphor?"))
172         answer = dialog.run()
173         dialog.destroy()
174         return answer == gtk.RESPONSE_YES
175
176     def show_diagram(self, diagram):
177         """
178         Show a Diagram element in a new tab.
179         If a tab is already open, show that one instead.
180         """
181         # Try to find an existing window/tab and let it get focus:
182         for tab in self.get_tabs():
183             if tab.get_diagram() is diagram:
184                 self.set_current_page(tab)
185                 return tab
186
187         tab = DiagramTab(self)
188         tab.set_diagram(diagram)
189         widget = tab.construct()
190         self.add_tab(tab, widget, tab.title)
191         self.set_current_page(tab)
192
193         return tab
194
195     def ui_component(self):
196         """
197         Create the widgets that make up the main window.
198         """
199         model = NamespaceModel(self.element_factory)
200         view = NamespaceView(model, self.element_factory)
201         scrolled_window = gtk.ScrolledWindow()
202         scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
203         scrolled_window.set_shadow_type(gtk.SHADOW_IN)
204         scrolled_window.add(view)
205         view.show()
206        
207         view.connect_after('event-after', self._on_view_event)
208         view.connect('row-activated', self._on_view_row_activated)
209         view.connect_after('cursor-changed', self._on_view_cursor_changed)
210
211         vbox = gtk.VBox()
212         vbox.pack_start(scrolled_window, expand=True)
213         scrolled_window.show()
214
215         paned = gtk.HPaned()
216         paned.set_property('position', 160)
217         paned.pack1(vbox)
218         vbox.show()
219        
220         notebook = gtk.Notebook()
221         notebook.set_scrollable(True)
222         notebook.set_show_border(False)
223
224         notebook.connect_after('switch-page', self._on_notebook_switch_page)
225         notebook.connect_after('page-removed', self._on_notebook_page_removed)
226
227         self.property_editor = PropertyEditor()
228         pe_notebook = self.property_editor.construct()
229         pe_notebook.set_size_request(-1, 50)
230
231         second_paned = gtk.VPaned()
232         second_paned.set_property('position',
233                                  int(self.properties.get('ui.property-editor-position', 600)))
234         second_paned.pack1(notebook)
235         notebook.show()
236         second_paned.pack2(pe_notebook)
237         pe_notebook.show()
238        
239         paned.pack2(second_paned)
240         second_paned.show()
241         paned.show()
242
243         second_paned.connect('notify::position',
244                             self._on_property_editor_notify_position)
245
246         self.notebook = notebook
247         self._tree_view = view
248        
249         vbox.set_border_width(3)
250
251         toolbox = Toolbox(TOOLBOX_ACTIONS)
252         vbox.pack_start(toolbox, expand=False)
253         toolbox.show()
254
255         self._toolbox = toolbox
256
257         return paned
258
259     def construct(self):
260         super(MainWindow, self).construct()
261
262         self.window.connect('delete-event', self._on_window_delete)
263
264         # We want to store the window size, so it can be reloaded on startup
265         self.window.set_property('allow-shrink', True)
266         self.window.connect('size-allocate', self._on_window_size_allocate)
267         self.window.connect('destroy', self._on_window_destroy)
268
269         Application.register_handler(self._action_executed)
270
271     def _update_toolbox(self, action_group):
272         """
273         Update the buttons in the toolbox. Each button should be connected
274         by an action. Each button is assigned a special _action_name_
275         attribute that can be used to fetch the action from the ui manager.
276         """
277         for button in self._toolbox.buttons:
278            
279             action_name = button.action_name
280             action = action_group.get_action(action_name)
281             if action:
282                 action.connect_proxy(button)
283
284     # Notebook methods:
285
286     def add_tab(self, tab_id, contents, label):
287         """
288         Create a new tab on the notebook with window as its contents.
289         Returns: The page number of the tab.
290         """
291         self.notebook_map[contents] = tab_id
292         #contents.connect('destroy', self._on_tab_destroy)
293         l = gtk.Label(label)
294         # Note: append_page() emits switch-page event
295         self.notebook.append_page(contents, l)
296         page_num = self.notebook.page_num(contents)
297         #self.notebook.set_current_page(page_num)
298         return page_num
299
300     def get_current_tab(self):
301         """
302         Return the window (DiagramTab) that is currently visible on the
303         notebook.
304         """
305         current = self.notebook.get_current_page()
306         content = self.notebook.get_nth_page(current)
307         return self.notebook_map.get(content)
308
309     def set_current_page(self, tab):
310         """
311         Force a specific tab (DiagramTab) to the foreground.
312         """
313         for p, t in self.notebook_map.iteritems():
314             if tab is t:
315                 num = self.notebook.page_num(p)
316                 self.notebook.set_current_page(num)
317                 return
318
319     def set_tab_label(self, tab, label):
320         for p, t in self.notebook_map.iteritems():
321             if tab is t:
322                 l = gtk.Label(label)
323                 l.show()
324                 self.notebook.set_tab_label(p, l)
325
326     def get_tabs(self):
327         return self.notebook_map.values()
328
329     def remove_tab(self, tab):
330         """
331         Remove the tab from the notebook. Tab is such a thing as
332         a DiagramTab.
333         """
334         for p, t in self.notebook_map.iteritems():
335             if tab is t:
336                 num = self.notebook.page_num(p)
337                 self.notebook.remove_page(num)
338                 del self.notebook_map[p]
339                 return
340
341     def select_element(self, element):
342         """
343         Select an element from the Namespace view.
344         The element is selected. After this an action may be executed,
345         such as OpenModelElement, which will try to open the element (if it's
346         a Diagram).
347         """
348         path = self.tree_model.path_from_element(element)
349         # Expand the first row:
350         if len(path) > 1:
351             self._tree_view.expand_row(path[:-1], False)
352         selection = self._tree_view.get_selection()
353         selection.select_path(path)
354         self._on_view_cursor_changed(self._tree_view)
355
356     # Signal callbacks:
357
358     @component.adapter(FileManagerStateChanged)
359     def _action_executed(self, event):
360         # We're only interested in file operations
361         print 'action:', event.service
362         if event.service is self.file_manager:
363             filename = self.file_manager.filename
364             if self.window:
365                 if filename:
366                     title = '%s - %s' % (self.title, filename)
367                 else:
368                     title = self.title
369                 self.window.set_title(title)
370
371
372     def _on_window_destroy(self, window):
373         """
374         Window is destroyed... Quit the application.
375         """
376         self._tree_view = None
377         self.window = None
378         if gobject.main_depth() > 0:
379             gtk.main_quit()
380         Application.unregister_handler(self._action_executed)
381
382     def _on_tab_destroy(self, widget):
383         tab = self.notebook_map[widget]
384         assert isinstance(tab, DiagramTab)
385         self.remove_tab(tab)
386
387     def _on_window_delete(self, window = None, event = None):
388         return not self.ask_to_close()
389
390     def _on_view_event(self, view, event):
391         """
392         Show a popup menu if button3 was pressed on the TreeView.
393         """
394         # handle mouse button 3:
395         if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
396             menu = self.ui_manager.get_widget('/namespace-popup')
397             menu.popup(None, None, None, event.button, event.time)
398
399
400     def _on_view_row_activated(self, view, path, column):
401         """
402         Double click on an element in the tree view.
403         """
404         self.action_manager.execute('tree-view-open')
405
406     def _on_view_cursor_changed(self, view):
407         """
408         Another row is selected, execute a dummy action.
409         """
410         element = view.get_selected_element()
411         self.action_group.get_action('tree-view-create-diagram').props.sensitive = isinstance(element, UML.Package)
412         self.action_group.get_action('tree-view-create-package').props.sensitive = isinstance(element, UML.Package)
413
414         self.action_group.get_action('tree-view-delete-diagram').props.visible = isinstance(element, UML.Diagram)
415         self.action_group.get_action('tree-view-delete-package').props.visible = isinstance(element, UML.Package) and not element.presentation
416
417         self.action_group.get_action('tree-view-open').props.sensitive = isinstance(element, UML.Diagram)
418
419     def _insensivate_toolbox(self):
420         for button in self._toolbox.buttons:
421             button.set_property('sensitive', False)
422
423     def _on_notebook_page_removed(self, notebook, tab, page_num):
424         if self._tab_ui_settings:
425             action_group, ui_id = self._tab_ui_settings
426             self.ui_manager.remove_action_group(action_group)
427             self.ui_manager.remove_ui(ui_id)
428             self._insensivate_toolbox()
429             self._tab_ui_settings = None
430             if notebook.get_current_page() != -1:
431                 self._on_notebook_switch_page(notebook, None, notebook.get_current_page())
432
433     def _on_notebook_switch_page(self, notebook, tab, page_num):
434         """
435         Another page (tab) is put on the front of the diagram notebook.
436         A dummy action is executed.
437         """
438         if self._tab_ui_settings:
439             action_group, ui_id = self._tab_ui_settings
440             self.ui_manager.remove_action_group(action_group)
441             self.ui_manager.remove_ui(ui_id)
442             self._tab_ui_settings = None
443
444         content = self.notebook.get_nth_page(page_num)
445         tab = self.notebook_map.get(content)
446         assert isinstance(tab, DiagramTab), str(tab)
447        
448         self.ui_manager.insert_action_group(tab.action_group, -1)
449         ui_id = self.ui_manager.add_ui_from_string(tab.menu_xml)
450         self._tab_ui_settings = tab.action_group, ui_id
451         log.debug('Menus updated with %s, %d' % self._tab_ui_settings)
452         self._update_toolbox(tab.toolbox.action_group)
453
454         # Make sure everyone knows the selection has changed.
455         component.handle(DiagramSelectionChange(tab.view, tab.view.focused_item, tab.view.selected_items))
456
457     def _on_window_size_allocate(self, window, allocation):
458         """
459         Store the window size in a property.
460         """
461         self.properties.set('ui.window-size', (allocation.width, allocation.height))
462
463     def _on_property_editor_notify_position(self, paned, arg):
464         self.properties.set('ui.property-editor-position',
465                      paned.get_position())
466
467     # Actions:
468
469     @action(name='file-quit', stock_id='gtk-quit')
470     def quit(self):
471         self.ask_to_close() and gtk.main_quit()
472
473     @action(name='tree-view-open', label='_Open')
474     def tree_view_open_selected(self):
475         element = self._tree_view.get_selected_element()
476         if isinstance(element, UML.Diagram):
477             self.show_diagram(element)
478         else:
479             log.debug('No action defined for element %s' % type(element).__name__)
480
481     @action(name='tree-view-rename', label=_('Rename'))
482     def tree_view_rename_selected(self):
483         view = self._tree_view
484         element = view.get_selected_element()
485         path = view.get_model().path_from_element(element)
486         column = view.get_column(0)
487         cell = column.get_cell_renderers()[1]
488         cell.set_property('editable', 1)
489         cell.set_property('text', element.name)
490         view.set_cursor(path, column, True)
491         cell.set_property('editable', 0)
492
493     @action(name='tree-view-create-diagram', label=_('_New diagram'), stock_id='gaphor-diagram')
494     @transactional
495     def