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

Revision 225, 21.1 kB (checked in by arjanmol, 5 years ago)

*** empty log message ***

  • 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 # vim:sw=4:et
5
6 # TODO: for Association.postload(): in some cases where the association ends
7 # are connected to the same Class, the head_end property is connected to the
8 # tail end and visa versa.
9
10 from __future__ import generators
11
12 import gobject
13 import pango
14 import diacanvas
15 import diacanvas.shape
16 import diacanvas.geometry
17 import gaphor
18 import gaphor.UML as UML
19 from gaphor.diagram import initialize_item
20
21 from diagramitem import DiagramItem
22 from relationship import RelationshipItem
23
24 class AssociationItem(RelationshipItem, diacanvas.CanvasAbstractGroup):
25     """AssociationItem represents associations.
26     An AssociationItem has two AssociationEnd items. Each AssociationEnd item
27     represents a Property (with Property.association == my association).
28     """
29     __gproperties__ = {
30         'head':         (gobject.TYPE_OBJECT, 'head',
31                          'AssociationEnd held by the head end of the association',
32                          gobject.PARAM_READABLE),
33         'tail':         (gobject.TYPE_OBJECT, 'tail',
34                          'AssociationEnd held by the tail end of the association',
35                          gobject.PARAM_READABLE),
36         'head-subject': (gobject.TYPE_PYOBJECT, 'head-subject',
37                          'subject held by the head end of the association',
38                          gobject.PARAM_READWRITE),
39         'tail-subject': (gobject.TYPE_PYOBJECT, 'tail-subject',
40                          'subject held by the tail end of the association',
41                          gobject.PARAM_READWRITE),
42     }
43
44     association_popup_menu = (
45         'separator',
46         'Side _A', (
47             'Head_isNavigable',
48             'separator',
49             'Head_AggregationNone',
50             'Head_AggregationShared',
51             'Head_AggregationComposite'),
52         'Side _B', (
53             'Tail_isNavigable',
54             'separator',
55             'Tail_AggregationNone',
56             'Tail_AggregationShared',
57             'Tail_AggregationComposite')
58     )
59
60     def __init__(self, id=None):
61         RelationshipItem.__init__(self, id)
62
63         # AssociationEnds are really inseperable from the AssociationItem.
64         # We give them the same id as the association item.
65         self._head_end = AssociationEnd()
66         self._head_end.set_child_of(self)
67         self._tail_end = AssociationEnd()
68         self._tail_end.set_child_of(self)
69
70     def save (self, save_func):
71         RelationshipItem.save(self, save_func)
72         if self._head_end.subject:
73             save_func('head_subject', self._head_end.subject)
74         if self._tail_end.subject:
75             save_func('tail_subject', self._tail_end.subject)
76
77     def load (self, name, value):
78         # end_head and end_tail were used in an older Gaphor version
79         if name in ( 'head_end', 'head_subject' ):
80             #type(self._head_end).subject.load(self._head_end, value)
81             self._head_end.load('subject', value)
82         elif name in ( 'tail_end', 'tail_subject' ):
83             #type(self._tail_end).subject.load(self._tail_end, value)
84             self._tail_end.load('subject', value)
85         else:
86             RelationshipItem.load(self, name, value)
87
88     def postload(self):
89         RelationshipItem.postload(self)
90         self._head_end.postload()
91         self._tail_end.postload()
92
93     def do_set_property (self, pspec, value):
94         if pspec.name == 'head-subject':
95             self._head_end.subject = value
96         elif pspec.name == 'tail-subject':
97             self._tail_end.subject = value
98         else:
99             RelationshipItem.do_set_property(self, pspec, value)
100
101     def do_get_property(self, pspec):
102         if pspec.name == 'head':
103             return self._head_end
104         if pspec.name == 'tail':
105             return self._tail_end
106         elif pspec.name == 'head-subject':
107             return self._head_end.subject
108         elif pspec.name == 'tail-subject':
109             return self._tail_end.subject
110         else:
111             return RelationshipItem.do_get_property(self, pspec)
112
113     head_end = property(lambda self: self._head_end)
114
115     tail_end = property(lambda self: self._tail_end)
116
117     def on_subject_notify(self, pspec, notifiers=()):
118         RelationshipItem.on_subject_notify(self, pspec,
119                                            notifiers + ('ownedEnd',))
120
121     def on_subject_notify__ownedEnd(self, subject, pspec):
122         self.request_update()
123
124     def on_update (self, affine):
125         """Update the shapes and sub-items of the association."""
126         # Update line endings:
127         head_subject = self._head_end.subject
128         tail_subject = self._tail_end.subject
129         if head_subject and tail_subject:
130             # Update line ends using the aggregation and isNavigable values:
131
132             if head_subject.aggregation == intern('composite'):
133                 self.set(has_head=1, head_a=20, head_b=10, head_c=6, head_d=6,
134                          head_fill_color=diacanvas.color(0,0,0,255))
135             elif not head_subject.owningAssociation:
136                 # This side is navigable:
137                 self.set(has_head=1, head_a=0, head_b=15, head_c=6, head_d=6)
138             else:
139                 self.set(has_head=0)
140
141             if tail_subject.aggregation == intern('composite'):
142                 self.set(has_tail=1, tail_a=20, tail_b=10, tail_c=6, tail_d=6,
143                          tail_fill_color=diacanvas.color(0,0,0,255))
144             elif not tail_subject.owningAssociation:
145                 # This side is navigable:
146                 self.set(has_tail=1, tail_a=0, tail_b=15, tail_c=6, tail_d=6)
147             else:
148                 self.set(has_tail=0)
149
150         RelationshipItem.on_update(self, affine)
151
152         handles = self.handles
153         # Calculate alignment of the head name and multiplicity
154         self._head_end.update_labels(handles[0].get_pos_i(),
155                                      handles[1].get_pos_i())
156
157         # Calculate alignment of the tail name and multiplicity
158         self._tail_end.update_labels(handles[-1].get_pos_i(),
159                                      handles[-2].get_pos_i())
160        
161         self.update_child(self._head_end, affine)
162         self.update_child(self._tail_end, affine)
163
164         # bounds calculation
165         b1 = self.bounds
166         b2 = self._head_end.get_bounds(self._head_end.affine)
167         b3 = self._tail_end.get_bounds(self._tail_end.affine)
168         self.set_bounds((min(b1[0], b2[0], b3[0]), min(b1[1], b2[1], b3[1]),
169                          max(b1[2], b2[2], b3[2]), max(b1[3], b2[3], b3[3])))
170                    
171     # Gaphor Connection Protocol
172
173     def allow_connect_handle(self, handle, connecting_to):
174         """This method is called by a canvas item if the user tries to connect
175         this object's handle. allow_connect_handle() checks if the line is
176         allowed to be connected. In this case that means that one end of the
177         line should be connected to a Class or Actor.
178         Returns: TRUE if connection is allowed, FALSE otherwise.
179         """
180         # TODO: Should allow to connect to Class and Actor.
181         #log.debug('AssociationItem.allow_connect_handle')
182         if isinstance(connecting_to.subject, (UML.Class, UML.Actor)):
183             return True
184         return False
185
186     def confirm_connect_handle (self, handle):
187         """This method is called after a connection is established. This method
188         sets the internal state of the line and updates the data model.
189         """
190         #log.debug('AssociationItem.confirm_connect_handle')
191
192         c1 = self.handles[0].connected_to
193         c2 = self.handles[-1].connected_to
194         if c1 and c2:
195             head_type = c1.subject
196             tail_type = c2.subject
197
198             # First check if we do not already contain the right subject:
199             if self.subject:
200                 end1 = self.subject.memberEnd[0]
201                 end2 = self.subject.memberEnd[1]
202                 if (end1.type is head_type and end2.type is tail_type) \
203                    or (end2.type is head_type and end1.type is tail_type):
204                     return
205                    
206             # Find all associations and determine if the properties on the
207             # association ends have a type that points to the class.
208             Association = UML.Association
209             for assoc in gaphor.resource(UML.ElementFactory).itervalues():
210                 if isinstance(assoc, Association):
211                     #print 'assoc.memberEnd', assoc.memberEnd
212                     end1 = assoc.memberEnd[0]
213                     end2 = assoc.memberEnd[1]
214                     if (end1.type is head_type and end2.type is tail_type) \
215                        or (end2.type is head_type and end1.type is tail_type):
216                         # check if this entry is not yet in the diagram
217                         # Return if the association is not (yet) on the canvas
218                         for item in assoc.presentation:
219                             if item.canvas is self.canvas:
220                                 break
221                         else:
222                             #return end1, end2, assoc
223                             self.subject = assoc
224                             if (end1.type is head_type and end2.type is tail_type):
225                                 self._head_end.subject = end1
226                                 self._tail_end.subject = end2
227                             else:
228                                 self._head_end.subject = end2
229                                 self._tail_end.subject = end1
230                             return
231             else:
232                 # TODO: How should we handle other types than Class???
233
234                 element_factory = gaphor.resource(UML.ElementFactory)
235                 relation = element_factory.create(UML.Association)
236                 head_end = element_factory.create(UML.Property)
237                 head_end.lowerValue = element_factory.create(UML.LiteralSpecification)
238                 tail_end = element_factory.create(UML.Property)
239                 tail_end.lowerValue = element_factory.create(UML.LiteralSpecification)
240                 relation.package = self.canvas.diagram.namespace
241                 relation.memberEnd = head_end
242                 relation.memberEnd = tail_end
243                 head_end.type = tail_end.class_ = head_type
244                 tail_end.type = head_end.class_ = tail_type
245
246                 # copy text from ends to AssociationEnds:
247                 #head_end.name = self._head_end._name.get_property('text')
248                 #head_end.multiplicity = self._head__end._mult.get_property('text')
249                 #tail_end.name = self._tail_end._name.get_property('text')
250                 #tail_end.multiplicity = self._tail_end._mult.get_property('text')
251
252                 self.subject = relation
253                 self._head_end.subject = head_end
254                 self._tail_end.subject = tail_end
255
256     def confirm_disconnect_handle (self, handle, was_connected_to):
257         #log.debug('AssociationItem.confirm_disconnect_handle')
258         if self.subject:
259             # First delete the Property's at the ends, otherwise they will
260             # be interpreted as attributes.
261             del self._head_end.subject
262             del self._tail_end.subject
263             del self.subject
264
265     # Groupable
266
267     def on_groupable_add(self, item):
268         return 0
269
270     def on_groupable_remove(self, item):
271         '''Do not allow the name to be removed.'''
272         return 1
273
274     def on_groupable_iter(self):
275         return iter([self._head_end, self._tail_end])
276
277     def get_popup_menu(self):
278         if self.subject:
279             return self.popup_menu + self.association_popup_menu
280         else:
281             return self.popup_menu
282
283
284 class AssociationEnd(diacanvas.CanvasItem, diacanvas.CanvasEditable, DiagramItem):
285     """An association end represents one end of an association. An association
286     has two ends. An association end has two labels: one for the name and
287     one for the multiplicity (and maybe one for tagged values in the future).
288
289     An AsociationEnd has no ID, hence it will not be stored, but it will be
290     recreated by the owning Association.
291     
292     TODO:
293     - add on_point() and let it return min(distance(_name), distance(_mult)) or
294       the first 20-30 units of the line, for association end popup menu.
295     """
296     __gproperties__ = DiagramItem.__gproperties__
297     ___gproperties__ = {
298         'name': (gobject.TYPE_STRING, 'name', '', '', gobject.PARAM_READWRITE),
299         'mult': (gobject.TYPE_STRING, 'mult', '', '', gobject.PARAM_READWRITE)
300     }
301
302     __gsignals__ = DiagramItem.__gsignals__
303
304     FONT='sans 10'
305
306     def __init__(self, id=None):
307         self.__gobject_init__()
308         DiagramItem.__init__(self, id)
309         self.set_flags(diacanvas.COMPOSITE)
310        
311         font = pango.FontDescription(AssociationEnd.FONT)
312         self._name = diacanvas.shape.Text()
313         self._name.set_font_description(font)
314         self._name.set_wrap_mode(diacanvas.shape.WRAP_NONE)
315         self._name.set_markup(False)
316         self._name_border = diacanvas.shape.Path()
317         self._name_border.set_color(diacanvas.color(128,128,128))
318         self._name_border.set_line_width(1.0)
319
320         self._mult = diacanvas.shape.Text()
321         self._mult.set_font_description(font)
322         self._mult.set_wrap_mode(diacanvas.shape.WRAP_NONE)
323         self._mult.set_markup(False)
324         self._mult_border = diacanvas.shape.Path()
325         self._mult_border.set_color(diacanvas.color(128,128,128))
326         self._mult_border.set_line_width(1.0)
327
328         self._name_bounds = self._mult_bounds = (0, 0, 0, 0)
329
330     # Ensure we call the right connect functions:
331     connect = DiagramItem.connect
332     disconnect = DiagramItem.disconnect
333     notify = DiagramItem.notify
334
335     def postload(self):
336         DiagramItem.postload(self)
337         #self.set_text()
338
339     def set_text(self):
340         """Set the text on the association end.
341         """
342         if self.subject:
343             n, m = self.subject.render()
344             self._name.set_text(n)
345             self._mult.set_text(m)
346             self.request_update()
347
348     def set_navigable(self, navigable):
349         """Change the AsociationEnd's navigability.
350
351         A warning is issued if the subject or opposite property is missing.
352         """
353         subject = self.subject
354         if subject and subject.opposite:
355             opposite = subject.opposite
356             if navigable:
357                 # Set owner to the class.
358                 if subject.owningAssociation:
359                     del subject.owningAssociation
360                 subject.class_ = opposite.type
361             else:
362                 if subject.class_:
363                     del subject.class_
364                 subject.owningAssociation = subject.association
365         else:
366             log.warning('AssociationEnd.set_navigable: %s missing' % \
367                         (subject and 'subject' or 'opposite Property'))
368
369     def update_labels(self, p1, p2):
370         """Update label placement for association's name and
371         multiplicity label. p1 is the line end and p2 is the last
372         but one point of the line.
373         """
374         ofs = 5
375
376         name_dx = 0.0
377         name_dy = 0.0
378         mult_dx = 0.0
379         mult_dy = 0.0
380
381         dx = float(p2[0]) - float(p1[0])
382         dy = float(p2[1]) - float(p1[1])
383        
384         name_w, name_h = map(max, self._name.to_pango_layout(True).get_pixel_size(), (10, 10))
385         mult_w, mult_h = map(max, self._mult.to_pango_layout(True).get_pixel_size(), (10, 10))
386
387         if dy == 0:
388             rc = 1000.0 # quite a lot...
389         else:
390             rc = dx / dy
391         abs_rc = abs(rc)
392         h = dx > 0 # right side of the box
393         v = dy > 0 # bottom side
394
395         if abs_rc > 6:
396             #print 'horizontal line'
397             if h:
398                 name_dx = ofs
399                 name_dy = -ofs - name_h # - height
400                 mult_dx = ofs
401                 mult_dy = ofs
402             else:
403                 name_dx = -ofs - name_w
404                 name_dy = -ofs - name_h # - height
405                 mult_dx = -ofs - mult_w
406                 mult_dy = ofs
407         elif 0 <= abs_rc <= 0.2:
408             #print 'vertical line'
409             if v:
410                 name_dx = -ofs - name_w # - width
411                 name_dy = ofs
412                 mult_dx = ofs
413                 mult_dy = ofs
414             else:
415                 name_dx = -ofs - name_w # - width
416                 name_dy = -ofs - name_h # - height
417                 mult_dx = ofs
418                 mult_dy = -ofs - mult_h # - height
419         else:
420             r = abs_rc < 1.0
421             align_left = (h and not r) or (r and not h)
422             align_bottom = (v and not r) or (r and not v)
423             if align_left:
424                 name_dx = ofs
425                 mult_dx = ofs
426             else:
427                 name_dx = -ofs - name_w # - width
428                 mult_dx = -ofs - mult_w # - width
429             if align_bottom:
430                 name_dy = -ofs - name_h # - height
431                 mult_dy = -ofs - name_h - mult_h # - height
432             else:
433                 name_dy = ofs
434                 mult_dy = ofs + mult_h # + height
435
436         self._name_bounds = (p1[0] + name_dx,
437                              p1[1] + name_dy,
438                              p1[0] + name_dx + name_w,
439                              p1[1] + name_dy + name_h)
440         self._name.set_pos((p1[0] + name_dx, p1[1] +