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

Revision 265, 13.7 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 # vim:sw=4:et
2 """DiagramItem provides basic functionality for presentations.
3 Such as a modifier 'subject' property and a unique id.
4 """
5
6 import gobject
7 from diacanvas import CanvasItem
8
9 import gaphor.misc.uniqueid as uniqueid
10 from gaphor.UML import Element, Presentation
11 from gaphor.UML.properties import association
12
13
14 class diagramassociation(association):
15     """Specialized association for use in diagram items.
16     It has the same interface as the association property defined in
17     gaphor/UML/properties.py, but delegates everything to the GObject property
18     handlers.
19     """
20
21     # TODO: Maybe we should not break our side of the association...
22     #       Since signals are still connected as the diagram item is unlinked.
23
24     def unlink(self, obj):
25         #print 'diagramassociation.unlink', obj, value
26         obj.preserve_property(self.name)
27         association.unlink(self, obj)
28
29     def _set2(self, obj, value):
30         #print 'diagramassociation._set2', obj, value
31         obj.preserve_property(self.name)
32         if obj.canvas and obj.canvas.in_undo and len(value.presentation) == 0:
33             print 'diagramassociation._set2(): relinking!'
34             value.relink()
35         return association._set2(self, obj, value)
36
37     def _del(self, obj, value):
38         #print 'diagramassociation._del', obj, value, value.id
39         obj.preserve_property(self.name)
40         # TODO: Add some extra notification here to tell the diagram
41         # item that the reference is about to be removed.
42         association._del(self, obj, value)
43         if len(value.presentation) == 0 or \
44            len(value.presentation) == 1 and obj in value.presentation:
45             #log.debug('diagramassociation._del: No more presentations: unlinking')
46             value.unlink()
47        
48
49 class DiagramItem(Presentation):
50     """Basic functionality for all model elements (lines and elements!).
51
52     This class contains common functionallity for model elements and
53     relationships.
54     It provides an interface similar to UML.Element for connecting and
55     disconnecting signals.
56
57     This class is not very useful on its own. It contains some glue-code for
58     diacanvas.DiaCanvasItem and gaphor.UML.Element.
59
60     Example:
61         class ModelElementItem(diacanvas.CanvasElement, DiagramItem):
62             connect = DiagramItem.connect
63             disconnect = DiagramItem.disconnect
64             ...
65     """
66     __gproperties__ = {
67         'subject':        (gobject.TYPE_PYOBJECT, 'subject',
68                          'subject held by the diagram item',
69                          gobject.PARAM_READWRITE),
70     }
71
72     __gsignals__ = {
73         '__unlink__': (gobject.SIGNAL_RUN_FIRST,
74                        gobject.TYPE_NONE, (gobject.TYPE_STRING,))
75     }
76
77     # Override the original subject as defined in UML.Presentation:
78     # Note that subject calls GObject.notify to emit changes
79     subject = diagramassociation('subject', Element, upper=1, opposite='presentation')
80
81     popup_menu = ()
82
83     def __init__(self, id=None):
84         Presentation.__init__(self)
85         self._id = id # or uniqueid.generate_id()
86
87         # Mapping to convert handlers to GObject signal ids.
88         self.__handler_to_id = { }
89
90         # Add the class' on_subject_notify() as handler:
91         self.connect('notify::subject', type(self).on_subject_notify)
92
93         # __the_subject is a backup that is used to disconnect signals when a
94         # new subject is set (or the original one is removed)
95         self.__the_subject = None
96
97     id = property(lambda self: self._id, doc='Id')
98
99     def do_set_property(self, pspec, value):
100         if pspec.name == 'subject':
101             #print 'set subject:', value
102             if value:
103                 self.subject = value
104             elif self.subject:
105                 del self.subject
106         else:
107             raise AttributeError, 'Unknown property %s' % pspec.name
108
109     def do_get_property(self, pspec):
110         if pspec.name == 'subject':
111             return self.subject
112         else:
113             raise AttributeError, 'Unknown property %s' % pspec.name
114
115     # UML.Element interface used by properties:
116
117     def save(self, save_func):
118         if self.subject:
119             save_func('subject', self.subject)
120
121     def load(self, name, value):
122         if name == 'subject':
123             #print 'loading subject', value
124             type(self).subject.load(self, value)
125         else:
126             #log.debug('Setting unknown property "%s" -> "%s"' % (name, value))
127             try:
128                 self.set_property(name, eval(value))
129             except:
130                 log.warning('%s has no property named %s (value %s)' % (self, name, value))
131
132     def postload(self):
133         if self.subject:
134             self.on_subject_notify(type(self).subject)
135
136     def unlink(self):
137         """Send the unlink signal and remove itself from the canvas.
138         """
139         #log.debug('DiagramItem.unlink(%s)' % self)
140         # emit the __unlink__ signal the way UML.Element would have done:
141         self.emit('__unlink__', '__unlink__')
142         # remove the subject if we have one
143         if self.subject:
144             del self.subject
145         self.set_property('parent', None)
146
147     def relink(self):
148         """Relinking is done by popping the undo stack...
149         """
150         log.info('RELINK DiagramItem')
151         #self.emit('__unlink__', '__relink__')
152
153     # gaphor.UML.Element like signal interface:
154
155     def connect(self, name, handler, *args):
156         """Connect a handler to signal name with args.
157         Note that in order to connect to the subject property, you have
158         to use "notify::subject".
159         A signal handler id is returned.
160         """
161         id = CanvasItem.connect(self, name, handler, *args)
162         key = (handler,) + args
163         try:
164             self.__handler_to_id[key].append(id)
165         except:
166             # it's a new entry:
167             self.__handler_to_id[key] = [id]
168         return id
169
170     def disconnect(self, handler_or_id, *args):
171         """Disconnect a signal handler. If handler_or_id is an integer (int)
172         it is expected to be the signal handler id. Otherwise
173         handler_or_id + *args are the same arguments passed to the connect()
174         method (except the signal name). The latter form is used by
175         gaphor.UML.Element.
176         """
177         if isinstance(handler_or_id, int):
178             CanvasItem.disconnect(self, handler_or_id)
179             for v in self.__handler_to_id.itervalues():
180                 if handler_or_id in v:
181                     v.remove(handler_or_id)
182                     break
183         else:
184             try:
185                 key = (handler_or_id,) + args
186                 ids = self.__handler_to_id[key]
187             except KeyError, e:
188                 log.error(e, e)
189             else:
190                 for id in ids:
191                     CanvasItem.disconnect(self, id)
192                 del self.__handler_to_id[key]
193
194     def notify(self, name, pspec=None):
195         CanvasItem.notify(self, name)
196
197     def save_property(self, save_func, name):
198         """Save a property, this is a shorthand method.
199         """
200         save_func(name, self.get_property(name))
201
202     def save_properties(self, save_func, *names):
203         """Save a property, this is a shorthand method.
204         """
205         for name in names:
206             self.save_property(save_func, name)
207
208     def get_popup_menu(self):
209         return self.popup_menu
210
211     def _subject_connect_helper(self, element, callback_prefix, prop_list):
212         """Connect a signal notifier. The notifier can be just the name of
213         one of the subjects properties.
214
215         See: DiagramItem.on_subject_notify()
216         """
217         prop = prop_list[0]
218         callback_name = '%s_%s' % (callback_prefix, prop)
219         if len(prop_list) == 1:
220             #log.debug('_subject_connect_helper - %s' % callback_name)
221             handler = getattr(self, callback_name)
222             element.connect(prop, handler)
223             # Call the handler, so it can update its state
224             #handler(prop, getattr(type(element), prop))
225             #handler(element, getattr(type(element), prop))
226         else:
227             p = getattr(element, prop)
228             #log.debug('_subject_connect_helper 2 - %s' % prop)
229             pl = prop_list[1:]
230             element.connect(prop, self._on_subject_notify_helper, callback_name, pl, p)
231             if p:
232                 self._subject_connect_helper(p, callback_name, pl)
233             else:
234                 pass
235
236     def _subject_disconnect_helper(self, element, callback_prefix, prop_list):
237         """Disconnect a previously connected signal handler.
238
239         See: DiagramItem.on_subject_notify()
240         """
241         prop = prop_list[0]
242         callback_name = '%s_%s' % (callback_prefix, prop)
243         if len(prop_list) == 1:
244             #log.debug('_subject_disconnect_helper - %s' % callback_name)
245             handler = getattr(self, callback_name)
246             element.disconnect(handler)
247             # Call the handler, so it can update its state
248             #handler(element, getattr(type(element), prop))
249         else:
250             p = getattr(element, prop)
251             #log.debug('_subject_disconnect_helper 2 - %s' % prop)
252             pl = prop_list[1:]
253             element.disconnect(self._on_subject_notify_helper, callback_name, pl, p)
254             if p:
255                 self._subject_disconnect_helper(p, callback_name, pl)
256             else:
257                 # TODO: Maybe do an update here to, if p is None.
258                 pass
259
260     def _on_subject_notify_helper(self, element, pspec, callback_name, prop_list, old):
261         """This signal handler handles signals that are not direct properties
262         of self.subject (e.g. 'subject.lowerValue.value'). This way the presentation class is
263         not bothered with the details of keeping track of those properties.
264
265         NOTE: This only works for properties with multiplicity [0..1] or [1].
266
267         See: DiagramItem.on_subject_notify()
268         """
269         name = pspec.name
270         prop = getattr(element, name)
271         if prop is not old:
272             # Attach a new signal handler with the new 'old' value:
273             if old:
274                 self._subject_disconnect_helper(old, callback_name, prop_list)
275             if prop:
276                 self._subject_connect_helper(prop, callback_name, prop_list)
277
278     def on_subject_notify(self, pspec, notifiers=()):
279         """A new subject is set on this model element.
280         notifiers is an optional tuple of elements that also need a
281         callback function. Callbacks have the signature
282         on_subject_notify__<notifier>(self, subject, pspec).
283
284         A notifier can be a property of subject (e.g. 'name') or a property
285         of a property of subject (e.g. 'lowerValue.value').
286         """
287         #log.info('DiagramItem.on_subject_notify: %s' % self.__subject_notifier_ids)
288         # First, split all notifiers on '.'
289         callback_prefix = 'on_subject_notify_'
290         notifiers = map(str.split, notifiers, ['.'] * len(notifiers))
291         old_subject = self.__the_subject
292         subject_connect_helper = self._subject_connect_helper
293         subject_disconnect_helper = self._subject_disconnect_helper
294
295         # TODO: Queue signal emissions.
296         if old_subject:
297             for n in notifiers:
298                 #self._subject_disconnect(self.__the_subject, n)
299                 subject_disconnect_helper(old_subject, callback_prefix, n)
300
301         if self.subject:
302             subject = self.__the_subject = self.subject
303             for n in notifiers:
304                 #log.debug('DiaCanvasItem.on_subject_notify: %s' % signal)
305                 #self._subject_connect(self.subject, n)
306                 subject_connect_helper(subject, callback_prefix, n)
307
308         self.request_update()
309
310     # DiaCanvasItem callbacks
311
312     def _on_glue(self, handle, wx, wy, parent_class):
313         """This function is used to notify the connecting item
314         about being connected. handle.owner.allow_connect_handle() is
315         called to determine if a connection is allowed.
316         """
317         if handle.owner.allow_connect_handle (handle, self):
318             #print self.__class__.__name__, 'Glueing allowed.'
319             return parent_class.on_glue (self, handle, wx, wy)
320         #else:
321             #print self.__class__.__name__, 'Glueing NOT allowed.'
322         # Dummy value with large distance value
323         return None
324
325     def _on_connect_handle (self, handle, parent_class):
326         """This function is used to notify the connecting item
327         about being connected. handle.owner.allow_connect_handle() is
328         called to determine if a connection is allowed. If the connection
329         succeeded handle.owner.confirm_connect_handle() is called.
330         """
331         if handle.owner.allow_connect_handle (handle, self):
332             #print self.__class__.__name__, 'Connection allowed.'
333             ret = parent_class.on_connect_handle (self, handle)
334             if ret != 0:
335                 handle.owner.confirm_connect_handle(handle)
336                 return ret
337         #else:
338             #print self.__class__.__name__, 'Connection NOT allowed.'
339         return 0
340
341     def _on_disconnect_handle (self, handle, parent_class):
342         """Use this function to disconnect handles. It notifies
343         the connected item about being disconnected.
344         handle.owner.allow_disconnect_handle() is
345         called to determine if a connection is allowed to be removed.
346         If the disconnect succeeded handle.owner.confirm_connect_handle()
347         is called.
348         """
349         if handle.owner.allow_disconnect_handle (handle):
350             #print self.__class__.__name__, 'Disconnecting allowed.'
351             ret = parent_class.on_disconnect_handle (self, handle)
352             if ret != 0:
353                 handle.owner.confirm_disconnect_handle(handle, self)
354                 return ret
355         #else:
356             #print self.__class__.__name__, 'Disconnecting NOT allowed.'
357         return 0
358
Note: See TracBrowser for help on using the browser.