root/gaphor/trunk/gaphor/diagram/classes/association.py

Revision 2296, 21.4 kB (checked in by wrobe..@pld-linux.org, 4 months ago)

- documentation updates

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