root/gaphor/tags/gaphor-0.12.0/gaphor/diagram/diagramitem.py

Revision 2009, 12.2 kB (checked in by arj..@yirdis.nl, 1 year ago)
  • Make Presentation inherit from Element
  • updated diagramitem.py to reflect change
  • fixed style property for named lines.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 """
2 DiagramItem provides basic functionality for presentations.
3 Such as a modifier 'subject' property and a unique id.
4 """
5
6 from gaphor import UML
7 from gaphor.misc import uniqueid
8 from gaphor.diagram import DiagramItemMeta
9 from gaphor.diagram.textelement import EditableTextSupport
10 from gaphor.diagram.style import ALIGN_CENTER, ALIGN_TOP
11
12 STEREOTYPE_OPEN  = '\xc2\xab' # '<<'
13 STEREOTYPE_CLOSE = '\xc2\xbb' # '>>'
14
15 class SubjectSupport(UML.Presentation):
16     """
17     Support class that adds support methods for Presentation.subject.
18
19     This is a support class that should could be added to subclasses of
20     UML.Presentation.
21     """
22
23     def __init__(self):
24         UML.Presentation.__init__(self)
25         # Add the class' on_subject_notify() as handler:
26         self.connect('subject', type(self).on_subject_notify)
27
28         # __the_subject is a backup that is used to disconnect signals when a
29         # new subject is set (or the original one is removed)
30         self.__the_subject = None
31
32     def _subject_connect_helper(self, element, callback_prefix, prop_list):
33         """
34         Connect a signal notifier. The notifier can be just the name of
35         one of the subjects properties.
36
37         See: DiagramItem.on_subject_notify()
38         """
39         #log.debug('_subject_connect_helper: %s %s %s' % (element, callback_prefix, prop_list))
40
41         prop = prop_list[0]
42         callback_name = '%s_%s' % (callback_prefix, prop)
43         if len(prop_list) == 1:
44             #log.debug('_subject_connect_helper - %s %s' % (element, callback_name))
45             handler = getattr(self, callback_name)
46             element.connect(prop, handler)
47             # Call the handler, so it can update its state
48             #handler(prop, getattr(type(element), prop))
49             #handler(element, getattr(type(element), prop))
50         else:
51             p = getattr(element, prop)
52             #log.debug('_subject_connect_helper 2 - %s: %s' % (prop, p))
53             pl = prop_list[1:]
54             element.connect(prop, self._on_subject_notify_helper, callback_name, pl, [p])
55             if p:
56                 self._subject_connect_helper(p, callback_name, pl)
57             else:
58                 pass
59
60     def _subject_disconnect_helper(self, element, callback_prefix, prop_list):
61         """
62         Disconnect a previously connected signal handler.
63
64         See: DiagramItem.on_subject_notify()
65         """
66         #log.debug('_subject_disconnect_helper: %s %s %s' % (element, callback_prefix, prop_list))
67         prop = prop_list[0]
68         callback_name = '%s_%s' % (callback_prefix, prop)
69         if len(prop_list) == 1:
70             #log.debug('_subject_disconnect_helper - %s %s' % (element, callback_name))
71             handler = getattr(self, callback_name)
72             element.disconnect(handler)
73             # Call the handler, so it can update its state
74             #handler(element, getattr(type(element), prop))
75         else:
76             p = getattr(element, prop)
77             #log.debug('_subject_disconnect_helper 2 - %s' % prop)
78             pl = prop_list[1:]
79             element.disconnect(self._on_subject_notify_helper, callback_name, pl, [p])
80             if p:
81                 self._subject_disconnect_helper(p, callback_name, pl)
82             else:
83                 pass
84
85     def _on_subject_notify_helper(self, element, pspec, callback_name, prop_list, old):
86         """
87         This signal handler handles signals that are not direct properties
88         of self.subject (e.g. 'subject.lowerValue.value'). This way the
89         presentation class is not bothered with the details of keeping track
90         of those properties.
91
92         NOTE: This only works for properties with multiplicity [0..1] or [1].
93
94         See: DiagramItem.on_subject_notify()
95         """
96         name = pspec.name
97         prop = getattr(element, name)
98         #log.debug('_on_subject_notify_helper: %s %s %s %s %s' % (element, name, callback_name, prop_list, old))
99         # Attach a new signal handler with the new 'old' value:
100         if old[0]:
101             #log.info('disconnecting')
102             self._subject_disconnect_helper(old[0], callback_name, prop_list)
103         if prop:
104             self._subject_connect_helper(prop, callback_name, prop_list)
105
106         # Set the new "old" value
107         old[0] = prop
108
109     def on_subject_notify(self, pspec, notifiers=()):
110         """
111         A new subject is set on this model element.
112         notifiers is an optional tuple of elements that also need a
113         callback function. Callbacks have the signature
114         on_subject_notify__<notifier>(self, subject, pspec).
115
116         A notifier can be a property of subject (e.g. 'name') or a property
117         of a property of subject (e.g. 'lowerValue.value').
118         """
119         #log.info('Setting subject from %s to %s' % (self.__the_subject, self.subject))
120         # First, split all notifiers on '.'
121         callback_prefix = 'on_subject_notify_'
122         notifiers = map(str.split, notifiers, ['.'] * len(notifiers))
123         old_subject = self.__the_subject
124         subject_connect_helper = self._subject_connect_helper
125         subject_disconnect_helper = self._subject_disconnect_helper
126
127         if old_subject:
128             for n in notifiers:
129                 #self._subject_disconnect(self.__the_subject, n)
130                 subject_disconnect_helper(old_subject, callback_prefix, n)
131
132         if self.subject:
133             subject = self.__the_subject = self.subject
134             for n in notifiers:
135                 #log.debug('DiaCanvasItem.on_subject_notify: %s' % signal)
136                 #self._subject_connect(self.subject, n)
137                 subject_connect_helper(subject, callback_prefix, n)
138
139         self.request_update()
140
141
142 class StereotypeSupport(object):
143     """
144     Support methods for stereotypes.
145     """
146     STEREOTYPE_ALIGN = {
147         'text-align'  : (ALIGN_CENTER, ALIGN_TOP),
148         'text-padding': (5, 10, 2, 10),
149         'text-outside': False,
150         'text-align-group': 'stereotype',
151     }
152
153     def __init__(self):
154         self._stereotype = self.add_text('stereotype',
155                 style=self.STEREOTYPE_ALIGN,
156                 pattern='%s%%s%s' % (STEREOTYPE_OPEN, STEREOTYPE_CLOSE),
157                 visible=self.is_stereotype_visible)
158
159
160     def is_stereotype_visible(self):
161         """
162         Display stereotype if it is not empty.
163         """
164         return self._stereotype.text
165
166
167     def set_stereotype(self, text=None):
168         """
169         Set the stereotype text for the diagram item.
170
171         Note, that text is not Stereotype object.
172
173         @arg text: stereotype text
174         """
175         self._stereotype.text = text
176         self.request_update()
177
178     stereotype = property(lambda s: s._stereotype, set_stereotype)
179
180     def update_stereotype(self):
181         """
182         Update the stereotype definitions (text) of this item.
183
184         Note, that this method is also called from
185         ExtensionItem.confirm_connect_handle method.
186         """
187         if self.subject:
188             applied_stereotype = self.subject.appliedStereotype
189         else:
190             applied_stereotype = None
191
192         def stereotype_name(name):
193             """
194             Return a nice name to display as stereotype. First will be
195             character lowercase unless the second character is uppercase.
196             """
197             if len(name) > 1 and name[1].isupper():
198                 return name
199             else:
200                 return name[0].lower() + name[1:]
201
202         # by default no stereotype, however check for __stereotype__
203         # attribute to assign some static stereotype see interfaces,
204         # use case relationships, package or class for examples
205         stereotype = getattr(self, '__stereotype__', None)
206         if stereotype:
207             stereotype = self.parse_stereotype(stereotype)
208
209         if applied_stereotype:
210             # generate string with stereotype names separated by coma
211             sl = ', '.join(stereotype_name(s.name) for s in applied_stereotype)
212             if stereotype:
213                 stereotype = '%s, %s' % (stereotype, sl)
214             else:
215                 stereotype = sl
216
217         # Phew! :]
218         self.set_stereotype(stereotype)
219
220     def parse_stereotype(self, data):
221         if isinstance(data, str): # return data as stereotype if it is a string
222             return data
223
224         subject = self.subject
225
226         for stereotype, condition in data.items():
227             if isinstance(condition, tuple):
228                 cls, predicate = condition
229             elif isinstance(condition, type):
230                 cls = condition
231                 predicate = None
232             elif callable(condition):
233                 cls = None
234                 predicate = condition
235             else:
236                 assert False, 'wrong conditional %s' % condition
237
238             ok = True
239             if cls:
240                 ok = type(subject) is cls #isinstance(subject, cls)
241             if predicate:
242                 ok = predicate(self)
243
244             if ok:
245                 return stereotype
246         return None
247
248
249 class DiagramItem(SubjectSupport, StereotypeSupport, EditableTextSupport):
250     """
251     Basic functionality for all model elements (lines and elements!).
252
253     This class contains common functionallity for model elements and
254     relationships.
255     It provides an interface similar to UML.Element for connecting and
256     disconnecting signals.
257
258     This class is not very useful on its own. It contains some glue-code for
259     diacanvas.DiaCanvasItem and gaphor.UML.Element.
260
261     Example:
262         class ElementItem(diacanvas.CanvasElement, DiagramItem):
263             connect = DiagramItem.connect
264             disconnect = DiagramItem.disconnect
265             ...
266
267     @cvar style: styles information (derived from DiagramItemMeta)
268     """
269
270     __metaclass__ = DiagramItemMeta
271
272     def __init__(self, id=None):
273         SubjectSupport.__init__(self)
274         EditableTextSupport.__init__(self)
275         StereotypeSupport.__init__(self)
276
277         self._id = id
278
279         # properties, which should be saved in file
280         self._persistent_props = set()
281
282
283     id = property(lambda self: self._id, doc='Id')
284
285
286     def set_prop_persistent(self, name):
287         """
288         Specify property of diagram item, which should be saved in file.
289         """
290         self._persistent_props.add(name)
291
292
293     # UML.Element interface used by properties:
294
295     # TODO: Use adapters for load/save functionality
296     def save(self, save_func):
297         if self.subject:
298             save_func('subject', self.subject)
299
300         # save persistent properties
301         for p in self._persistent_props:
302             save_func(p, getattr(self, p.replace('-', '_')), reference=True)
303
304
305     def load(self, name, value):
306         if name == 'subject':
307             type(self).subject.load(self, value)
308         else:
309             #log.debug('Setting unknown property "%s" -> "%s"' % (name, value))
310             try:
311                 setattr(self, name.replace('-', '_'), eval(value))
312             except:
313                 log.warning('%s has no property named %s (value %s)' % (self, name, value))
314
315     def postload(self):
316         if self.subject:
317             self.on_subject_notify(type(self).subject)
318
319
320     def save_property(self, save_func, name):
321         """
322         Save a property, this is a shorthand method.
323         """
324         save_func(name, getattr(self, name.replace('-', '_')))
325
326     def save_properties(self, save_func, *names):
327         """
328         Save a property, this is a shorthand method.
329         """
330         for name in names:
331             self.save_property(save_func, name)
332
333     def unlink(self):
334         """
335         Remove the item from the canvas and set subject to None.
336         """
337         if self.canvas:
338             try:
339                 self.canvas.remove(self)
340             except KeyError:
341                 # Canvas was already removed
342                 pass
343         self.subject = None
344         super(DiagramItem, self).unlink()
345
346
347     def draw(self, context):
348         EditableTextSupport.draw(self, context)
349
350
351     def item_at(self, x, y):
352         return self
353
354     def on_subject_notify__appliedStereotype(self, subject, pspec=None):
355         if self.subject:
356             self.update_stereotype()
357             self.request_update()
358
359     def request_update(self):
360         """
361         Placeholder for gaphor.Item's request_update() method.
362         """
363         pass
364
365     def on_subject_notify(self, pspec, notifiers=()):
366         SubjectSupport.on_subject_notify(self, pspec, notifiers + ('appliedStereotype',))
367
368
369 # vim:sw=4:et:ai
Note: See TracBrowser for help on using the browser.