root/gaphor/tags/gaphor-0.12.5/gaphor/diagram/association.py

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

- code cleanup

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1
2 """AssociationItem -- Graphical representation of an association.
3
4 Plan:
5  - transform AssociationEnd in a (dumb) data class
6  - for assocation name and direction tag, use the same trick as is used
7    for line ends.
8  -
9
10 """
11
12 # TODO: for Association.postload(): in some cases where the association ends
13 # are connected to the same Class, the head_end property is connected to the
14 # tail end and visa versa.
15
16 from gaphas.util import text_extents, text_align, text_multiline
17 from gaphas.state import reversible_property
18 from gaphas import Item
19 from gaphas.geometry import Rectangle, distance_point_point_fast
20 from gaphas.geometry import distance_rectangle_point, distance_line_point
21
22 from gaphor import UML
23 from gaphor.diagram.diagramitem import SubjectSupport
24 from gaphor.diagram.diagramline import NamedLine
25
26
27 class AssociationItem(NamedLine):
28     """
29     AssociationItem represents associations.
30     An AssociationItem has two AssociationEnd items. Each AssociationEnd item
31     represents a Property (with Property.association == my association).
32     """
33
34     __uml__ = UML.Association
35
36     def __init__(self, id=None):
37         NamedLine.__init__(self, id)
38
39         # AssociationEnds are really inseperable from the AssociationItem.
40         # We give them the same id as the association item.
41         self._head_end = AssociationEnd(owner=self, end="head")
42         self._tail_end = AssociationEnd(owner=self, end="tail")
43
44         # Direction depends on the ends that hold the ownedEnd attributes.
45         self._show_direction = False
46         self._dir_angle = 0
47         self._dir_pos = 0, 0
48
49     def set_show_direction(self, dir):
50         self._show_direction = dir
51         self.request_update()
52
53     show_direction = reversible_property(lambda s: s._show_direction, set_show_direction)
54
55     def setup_canvas(self):
56         super(AssociationItem, self).setup_canvas()
57         self._head_end._canvas = self.canvas
58         self._tail_end._canvas = self.canvas
59
60     def teardown_canvas(self):
61         super(AssociationItem, self).teardown_canvas()
62         del self._head_end._canvas
63         del self._tail_end._canvas
64
65     def save(self, save_func):
66         NamedLine.save(self, save_func)
67         save_func('show-direction', self._show_direction)
68         if self._head_end.subject:
69             save_func('head-subject', self._head_end.subject)
70         if self._tail_end.subject:
71             save_func('tail-subject', self._tail_end.subject)
72
73     def load(self, name, value):
74         # end_head and end_tail were used in an older Gaphor version
75         if name in ( 'head_end', 'head_subject', 'head-subject' ):
76             #type(self._head_end).subject.load(self._head_end, value)
77             #self._head_end.load('subject', value)
78             self._head_end.subject = value
79         elif name in ( 'tail_end', 'tail_subject', 'tail-subject' ):
80             #type(self._tail_end).subject.load(self._tail_end, value)
81             #self._tail_end.load('subject', value)
82             self._tail_end.subject = value
83         else:
84             NamedLine.load(self, name, value)
85
86     def postload(self):
87         NamedLine.postload(self)
88         self._head_end.set_text()
89         self._tail_end.set_text()
90
91     head_end = property(lambda self: self._head_end)
92
93     tail_end = property(lambda self: self._tail_end)
94
95     def unlink(self):
96         self._head_end.unlink()
97         self._tail_end.unlink()
98         NamedLine.unlink(self)
99
100     def invert_direction(self):
101         """Invert the direction of the association, this is done by
102         swapping the head and tail-ends subjects.
103         """
104         if not self.subject:
105             return
106
107         self.subject.memberEnd.swap(self.subject.memberEnd[0], self.subject.memberEnd[1])
108         self.request_update()
109
110     def on_subject_notify(self, pspec, notifiers=()):
111         NamedLine.on_subject_notify(self, pspec,
112                 notifiers + ('ownedEnd', 'memberEnd'))
113
114     def on_subject_notify__ownedEnd(self, subject, pspec):
115         self.request_update()
116
117     def on_subject_notify__memberEnd(self, subject, pspec):
118         self.request_update()
119
120     def post_update(self, context):
121         """
122         Update the shapes and sub-items of the association.
123         """
124
125         handles = self.handles()
126
127         # Update line endings:
128         head_subject = self._head_end.subject
129         tail_subject = self._tail_end.subject
130        
131         # Update line ends using the aggregation and isNavigable values:
132         if head_subject and tail_subject:
133             if tail_subject.aggregation == intern('composite'):
134                 self.draw_head = self.draw_head_composite
135             elif tail_subject.aggregation == intern('shared'):
136                 self.draw_head = self.draw_head_shared
137             elif self._head_end.get_navigability():
138                 self.draw_head = self.draw_head_navigable
139             elif self._head_end.get_navigability() == False:
140                 self.draw_head = self.draw_head_none
141             else:
142                 self.draw_head = self.draw_head_undefined
143
144             if head_subject.aggregation == intern('composite'):
145                 self.draw_tail = self.draw_tail_composite
146             elif head_subject.aggregation == intern('shared'):
147                 self.draw_tail = self.draw_tail_shared
148             elif self._tail_end.get_navigability():
149                 self.draw_tail = self.draw_tail_navigable
150             elif self._tail_end.get_navigability() == False:
151                 self.draw_tail = self.draw_tail_none
152             else:
153                 self.draw_tail = self.draw_tail_undefined
154
155             if self._show_direction:
156                 inverted = self.tail_end.subject is self.subject.memberEnd[0]
157                 pos, angle = self._get_center_pos(inverted)
158                 self._dir_pos = pos
159                 self._dir_angle = angle
160         else:
161             self.draw_head = self.draw_head_undefined
162             self.draw_tail = self.draw_tail_undefined
163
164         # update relationship after self.set calls to avoid circural updates
165         NamedLine.post_update(self, context)
166
167         # Calculate alignment of the head name and multiplicity
168         self._head_end.post_update(context, handles[0].pos,
169                                      handles[1].pos)
170
171         # Calculate alignment of the tail name and multiplicity
172         self._tail_end.post_update(context, handles[-1].pos,
173                                      handles[-2].pos)
174        
175
176     def point(self, x, y):
177         """Returns the distance from the Association to the (mouse) cursor.
178         """
179         return min(super(AssociationItem, self).point(x, y),
180                    self._head_end.point(x, y),
181                    self._tail_end.point(x, y))
182
183     def draw_head_none(self, context):
184         """Draw an 'x' on the line end, indicating no traversing.
185         """
186         cr = context.cairo
187         cr.move_to(6, -4)
188         cr.rel_line_to(8, 8)
189         cr.rel_move_to(0, -8)
190         cr.rel_line_to(-8, 8)
191         cr.stroke()
192         cr.move_to(0, 0)
193
194     def draw_tail_none(self, context):
195         """Draw an 'x' on the line end, indicating no traversing.
196         """
197         cr = context.cairo
198         cr.line_to(0, 0)
199         cr.move_to(6, -4)
200         cr.rel_line_to(8, 8)
201         cr.rel_move_to(0, -8)
202         cr.rel_line_to(-8, 8)
203         cr.stroke()
204
205     def _draw_diamond(self, cr):
206         cr.move_to(20, 0)
207         cr.line_to(10, -6)
208         cr.line_to(0, 0)
209         cr.line_to(10, 6)
210         #cr.line_to(20, 0)
211         cr.close_path()
212
213     def draw_head_composite(self, context):
214         """Draw a closed diamond on the line end.
215         """
216         cr = context.cairo
217         self._draw_diamond(cr)
218         context.cairo.fill_preserve()
219         cr.stroke()
220         cr.move_to(20, 0)
221
222     def draw_tail_composite(self, context):
223         """Draw a closed diamond on the line end.
224         """
225         cr = context.cairo
226         cr.line_to(20, 0)
227         cr.stroke()
228         self._draw_diamond(cr)
229         cr.fill_preserve()
230         cr.stroke()
231
232     def draw_head_shared(self, context):
233         """Draw an open diamond on the line end.
234         """
235         cr = context.cairo
236         self._draw_diamond(cr)
237         cr.move_to(20, 0)
238
239     def draw_tail_shared(self, context):
240         """Draw an open diamond on the line end.
241         """
242         cr = context.cairo
243         cr.line_to(20, 0)
244         cr.stroke()
245         self._draw_diamond(cr)
246         cr.stroke()
247
248     def draw_head_navigable(self, context):
249         """Draw a normal arrow.
250         """
251         cr = context.cairo
252         cr.move_to(15, -6)
253         cr.line_to(0, 0)
254         cr.line_to(15, 6)
255         cr.stroke()
256         cr.move_to(0, 0)
257
258     def draw_tail_navigable(self, context):
259         """Draw a normal arrow.
260         """
261         cr = context.cairo
262         cr.line_to(0, 0)
263         cr.stroke()
264         cr.move_to(15, -6)
265         cr.line_to(0, 0)
266         cr.line_to(15, 6)
267
268     def draw_head_undefined(self, context):
269         """Draw nothing. undefined.
270         """
271         context.cairo.move_to(0, 0)
272
273     def draw_tail_undefined(self, context):
274         """Draw nothing. undefined.
275         """
276         context.cairo.line_to(0, 0)
277
278     def draw(self, context):
279         cr = context.cairo
280         super(AssociationItem, self).draw(context)
281         self._head_end.draw(context)
282         self._tail_end.draw(context)
283         if self._show_direction:
284             cr.save()
285             try:
286                 cr.translate(*self._dir_pos)
287                 cr.rotate(self._dir_angle)
288                 cr.move_to(0, 0)
289                 cr.line_to(6, 5)
290                 cr.line_to(0, 10)
291                 cr.fill()
292             finally:
293                 cr.restore()
294
295
296     def item_at(self, x, y):
297         if distance_point_point_fast(self._handles[0].pos, (x, y)) < 10:
298             return self._head_end
299         elif distance_point_point_fast(self._handles[-1].pos, (x, y)) < 10:
300             return self._tail_end
301         return self
302        
303        
304 class AssociationEnd(SubjectSupport):
305     """
306     An association end represents one end of an association. An association
307     has two ends. An association end has two labels: one for the name and
308     one for the multiplicity (and maybe one for tagged values in the future).
309
310     An AsociationEnd has no ID, hence it will not be stored, but it will be
311     recreated by the owning Association.
312     
313     TODO:
314     - add on_point() and let it return min(distance(_name), distance(_mult)) or
315       the first 20-30 units of the line, for association end popup menu.
316     """
317
318     head_popup_menu = (
319         'Head_unknownNavigation',
320         'Head_isNotNavigable',
321         'Head_isNavigable',
322         'separator',
323         'Head_AggregationNone',
324         'Head_AggregationShared',
325         'Head_AggregationComposite'
326     )
327
328     tail_popup_menu = (
329         'Tail_unknownNavigation',
330         'Tail_isNotNavigable',
331         'Tail_isNavigable',
332         'separator',
333         'Tail_AggregationNone',
334         'Tail_AggregationShared',
335         'Tail_AggregationComposite'
336     )
337
338     def __init__(self, owner, id=None, end=None):
339         SubjectSupport.__init__(self)
340         self._owner = owner
341         self._end = end
342        
343         # Rendered text for name and multiplicity
344         self._name = None
345         self._mult = None
346
347         self._name_bounds = Rectangle()
348         self._mult_bounds = Rectangle()
349
350         self._subject = None
351
352     def _set_subject(self, value):
353         self._subject = value
354         class pspec:
355             name = 'subject'
356         self.on_subject_notify(pspec)
357
358     def _del_subject(self):
359         self._subject = None
360         class pspec:
361             name = 'subject'
362         self.on_subject_notify(pspec)
363
364     subject = property(lambda s: s._subject, _set_subject, _del_subject)
365
366     def get_popup_menu(self):
367         if self.subject:
368             if self._end == 'head':
369                 return self.head_popup_menu
370             else:
371                 return self.tail_popup_menu
372         elif self.parent:
373             return self.parent.get_popup_menu()
374
375     def request_update(self):
376         self._owner.request_update()
377
378     def set_text(self):
379         """Set the text on the association end.
380         """
381         if self.subject:
382             try:
383                 n, m = self.subject.render()
384             except ValueError:
385                 # need more than 0 values to unpack: property was rendered as
386                 # attribute while in a UNDO action for example.
387                 pass
388             else:
389                 self._name = n
390                 self._mult = m
391                 self.request_update()
392
393     def get_navigability(self):
394         """
395         Check navigability of the AssociationEnd. If property is owned by
396         class via ownedAttribute, then it is navigable. If property is
397         owned by association by owndedEnd, then it is not navigable.
398         Otherwise the navigability is unknown.
399
400         Returned navigability values:
401             - None  - unknown
402             - False - not navigable
403             - True  - navigable
404         """
405         navigability = None # unknown navigability as default
406         subject = self.subject
407
408         if subject and subject.opposite:
409             opposite = subject.opposite
410             if isinstance(opposite.type, UML.Interface):
411                 type = subject.interface_
412             else: # isinstance(opposite.type, UML.Class):
413                 type = subject.class_
414
415             if type and subject in type.ownedAttribute:
416                 navigability = True
417             elif subject.association and subject in subject.association.ownedEnd:
418                 navigability = False
419
420         return navigability
421                
422
423     def _set_navigability(self, navigable):
424         """
425         Change the AssociationEnd's navigability.
426
427         A warning is issued if the subject or opposite property is missing.
428         """
429         subject = self.subject
430         if subject and subject.opposite:
431             opposite = subject.opposite
432
433             #
434             # Remove any navigability info, so unknown navigability state
435             # is the default.
436             #
437
438             # if navigable
439             if isinstance(opposite.type, UML.Class):
440                 if subject.class_:
441                     del subject.class_
442             elif isinstance(opposite.type, UML.Interface):
443                 if subject.interface_:
444                     del subject.interface_
445             else:
446                 assert 0, 'Should never be reached'
447
448             # if not navigable
449             if subject.owningAssociation:
450                 del subject.owningAssociation
451
452
453             #
454             # Set navigability.
455             #
456             if navigable:
457                 if isinstance(opposite.type, UML.Class):
458                     subject.class_ = opposite.type
459                 elif isinstance(opposite.type, UML.Interface):
460                     subject.interface_ = opposite.type
461                 else:
462                     assert 0, 'Should never be reached'
463             elif navigable == False:
464                 subject.owningAssociation = subject.association
465
466             # else navigability is unknown
467
468         else:
469             log.warning('AssociationEnd.set_navigable: %s missing' % \
470                         (subject and 'subject' or 'opposite Property'))
471
472     navigability = property(get_navigability, _set_navigability)
473
474     def point_name(self, x, y):
475         p = (x, y)
476         drp = distance_rectangle_point
477         return drp(self._name_bounds, p)
478
479     def point_mult(self, x, y):
480         p = (x, y)
481         drp = distance_rectangle_point
482         return drp(self._mult_bounds, p)
483
484     def point(self, x, y):
485         return min(self.point_name(x, y), self.point_mult(x, y))
486
487     def get_name(self):
488         return self._name
489
490     def get_mult(self):
491         return self._mult
492
493     def post_update(self, context, p1, p2):
494         """
495         Update label placement for association's name and
496         multiplicity label. p1 is the line end and p2 is the last
497         but one point of the line.
498         """
499         cr = context.cairo
500         ofs = 5
501
502         name_dx = 0.0
503         name_dy = 0.0
504         mult_dx = 0.0
505         mult_dy = 0.0
506
507         dx = float(p2[0]) - float(p1[0])
508         dy = float(p2[1]) - float