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

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

Catch errors that occur inside the namespace model and try a refresh. This prevents the rest of the application from going into an inconsistent state.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 """
2 This is the TreeView that is most common (for example: it is used
3 in Rational Rose). This is a tree based on namespace relationships. As
4 a result only classifiers are shown here.
5 """
6
7 import gobject
8 import gtk
9 import operator
10 import stock
11
12 from zope import component
13
14 from gaphor import UML
15 from gaphor.UML.event import ModelFactoryEvent, FlushFactoryEvent
16 from gaphor.UML.interfaces import IAttributeChangeEvent, IElementCreateEvent, IElementDeleteEvent
17 from gaphor.UML.event import DerivedUnionSetEvent
18 from gaphor.transaction import Transaction
19
20
21 # The following items will not be shown in the treeview, although they
22 # are UML.Namespace elements.
23 _default_filter_list = (
24     UML.Class,
25     UML.Package,
26     UML.Diagram,
27     UML.Profile,
28     UML.Stereotype,
29     UML.Property,
30     UML.Operation
31     )
32
33 # TODO: update tree sorter:
34 # Diagram before Class & Package.
35 # Property before Operation
36 _tree_sorter = operator.attrgetter('name')
37
38
39 def catchall(func):
40     def catchall_wrapper(*args, **kwargs):
41         try:
42             func(*args, **kwargs)
43         except Exception, e:
44             log.error('Exception in %s. Try to refresh the entire model' % (func,), e)
45             try:
46                 args[0].refresh()
47             except Exception, e:
48                 log.error('Failed to refresh')
49            
50     return catchall_wrapper
51
52
53 class NamespaceModel(gtk.GenericTreeModel):
54     """
55     The NamespaceModel holds a view on the data model based on namespace
56     relationships (such as a Package containing a Class).
57
58     NamedElement.namespace[1] -- Namespace.ownedMember[*]
59
60     NOTE: when a model is loaded no IAssociation*Event's are emitted.
61     
62     """
63
64
65     def __init__(self, factory):
66         # Init parent:
67         gtk.GenericTreeModel.__init__(self)
68
69         # We own the references to the iterators.
70         self.set_property ('leak_references', 0)
71
72         self.factory = factory
73
74         self._nodes = { None: [] }
75
76         self.filter = _default_filter_list
77
78         component.provideHandler(self.flush)
79         component.provideHandler(self.refresh)
80         component.provideHandler(self._on_element_change)
81         component.provideHandler(self._on_element_create)
82         component.provideHandler(self._on_element_delete)
83         component.provideHandler(self._on_association_set)
84
85         self._build_model()
86
87     def path_from_element(self, e):
88         if e:
89             ns = e.namespace
90             n = self._nodes.get(ns)
91             if n:
92                 return self.path_from_element(ns) + (n.index(e),)
93             else:
94                 return ()
95         else:
96             return ()
97
98     def element_from_path(self, path):
99         """
100         Get the node form a path. None is returned if no node is found.
101         """
102         try:
103             nodes = self._nodes
104             node = None
105             for index in path:
106                 node = nodes[node][index]
107             return node
108         except IndexError:
109             return None
110
111     @component.adapter(IAttributeChangeEvent)
112     @catchall
113     def _on_element_change(self, event):
114         """
115         Element changed, update appropriate row.
116         """
117         element = event.element
118         if element not in self._nodes:
119             return
120
121         if event.property is UML.Classifier.isAbstract or \
122                 event.property is UML.BehavioralFeature.isAbstract:
123             path = self.path_from_element(element)
124             if path:
125                 self.row_changed(path, self.get_iter(path))
126
127         if event.property is UML.NamedElement.name:
128             try:
129                 path = self.path_from_element(element)
130             except KeyError:
131                 # Element not visible in the tree view
132                 return
133
134             if not path:
135                 return
136             self.row_changed(path, self.get_iter(path))
137             parent_nodes = self._nodes[element.namespace]
138             parent_path = self.path_from_element(element.namespace)
139             if not parent_path:
140                 return
141
142             original = list(parent_nodes)
143             parent_nodes.sort(key=_tree_sorter)
144             if parent_nodes != original:
145                 # reorder the list:
146                 self.rows_reordered(parent_path, self.get_iter(parent_path),
147                                     map(list.index,
148                                         [original] * len(parent_nodes),
149                                         parent_nodes))
150
151     def _add_elements(self, element):
152         """
153         Add a single element.
154         """
155         self._nodes.setdefault(element, [])
156         parent = self._nodes[element.namespace]
157         parent.append(element)
158         parent.sort(key=_tree_sorter)
159         path = self.path_from_element(element)
160         self.row_inserted(path, self.get_iter(path))
161
162         # Add children
163         if isinstance(element, UML.Namespace):
164             for e in element.ownedMember:
165                 if type(e) in self.filter:
166                     self._add_elements(e)
167
168
169     def _remove_element(self, element):
170         """
171         Remove elements from the nodes. No update signal is emitted.
172         """
173         def remove(n):
174             for c in self._nodes.get(n, []):
175                 remove(c)
176             try:
177                 del self._nodes[n]
178             except KeyError:
179                 pass
180
181         remove(element)
182
183
184     @component.adapter(IElementCreateEvent)
185     @catchall
186     def _on_element_create(self, event):
187         element = event.element
188         if event.service is self.factory and \
189                  type(element) in self.filter:
190             self._add_elements(element)
191
192
193     @component.adapter(IElementDeleteEvent)
194     @catchall
195     def _on_element_delete(self, event):
196         element = event.element
197         if event.service is self.factory and \
198                 type(element) in self.filter:
199             path = self.path_from_element(element)
200
201             # Remove all sub-elements:
202             if path:
203                 self.row_deleted(path)
204                 if path[:-1]:
205                     self.row_has_child_toggled(path[:-1], self.get_iter(path[:-1]))
206             self._remove_element(element)
207
208             parent_node = self._nodes.get(element.namespace)
209             if parent_node:
210                 parent_node.remove(element)
211
212 #            if path and parent_node and len(self._nodes[parent_node]) == 0:
213 #                self.row_has_child_toggled(path[:-1], self.get_iter(path[:-1]))
214
215
216     @component.adapter(DerivedUnionSetEvent)
217     @catchall
218     def _on_association_set(self, event):
219
220         element = event.element
221         if type(element) not in self.filter:
222             return
223
224         if event.property is UML.NamedElement.namespace:
225             # Check if the element is actually in the element factory:
226             if element not in self.factory:
227                 return
228
229             old_value, new_value = event.old_value, event.new_value
230
231             # Remove entry from old place
232             if self._nodes.has_key(old_value):
233                 try:
234                     path = self.path_from_element(old_value) + (self._nodes[old_value].index(element),)
235                 except ValueError:
236                     log.error('Unable to create path for element %s and old_value %s' % (element, self._nodes[old_value]))
237                 else:
238                     self._nodes[old_value].remove(element)
239                     self.row_deleted(path)
240                     path = path[:-1] #self.path_from_element(old_value)
241                     if path:
242                         self.row_has_child_toggled(path, self.get_iter(path))
243
244             # Add to new place. This may fail if the type of the new place is
245             # not in the tree model (because it's filtered)
246             if self._nodes.has_key(new_value):
247                 if self._nodes.has_key(element):
248                     parent = self._nodes[new_value]
249                     parent.append(element)
250                     parent.sort(key=_tree_sorter)
251                     path = self.path_from_element(element)
252                     self.row_inserted(path, self.get_iter(path))
253                 elif type(element) in self.filter:
254                     self._add_elements(element)
255             elif self._nodes.has_key(element):
256                 # Non-existent: remove entirely
257                 self._remove_element(element)
258
259     @component.adapter(ModelFactoryEvent)
260     def refresh(self, event=None):
261         self.flush()
262         self._build_model()
263
264     @component.adapter(FlushFactoryEvent)
265     def flush(self, event=None):
266         for n in self._nodes[None]:
267             self.row_deleted((0,))
268         self._nodes = {None: []}
269
270     def _build_model(self):
271         toplevel = self.factory.select(lambda e: isinstance(e, UML.Namespace) and not e.namespace)
272
273         for element in toplevel:
274             self._add_elements(element)
275
276     # TreeModel methods:
277
278     def on_get_flags(self):
279         """
280         Returns the GtkTreeModelFlags for this particular type of model.
281         """
282         return 0
283
284     def on_get_n_columns(self):
285         """
286         Returns the number of columns in the model.
287         """
288         return 1
289
290     def on_get_column_type(self, index):
291         """
292         Returns the type of a column in the model.
293         """
294         return gobject.TYPE_PYOBJECT
295
296     def on_get_path(self, node):
297         """
298         Returns the path for a node as a tuple (0, 1, 1).
299         """
300         path = self.path_from_element(node)
301         return path
302
303     def on_get_iter(self, path):
304         """
305         Returns the node corresponding to the given path.
306         The path is a tuple of values, like (0 1 1). Returns None if no
307         iterator can be created.
308         """
309         return self.element_from_path(path)
310
311     def on_get_value(self, node, column):
312         """
313         Returns the model element that matches 'node'.
314         """
315         assert column == 0, 'column can only be 0'
316         return node
317
318     def on_iter_next(self, node):
319         """
320         Returns the next node at this level of the tree. None if no
321         next element.
322         """
323         try:
324             parent = self._nodes[node.namespace]
325             index = parent.index(node)
326             return parent[index + 1]
327         except IndexError, e:
328             return None
329        
330     def on_iter_has_child(self, node):
331         """
332         Returns true if this node has children, or None.
333         """
334         n = self._nodes.get(node)
335         return n or len(n) > 0
336
337     def on_iter_children(self, node):
338         """
339         Returns the first child of this node, or None.
340         """
341         try:
342             return self._nodes[node][0]
343         except KeyError:
344             pass
345
346     def on_iter_n_children(self, node):
347         """
348         Returns the number of children of this node.
349         """
350         return len(self._nodes[node])
351
352     def on_iter_nth_child(self, node, n):
353         """
354         Returns the nth child of this node.
355         """
356         try:
357             nodes = self._nodes[node]
358             return nodes[n]
359         except TypeError, e:
360             return None
361
362     def on_iter_parent(self, node):
363         """
364         Returns the parent of this node or None if no parent
365         """
366         return node.namespace
367
368
369 class NamespaceView(gtk.TreeView):
370
371     TARGET_STRING = 0
372     TARGET_ELEMENT_ID = 1
373     DND_TARGETS = [
374         ('STRING', 0, TARGET_STRING),
375         ('text/plain', 0, TARGET_STRING),
376         ('gaphor/element-id', 0, TARGET_ELEMENT_ID)]
377     # Can not set signals for some reason...
378     #__gsignals__ = { 'drag-drop': 'override',
379     #                 'drag-data-get': 'override',
380     #                 'drag-data-delete': 'override',
381     #                 'drag-data-received': 'override' }
382
383     def __init__(self, model, factory):
384         assert isinstance (model, NamespaceModel), 'model is not a NamespaceModel (%s)' % str(model)
385         self.__gobject_init__()
386         gtk.TreeView.__init__(self, model)
387         self.factory = factory
388         self.icon_cache = {}
389
390         self.set_property('headers-visible', False)
391         self.set_property('search-column', 0)
392         def search_func(model, column, key, iter, data=None):
393             assert column == 0
394             element = model.get_value(iter, column)
395             if element.name:
396                 return not element.name.startswith(key)
397         self.set_search_equal_func(search_func)
398
399         self.set_rules_hint(True)
400         selection = self.get_selection()
401         selection.set_mode(gtk.SELECTION_BROWSE)
402         column = gtk.TreeViewColumn ('')
403         # First cell in the column is for an image...
404         cell = gtk.CellRendererPixbuf ()
405         column.pack_start (cell, 0)
406         column.set_cell_data_func (cell, self._set_pixbuf, None)
407        
408         # Second cell if for the name of the object...
409         cell = gtk.CellRendererText ()
410         #cell.set_property ('editable', 1)
411         cell.connect('edited', self._text_edited)
412         column.pack_start (cell, 0)
413         column.set_cell_data_func (cell, self._set_text, None)
414
415         assert len (column.get_cell_renderers()) == 2
416         self.append_column (column)
417
418         # DND info:
419         # drag
420         self.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
421                              [NamespaceView.DND_TARGETS[-1]],
422                              gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)
423         self.drag_source_set(gtk.gdk.BUTTON1_MASK | gtk.gdk.BUTTON3_MASK,
424                              NamespaceView.DND_TARGETS,
425                              gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_LINK)
426         self.connect('drag-data-get', NamespaceView.on_drag_data_get)
427
428         # drop
429         #self.drag_dest_set (gtk.DEST_DEFAULT_ALL, [NamespaceView.DND_TARGETS[-1]],
430         #                    gtk.gdk.ACTION_DEFAULT)
431         self.enable_model_drag_dest([NamespaceView.DND_TARGETS[-1]],
432                                     gtk.gdk.ACTION_DEFAULT)
433         self.connect('drag-data-received', NamespaceView.on_drag_data_received)
434         self.connect('drag-drop', NamespaceView.on_drag_drop)
435         self.connect('drag-data-delete', NamespaceView.on_drag_data_delete)
436
437     def get_selected_element(self):
438         selection = self.get_selection()
439         model, iter = selection.get_selected()
440         if not iter:
441             return
442         return model.get_value(iter, 0)
443
444     def expand_root_nodes(self):
445         self.expand_row((0,), False)
446
447     def _set_pixbuf(self, column, cell, model, iter, data):
448         value = model.get_value(iter, 0)
449         try:
450             icon = self.icon_cache[type(value)]
451         except KeyError:
452             stock_id = stock.get_stock_id(type(value))
453             if stock_id:
454                 icon = self.render_icon(stock_id, gtk.ICON_SIZE_MENU, '')
455             else:
456                 icon = None
457             self.icon_cache[type(value)] = icon
458         cell.set_property('pixbuf', icon)
459
460
461     def _set_text(self, column, cell, model, iter, data):
462         """
463         Set font and of model elements in tree view.
464         """
465         value = model.get_value(iter, 0)
466         text = value and (value.name or '').replace('\n', ' ') or '<None>'
467
468         if isinstance(value, UML.Diagram):
469             text = '<b>%s</b>' % text
470         elif (isinstance(value, UML.Classifier)
471                 or isinstance(value, UML.Operation)) and value.isAbstract:
472             text = '<i>%s</i>' % text
473
474         cell.set_property('markup', text)
475
476
477     def _text_edited(self, cell, path_str, new_text):
478         """
479         The text has been edited. This method updates the data object.
480         Note that 'path_str' is a string where the fields are separated by
481         colons ':', like this: '0:1:1'. We first turn them into a tuple.
482         """
483         try:
484             model = self.get_property('model')
485             iter = model.get_iter_from_string(path_str)
486             element = model.get_value(iter, 0)
487             tx = Transaction()
488             element.name = new_text
489             tx.commit()
490         except Exception, e:
491             log.error('Could not create path from string "%s"' % path_str)
492
493 #    def do_drag_begin (self, context):
494 #        print 'do_drag_begin'
495
496     def on_drag_data_get(self, context, selection_data, info, time):
497         """
498         Get the data to be dropped by on_drag_data_received().
499         We send the id of the dragged element.
500         """
501         #log