root/gaphor/tags/gaphor-0.12.5/gaphor/adapters/propertypages.py

Revision 2124, 40.3 kB (checked in by wrobe..@pld-linux.org, 1 year ago)

- merged changes 2120:2122 from trunk which fix bug #78

Line 
1 """
2 Adapters for the Property Editor
3
4 # TODO: make all labels align top-left
5 # Add hidden columns for list stores where i can put the actual object
6 # being edited.
7
8 TODO:
9  - stereotypes
10  - association / association ends.
11  - Follow HIG guidelines:
12    * Leave a 12-pixel border between the edge of the window and
13      the nearest controls.
14    * Leave a 12-pixel horizontal gap between a control and its label. (The gap
15      may be bigger for other controls in the same group, due to differences in
16      the lengths of the labels.)
17    * Labels must be concise and make sense when taken out of context.
18      Otherwise, users relying on screenreaders or similar assistive
19      technologies will not always be able to immediately understand the
20      relationship between a control and those surrounding it.
21    * Assign access keys to all editable controls. Ensure that using the access
22      key focuses its associated control.
23  
24 """
25
26 import gtk
27 from gaphor.core import _, inject, transactional
28 from gaphor.application import Application
29 from gaphor.ui.interfaces import IPropertyPage
30 from gaphor.diagram import items
31 from zope import interface, component
32 from gaphor import UML
33 from gaphor.UML.interfaces import IAttributeChangeEvent
34 from gaphor.UML.umllex import parse_attribute, render_attribute
35 import gaphas.item
36
37
38 class EditableTreeModel(gtk.ListStore):
39     """
40     Editable GTK tree model based on ListStore model.
41
42     Every row is represented by a list of editable values. Last column
43     contains an object, which is being edited (this column is not
44     displayed). When editable value in first column is set to empty string
45     then object is deleted.
46
47     Last row is empty and contains no object to edit. It allows to enter
48     new values.
49
50     When model is edited, then item is requested to be updated on canvas.
51
52     Attributes:
53     - _item: diagram item owning tree model
54     """
55     element_factory = inject('element_factory')
56
57     def __init__(self, item, cols=None):
58         """
59         Create new model.
60
61         Parameters:
62         - _item: diagram item owning tree model
63         - cols: model columns, defaults to [str, object]
64         """
65         if cols is None:
66             cols = (str, object)
67         super(EditableTreeModel, self).__init__(*cols)
68         self._item = item
69
70         for data in self._get_rows():
71             self.append(data)
72         self._add_empty()
73
74
75     def _get_rows(self):
76         """
77         Return rows to be edited. Last row has to contain object being
78         edited.
79         """
80         raise NotImplemented
81
82
83     def _create_object(self):
84         """
85         Create new object.
86         """
87         raise NotImplemented
88
89
90     def _set_object_value(self, row, col, value):
91         """
92         Update row's column with a value.
93         """
94         raise NotImplemented
95
96
97     def _swap_objects(self, o1, o2):
98         """
99         Swap two objects. If objects are swapped, then return ``True``.
100         """
101         raise NotImplemented
102
103
104     def _get_object(self, iter):
105         """
106         Get object from ``iter``.
107         """
108         path = self.get_path(iter)
109         return self[path][-1]
110
111
112     def swap(self, a, b):
113         """
114         Swap two list rows.
115         Parameters:
116         - a: path to first row
117         - b: path to second row
118         """
119         if not a or not b:
120             return
121         o1 = self[a][-1]
122         o2 = self[b][-1]
123         if o1 and o2 and self._swap_objects(o1, o2):
124             self._item.request_update(matrix=False)
125             super(EditableTreeModel, self).swap(a, b)
126
127
128     def _add_empty(self):
129         """
130         Add empty row to the end of the model.
131         """
132         self.append([None] * self.get_n_columns())
133
134
135     def iter_prev(self, iter):
136         """
137         Get previous GTK tree iterator to ``iter``.
138         """
139         i = self.get_path(iter)[0]
140         if i == 0:
141             return None
142         return self.get_iter((i - 1,))
143
144
145     def set_value(self, iter, value, col):
146         path = self.get_path(iter)
147         row = self[path]
148
149         if col == 0 and not value and row[-1]:
150             # kill row and delete object if text of first column is empty
151             self.remove(iter)
152
153         elif value and not row[-1]:
154             # create new object
155             obj = self._create_object()
156             row[-1] = obj
157             self._set_object_value(row, col, value)
158             self._add_empty()
159
160         elif value:
161             self._set_object_value(row, col, value)
162         self._item.request_update(matrix=False)
163
164
165     def remove(self, iter):
166         """
167         Remove object from GTK model and destroy it.
168         """
169         obj = self._get_object(iter)
170         if obj:
171             obj.unlink()
172             self._item.request_update(matrix=False)
173             return super(EditableTreeModel, self).remove(iter)
174         else:
175             return iter
176
177
178
179 class ClassAttributes(EditableTreeModel):
180     """
181     GTK tree model to edit class attributes.
182     """
183     def _get_rows(self):
184         for attr in self._item.subject.ownedAttribute:
185             if not attr.association:
186                 yield [attr.render(), attr]
187
188
189     def _create_object(self):
190         attr = self.element_factory.create(UML.Property)
191         self._item.subject.ownedAttribute = attr
192         return attr
193
194
195     def _set_object_value(self, row, col, value):
196         attr = row[-1]
197         attr.parse(value)
198         row[0] = attr.render()
199
200
201     def _swap_objects(self, o1, o2):
202         return self._item.subject.ownedAttribute.swap(o1, o2)
203
204
205
206 class ClassOperations(EditableTreeModel):
207     """
208     GTK tree model to edit class operations.
209     """
210     def _get_rows(self):
211         for operation in self._item.subject.ownedOperation:
212             yield [operation.render(), operation]
213
214
215     def _create_object(self):
216         operation = self.element_factory.create(UML.Operation)
217         self._item.subject.ownedOperation = operation
218         return operation
219
220
221     def _set_object_value(self, row, col, value):
222         operation = row[-1]
223         operation.parse(value)
224         row[0] = operation.render()
225
226
227     def _swap_objects(self, o1, o2):
228         return self._item.subject.ownedOperation.swap(o1, o2)
229
230
231
232 class TaggedValues(EditableTreeModel):
233     """
234     GTK tree model to edit tagged values.
235     """
236     def __init__(self, item):
237         super(TaggedValues, self).__init__(item, [str, str, object])
238
239
240     def _get_rows(self):
241         for tv in self._item.subject.taggedValue:
242             tag, value = tv.value.split("=")
243             yield [tag, value, tv]
244
245
246     def _create_object(self):
247         tv = self.element_factory.create(UML.LiteralSpecification)
248         self._item.subject.taggedValue.append(tv)
249         return tv
250
251
252     def _set_object_value(self, row, col, value):
253         tv = row[-1]
254         row[col] = value
255         tv.value = '%s=%s' % (row[0], row[1])
256
257
258     def _swap_objects(self, o1, o2):
259         return self._item.subject.taggedValue.swap(o1, o2)
260
261
262
263 class CommunicationMessageModel(EditableTreeModel):
264     """
265     GTK tree model for list of messages on communication diagram.
266     """
267     def __init__(self, item, cols=None, inverted=False):
268         self.inverted = inverted
269         super(CommunicationMessageModel, self).__init__(item, cols)
270
271     def _get_rows(self):
272         if self.inverted:
273             for message in self._item._inverted_messages:
274                 yield [message.name, message]
275         else:
276             for message in self._item._messages:
277                 yield [message.name, message]
278
279
280     def remove(self, iter):
281         """
282         Remove message from message item and destroy it.
283         """
284         message = self._get_object(iter)
285         item = self._item
286         super(CommunicationMessageModel, self).remove(iter)
287         item.remove_message(message, self.inverted)
288
289
290     def _create_object(self):
291         item = self._item
292         subject = item.subject
293         message = self.element_factory.create(UML.Message)
294         if self.inverted:
295             # inverted message goes in different direction, than subject
296             message.sendEvent = subject.receiveEvent
297             message.receiveEvent = subject.sendEvent
298         else:
299             # additional message goes in the same direction as subject
300             message.sendEvent = subject.sendEvent
301             message.receiveEvent = subject.receiveEvent
302         item.add_message(message, self.inverted)
303         return message
304
305
306     def _set_object_value(self, row, col, value):
307         message = row[-1]
308         message.name = value
309         row[0] = value
310         self._item.set_message_text(message, value, self.inverted)
311
312
313     def _swap_objects(self, o1, o2):
314         return self._item.swap_messages(o1, o2, self.inverted)
315
316
317
318 def remove_on_keypress(tree, event):
319     """
320     Remove selected items from GTK model on ``backspace`` keypress.
321     """
322     k = gtk.gdk.keyval_name(event.keyval).lower()
323     if k == 'backspace' or k == 'kp_delete':
324         model, iter = tree.get_selection().get_selected()
325         if iter:
326             model.remove(iter)
327
328
329 def swap_on_keypress(tree, event):
330     """
331     Swap selected and previous (or next) items.
332     """
333     k = gtk.gdk.keyval_name(event.keyval).lower()
334     if k == 'equal' or k == 'kp_add':
335         model, iter = tree.get_selection().get_selected()
336         model.swap(iter, model.iter_next(iter))
337         return True
338     elif k == 'minus':
339         model, iter = tree.get_selection().get_selected()
340         model.swap(iter, model.iter_prev(iter))
341         return True
342        
343
344 @transactional
345 def on_cell_edited(renderer, path, value, model, col):
346     """
347     Update editable tree model based on fresh user input.
348     """
349     iter = model.get_iter(path)
350     model.set_value(iter, value, col)
351
352
353 class UMLComboModel(gtk.ListStore):
354     """
355     UML combo box model.
356
357     Model allows to easily create a combo box with values and their labels,
358     for example
359
360         label1  ->  value1
361         label2  ->  value2
362         label3  ->  value3
363
364     Labels are displayed by combo box and programmer has easy access to
365     values associated with given label.
366
367     Attributes:
368
369     - _data: model data
370     - _indices: dictionary of values' indices
371     """
372     def __init__(self, data):
373         super(UMLComboModel, self).__init__(str)
374
375         self._indices = {}
376         self._data = data
377
378         # add labels to underlying model and store index information
379         for i, (label, value) in enumerate(data):
380             self.append([label])
381             self._indices[value] = i
382
383        
384     def get_index(self, value):
385         """
386         Return index of a ``value``.
387         """
388         return self._indices[value]
389
390
391     def get_value(self, index):
392         """
393         Get value for given ``index``.
394         """
395         return self._data[index][1]
396
397
398
399 def create_uml_combo(data, callback):
400     """
401     Create a combo box using ``UMLComboModel`` model.
402
403     Combo box is returned.
404     """
405     model = UMLComboModel(data)
406     combo = gtk.ComboBox(model)
407     cell = gtk.CellRendererText()
408     combo.pack_start(cell, True)
409     combo.add_attribute(cell, 'text', 0)
410     combo.connect('changed', callback)
411     return combo
412
413
414 def watch_attribute(attribute, widget, handler):
415     """
416     Watch attribute ``attribute`` for changes. If it changes
417     ``handler(event)`` is called. When ``widget`` is destroyed, the
418     handler is unregistered.
419     If ``attribute`` is None, all attribute events are propagated to teh handler.
420     """
421     @component.adapter(IAttributeChangeEvent)
422     def attribute_watcher(event):
423         if attribute is None or event.property is attribute:
424             handler(event)
425
426     Application.register_handler(attribute_watcher)
427
428     def destroy_handler(_widget):
429         Application.unregister_handler(attribute_watcher)
430     widget.connect('destroy', destroy_handler)
431
432
433
434 def create_hbox_label(adapter, page, label):
435     """
436     Create a HBox with a label for given property page adapter and page
437     itself.
438     """
439     hbox = gtk.HBox()
440     label = gtk.Label(label)
441     label.set_justify(gtk.JUSTIFY_LEFT)
442     adapter.size_group.add_widget(label)
443     hbox.pack_start(label, expand=False)
444     page.pack_start(hbox, expand=False)
445     return hbox
446
447
448 def create_tree_view(model, names, tip=""):
449     """
450     Create a tree view for a editable tree model.
451     """
452     tree_view = gtk.TreeView(model)
453     tree_view.set_rules_hint(True)
454    
455     n = model.get_n_columns() - 1
456     for i in range(n):
457         renderer = gtk.CellRendererText()
458         renderer.set_property('editable', True)
459         renderer.connect('edited', on_cell_edited, model, i)
460         col = gtk.TreeViewColumn(names[i], renderer, text=i)
461         tree_view.append_column(col)
462
463     tree_view.connect('key_press_event', remove_on_keypress)
464     tree_view.connect('key_press_event', swap_on_keypress)
465
466     tip = tip + """
467 Press ENTER to edit item, BS/DEL to remove item.
468 Use -/= to move items up or down.\
469     """
470     tooltips = gtk.Tooltips()
471     tooltips.set_tip(tree_view, tip)
472
473     return tree_view
474
475
476
477 class CommentItemPropertyPage(object):
478     """
479     Property page for Comments
480     """
481     interface.implements(IPropertyPage)
482     component.adapts(items.CommentItem)
483
484     def __init__(self, context):
485         self.context = context
486
487     def construct(self):
488         subject = self.context.subject
489         page = gtk.VBox()
490
491         label = gtk.Label(_('Comment'))
492         label.set_justify(gtk.JUSTIFY_LEFT)
493         page.pack_start(label, expand=False)
494
495         buffer = gtk.TextBuffer()
496         if subject.body:
497             buffer.set_text(subject.body)
498         text_view = gtk.TextView()
499         text_view.set_buffer(buffer)
500         text_view.show()
501         page.pack_start(text_view)
502
503         changed_id = buffer.connect('changed', self._on_body_change)
504
505         def handler(event):
506             if event.element is subject and event.new_value is not None:
507                 buffer.handler_block(changed_id)
508                 buffer.set_text(event.new_value)
509                 buffer.handler_unblock(changed_id)
510         watch_attribute(type(subject).body, text_view, handler)
511
512         page.show_all()
513         return page
514
515     @transactional
516     def _on_body_change(self, buffer):
517         self.context.subject.body = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
518        
519 component.provideAdapter(CommentItemPropertyPage, name='Properties')
520
521
522 class NamedItemPropertyPage(object):
523     """
524     An adapter which works for any named item view.
525
526     It also sets up a table view which can be extended.
527     """
528
529     interface.implements(IPropertyPage)
530
531     NAME_LABEL = _('Name')
532
533     def __init__(self, context):
534         self.context = context
535         self.size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
536    
537     def construct(self):
538         page = gtk.VBox()
539
540         subject = self.context.subject
541         if not subject:
542             return page
543
544         hbox = create_hbox_label(self, page, self.NAME_LABEL)
545         entry = gtk.Entry()       
546         entry.set_text(subject and subject.name or '')
547         hbox.pack_start(entry)
548
549         # monitor subject.name attribute
550         changed_id = entry.connect('changed', self._on_name_change)
551
552         def