root/gaphor/tags/gaphor-0.12.0/gaphor/adapters/connectors.py

Revision 2092, 46.4 kB (checked in by arj..@yirdis.nl, 1 year ago)

add extra check for constraint removal, so the undo system is not upset by None constraints.

Line 
1 """
2 Adapters
3 """
4
5 from zope import interface, component
6
7 from gaphas.item import NW, NE, SW, SE
8 from gaphas.canvas import CanvasProjection
9 from gaphas.matrix import Matrix
10 from gaphas import geometry
11 from gaphas import constraint
12 from gaphor import UML
13 from gaphor.core import inject
14 from gaphor.diagram.interfaces import IConnect
15 from gaphor.diagram import items
16 from gaphor.misc.ipair import ipair
17
18 class AbstractConnect(object):
19     """
20     Connection adapter for Gaphor diagram items.
21
22     Line item ``line`` connects with a handle to a connectable item ``element``.
23
24     Line constraint is created between ``line`` handle and two handles of ``element``
25     (called segment, see ``AbstractConnect._get_segment`` method).
26
27     Attributes:
28
29     - line: connecting item
30     - element: connectable item
31     - _canvas: canvas reference
32     - _matrix_e2l: element to line matrix
33     - _matrix_l2e: line to element matrix
34     """
35     interface.implements(IConnect)
36
37     def __init__(self, element, line):
38         self.element = element
39         self.line = line
40         canvas = self._canvas = element.canvas
41
42         i2c = canvas.get_matrix_i2c
43         c2i = canvas.get_matrix_c2i
44         self._matrix_e2l = i2c(element) * c2i(line)
45         self._matrix_l2e = i2c(line) * c2i(element)
46
47         # code below returns None from transform_point... why?
48         # self._matrix_l2e = Matrix(*self._matrix_e2l)
49         # self._matrix_l2e.invert()
50
51
52     def glue(self, handle):
53         """
54         Return the point the handle could connect to. None if no connection
55         is allowed.
56         """
57         raise NotImplemented, 'Implement glue() in the subclass'
58
59     def connect(self, handle):
60         """
61         Connect to an element. Note that at this point the line may
62         be connected to some other, or the same element by means of the
63         handle.connected_to property. Also the connection at UML level
64         still exists.
65         
66         Returns True if a connection is established.
67         """
68         element = self.element
69         canvas = element.canvas
70         solver = canvas.solver
71
72         # Disconnect old model connection
73         if handle.connected_to and handle.connected_to is not self.element:
74             handle.disconnect()
75  
76         # Save guard: only connect if it's permitted by glue()
77         #   -> glue() does not return None
78         if not self.glue(handle):
79             return False
80
81         self.connect_constraints(handle)
82
83         # Set disconnect handler in the adapter, so it will also wotk if
84         # connections are created programmatically.
85         def _disconnect():
86             self.disconnect(handle)
87             handle.disconnect = lambda: 0
88         handle.disconnect = _disconnect
89
90         return True
91
92     def disconnect(self, handle):
93         """
94         Do a full disconnect, also disconnect at UML model level.
95         Subclasses should disconnect model-level connections.
96         """
97         self.disconnect_constraints(handle)
98         handle.connected_to = None
99
100
101     def connect_constraints(self, handle):
102         """
103         Create the actual constraint. The handle should be moved into connection
104         position before this method is called.
105         """
106         h1, h2 = self._get_segment(handle)
107
108         element = self.element
109         line = self.line
110
111         lc = constraint.LineConstraint(line=(CanvasProjection(h1.pos, element),
112                                              CanvasProjection(h2.pos, element)),
113                                        point=CanvasProjection(handle.pos, line))
114         handle._connect_constraint = lc
115         element.canvas.solver.add_constraint(lc)
116
117         handle.connected_to = element
118
119
120     def disconnect_constraints(self, handle):
121         """
122         Disconnect() takes care of disconnecting the handle from the
123         element it's attached to, by removing the constraints.
124         """
125         try:
126             if handle._connect_constraint:
127                 self.line.canvas.solver.remove_constraint(handle._connect_constraint)
128         except AttributeError:
129             pass # No _connect_constraint property yet
130         handle._connect_constraint = None
131
132
133     def _get_segment(self, handle):
134         """
135         Get line segment, to which handle should connect to. Line segment is defined by two handles.
136         Examples of line segmets
137         - side of element item like class, action, etc.
138         - line segment of association
139         - lifeline's lifetime
140         """
141         raise NotImplemented()
142
143
144
145 class ElementConnect(AbstractConnect):
146     """
147     Base class for connecting a line to an ElementItem class.
148     """
149
150     def _get_segment(self, handle):
151         """
152         Determine the side on which the handle is connecting.
153         This is done by determining the proximity to the nearest edge.
154
155         Handles of one of the sides is returned.
156         """
157         handles = self.element.handles()
158         l2e = self._matrix_l2e
159         hx, hy = l2e.transform_point(*handle.pos)
160         ax, ay = handles[NW].x, handles[NW].y
161         bx, by = handles[SE].x, handles[SE].y
162
163         if abs(hx - ax) < 0.01:
164             return handles[NW], handles[SW]
165         elif abs(hy - ay) < 0.01:
166             return handles[NW], handles[NE]
167         elif abs(hx - bx) < 0.01:
168             return handles[NE], handles[SE]
169         else:
170             return handles[SW], handles[SE]
171         assert False
172
173
174     def bounds(self, element):
175         """
176         Returns bounds of the element that we're connecting to.
177         """
178         h = element.handles()
179         return map(float, (h[NW].pos + h[SE].pos))
180
181
182     def glue(self, handle):
183         """
184         Return the point the handle could connect to. None if no connection
185         is allowed.
186         """
187         l2e = self._matrix_l2e
188         e2l = self._matrix_e2l
189
190         pos = l2e.transform_point(*handle.pos)
191         bounds = self.bounds(self.element)
192
193         pos = geometry.point_on_rectangle(bounds, pos, border=True)
194         pos = e2l.transform_point(*pos)
195         return pos
196
197
198
199 class LineConnect(AbstractConnect):
200     """
201     Base class for connecting two lines to each other.
202     The line that is conencted to is called 'element', as in ElementConnect.
203
204     Once a line has been connected at both ends, and a model element is
205     assigned to it, all items connectedt to this line (e.g. Comments)
206     receive a connect() call. This allows already connected lines to set
207     up relationships at model level too.
208     """
209
210     def _get_segment_data(self, handle):
211         """
212         Get segment data
213         - pos: position on a segment
214         - h1: handle starting segment
215         - h2: handle ending segment
216         """
217         dlp = geometry.distance_line_point
218
219         handles = self.element.handles()
220
221         def segment(h1, h2, pos):
222             d, pos = dlp(h1.pos, h2.pos, pos)
223             return d, pos, h1, h2
224
225         pos = self._matrix_l2e.transform_point(*handle.pos)
226
227         # find the nearest segment from handle
228         data = (segment(h1, h2, pos) for h1, h2 in ipair(handles))
229         # No key needed, distance is first
230         d, pos, h1, h2 = min(data) #, key=lambda s: s[0])
231         return pos, h1, h2
232
233
234     def _get_segment(self, handle):
235         """
236         Return diagram line segment closest to a handle.
237         """
238         _, h1, h2 = self._get_segment_data(handle)
239         return h1, h2
240
241
242     def glue(self, handle):
243         """
244         Return a point on a connectable element closest to a handle.
245         """
246         pos, _, _ = self._get_segment_data(handle)
247         return self._matrix_e2l.transform_point(*pos)
248
249
250
251 class CommentLineElementConnect(ElementConnect):
252     """
253     Connect a comment line to any element item.
254     """
255     component.adapts(items.ElementItem, items.CommentLineItem)
256
257     def glue(self, handle):
258         """
259         In addition to the normal check, both line ends may not be connected
260         to the same element. Same goes for subjects.
261         One of the ends should be connected to a UML.Comment element.
262         """
263         opposite = self.line.opposite(handle)
264         element = self.element
265         connected_to = opposite.connected_to
266         if connected_to is element:
267             return None
268
269         # Same goes for subjects:
270         if connected_to and \
271                 (not (connected_to.subject or element.subject)) \
272                  and connected_to.subject is element.subject:
273             #print 'Subjects none or match:', connected_to.subject, element.subject
274             return None
275
276         # One end should be connected to a CommentItem:
277         cls = items.CommentItem
278         glue_ok = isinstance(connected_to, cls) ^ isinstance(self.element, cls)
279         if connected_to and not glue_ok:
280             return None
281
282         return super(CommentLineElementConnect, self).glue(handle)
283
284     def connect(self, handle):
285         if super(CommentLineElementConnect, self).connect(handle):
286             opposite = self.line.opposite(handle)
287             if opposite.connected_to:
288                 if isinstance(opposite.connected_to.subject, UML.Comment):
289                     opposite.connected_to.subject.annotatedElement = self.element.subject
290                 else:
291                     self.element.subject.annotatedElement = opposite.connected_to.subject
292
293     def disconnect(self, handle):
294         opposite = self.line.opposite(handle)
295         if handle.connected_to and opposite.connected_to:
296             if isinstance(opposite.connected_to.subject, UML.Comment):
297                 del opposite.connected_to.subject.annotatedElement[handle.connected_to.subject]
298             else:
299                 del handle.connected_to.subject.annotatedElement[opposite.connected_to.subject]
300         super(CommentLineElementConnect, self).disconnect(handle)
301
302 component.provideAdapter(CommentLineElementConnect)
303
304
305 class CommentLineLineConnect(LineConnect):
306     """
307     Connect a comment line to any diagram line.
308     """
309     component.adapts(items.DiagramLine, items.CommentLineItem)
310
311     def glue(self, handle):
312         """
313         In addition to the normal check, both line ends may not be connected
314         to the same element. Same goes for subjects.
315         One of the ends should be connected to a UML.Comment element.
316         """
317         opposite = self.line.opposite(handle)
318         element = self.element
319         connected_to = opposite.connected_to
320
321         # do not connect to the same item nor connect to other comment line
322         if connected_to is element or isinstance(element, items.CommentLineItem):
323             return None
324
325         # Same goes for subjects:
326         if connected_to and \
327                 (not (connected_to.subject or element.subject)) \
328                  and connected_to.subject is element.subject:
329             return None
330
331         # One end should be connected to a CommentItem:
332         cls = items.CommentItem
333         glue_ok = isinstance(connected_to, cls) ^ isinstance(self.element, cls)
334         if connected_to and not glue_ok:
335             return None
336
337         return super(CommentLineLineConnect, self).glue(handle)
338
339     def connect(self, handle):
340         if super(CommentLineLineConnect, self).connect(handle):
341             opposite = self.line.opposite(handle)
342             if opposite.connected_to and self.element.subject:
343                 if isinstance(opposite.connected_to.subject, UML.Comment):
344                     opposite.connected_to.subject.annotatedElement = self.element.subject
345                 else:
346                     self.element.subject.annotatedElement = opposite.connected_to.subject
347
348     def disconnect(self, handle):
349         opposite = self.line.opposite(handle)
350         if handle.connected_to and opposite.connected_to:
351             if isinstance(handle.connected_to.subject, UML.Comment):
352                 del handle.connected_to.subject.annotatedElement[opposite.connected_to.subject]
353             elif opposite.connected_to.subject:
354                 del opposite.connected_to.subject.annotatedElement[handle.connected_to.subject]
355         super(CommentLineLineConnect, self).disconnect(handle)
356
357 component.provideAdapter(CommentLineLineConnect)
358
359
360 class RelationshipConnect(ElementConnect):
361     """
362     Base class for relationship connections, such as associations,
363     dependencies and implementations.
364
365     This class introduces a new method: relationship() which is used to
366     find an existing relationship in the model that does not yet exist
367     on the canvas.
368     """
369
370     element_factory = inject('element_factory')
371
372     def relationship(self, required_type, head, tail):
373         """
374         Find an existing relationship in the model that meets the
375         required type and is connected to the same model element the head
376         and tail of the line are conncted to.
377
378         type - the type of relationship we're looking for
379         head - tuple (association name on line, association name on element)
380         tail - tuple (association name on line, association name on element)
381         """
382         line = self.line
383
384         head_subject = line.head.connected_to.subject
385         tail_subject = line.tail.connected_to.subject
386
387         edge_head_name = head[0]
388         node_head_name = head[1]
389         edge_tail_name = tail[0]
390         node_tail_name = tail[1]
391
392         # First check if the right subject is already connected:
393         if line.subject \
394            and getattr(line.subject, edge_head_name) is head_subject \
395            and getattr(line.subject, edge_tail_name) is tail_subject:
396             return line.subject
397
398         # This is the type of the relationship we're looking for
399         #required_type = getattr(type(tail_subject), node_tail_name).type
400
401         # Try to find a relationship, that is already created, but not
402         # yet displayed in the diagram.
403         for gen in getattr(tail_subject, node_tail_name):
404             if not isinstance(gen, required_type):
405                 continue
406                
407             gen_head = getattr(gen, edge_head_name)
408             try:
409                 if not head_subject in gen_head:
410                     continue
411             except TypeError:
412                 if not gen_head is head_subject:
413                     continue
414
415             # check for this entry on line.canvas
416             for item in gen.presentation:
417                 # Allow line to be returned. Avoids strange
418                 # behaviour during loading
419                 if item.canvas is line.canvas and item is not line:
420                     break
421             else:
422                 return gen
423         return None
424
425     def relationship_or_new(self, type, head, tail):
426         """
427         Like relation(), but create a new instance of none was found.
428         """
429         relation = self.relationship(type, head, tail)
430         if not relation:
431             line = self.line
432             relation = self.element_factory.create(type)
433             setattr(relation, head[0], line.head.connected_to.subject)
434             setattr(relation, tail[0], line.tail.connected_to.subject)
435         return relation
436
437     def connect_connected_items(self, connected_items=None):
438         """
439         Cause items connected to ``line`` to reconnect, allowing them to
440         establish or destroy relationships at model level.
441         """
442         line = self.line
443         canvas = line.canvas
444         solver = canvas.solver
445
446         # First make sure coordinates match
447         solver.solve()
448         for item, handle in connected_items or line.canvas.get_connected_items(line):
449             adapter = component.queryMultiAdapter((line, item), IConnect)
450             assert adapter
451             adapter.connect(handle)
452        
453     def disconnect_connected_items(self):
454         """
455         Cause items connected to @line to be disconnected.
456         This is nessesary if the subject of the @line is to be removed.
457
458         Returns a list of (item, handle) pairs that were connected (this
459         list can be used to connect items again with connect_connected_items()).
460         """
461         line = self.line
462         canvas = line.canvas
463         solver = canvas.solver
464
465         # First make sure coordinates match
466         solver.solve()
467         connected_items = list(line.canvas.get_connected_items(line))
468         for item, handle in connected_items:
469             adapter = component.queryMultiAdapter((line, item), IConnect)
470             assert adapter
471             adapter.disconnect(handle)
472         return connected_items
473
474     def connect_subject(self, handle):
475         """
476         Establish the relationship at model level.
477         """
478         raise NotImplemented, 'Implement connect_subject() in a subclass'
479
480     def disconnect_subject(self, handle):
481         """
482         Disconnect the diagram item from its model element. If there are
483         no more presentations(diagram items) connected to the model element,
484         unlink() it too.
485         """
486         line = self.line
487         old = line.subject
488         del line.subject
489         if old and len(old.presentation) == 0:
490             old.unlink()
491
492     def connect(self, handle):
493         """
494         Connect the items to each other. The model level relationship
495         is created by create_subject()
496         """
497         if super(RelationshipConnect, self).connect(handle):
498             opposite = self.line.opposite(handle)
499             if opposite.connected_to:
500                 self.connect_subject(handle)
501                 line = self.line
502                 if line.subject:
503                     self.connect_connected_items()
504
505     def disconnect(self, handle):
506         """
507         Disconnect model element.
508         """
509         line = self.line
510         opposite = line.opposite(handle)
511         if handle.connected_to and opposite.connected_to:
512             old = line.subject
513              
514             connected_items = self.disconnect_connected_items()
515            
516             self.disconnect_subject(handle)
517             if old:
518                 self.connect_connected_items(connected_items)
519
520         super(RelationshipConnect, self).disconnect(handle)
521