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

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

Move need_sync logic from features to classes. Feature was expecting obsolete "parent" attribute in the update context.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 """
2 Classifier diagram item.
3 """
4
5 from gaphas.state import observed, reversible_property
6
7 from gaphor import UML
8 from gaphor.diagram.nameditem import NamedItem
9
10 import font
11
12 class Compartment(list):
13     """
14     Specify a compartment in a class item.
15     A compartment has a line on top and a list of FeatureItems.
16     """
17
18     def __init__(self, name, owner):
19         self.name = name
20         self.owner = owner
21         self.visible = True
22         self.width = 0
23         self.height = 0
24
25     def save(self, save_func):
26         #log.debug('Compartment.save: %s' % self)
27         for item in self:
28             save_func(None, item)
29
30     def get_need_sync(self):
31         for item in self:
32             if item.need_sync:
33                 return True
34
35     need_sync = property(get_need_sync)
36
37     def has_item(self, item):
38         """
39         Check if the compartment already contains an item with the
40         same subject as item.
41         """
42         s = item.subject
43         local_elements = [f.subject for f in self]
44         return s and s in local_elements
45
46     def get_size(self):
47         """
48         Get width, height of the compartment. pre_update should have
49         been called so widthand height have been calculated.
50         """
51         if self.visible:
52             return self.width, self.height
53         else:
54             return 0, 0
55
56     def pre_update(self, context):
57         """
58         Pre update, determine width and height of the compartment.
59         """
60         self.width = self.height = 0
61         cr = context.cairo
62         for item in self:
63             item.pre_update(context)
64        
65         if self:
66             # self (=list) contains items
67             sizes = [ (0, 0) ] # to not throw exceptions by max and sum
68             sizes.extend(f.get_size(True) for f in self)
69             self.width = max(size[0] for size in sizes)
70             self.height = sum(size[1] for size in sizes)
71             vspacing = self.owner.style.compartment_vspacing
72             self.height += vspacing * (len(sizes) - 1)
73
74         padding = self.owner.style.compartment_padding
75         self.width += padding[1] + padding[3]
76         self.height += padding[0] + padding[2]
77
78     def post_update(self, context):
79         for item in self:
80             item.post_update(context)
81
82     def draw(self, context):
83         cr = context.cairo
84         padding = self.owner.style.compartment_padding
85         vspacing = self.owner.style.compartment_vspacing
86         cr.translate(padding[1], padding[0])
87         offset = 0
88         for item in self:
89             cr.save()
90             try:
91                 cr.translate(0, offset)
92                 #cr.move_to(0, offset)
93                 item.draw(context)
94                 offset += vspacing + item.height
95             finally:
96                 cr.restore()
97
98     def item_at(self, x, y):
99         if 0 > x > self.width:
100             return None
101        
102         padding = self.owner.style.compartment_padding
103         height = padding[0]
104         if y < height:
105             return None
106
107         vspacing = self.owner.style.compartment_vspacing
108         for f in self:
109             w, h = f.get_size(True)
110             height += h + vspacing
111             if y < height:
112                 return f
113         return None
114
115
116 class ClassifierItem(NamedItem):
117     """
118     This item visualizes a Class instance.
119
120     A ClassifierItem is a superclass for (all) Classifier like objects,
121     such as Class, Interface, Component and Actor.
122
123     ClassifierItem controls the stereotype, namespace and owning package.
124
125     A classifier has three drawing style (ClassifierItem.drawing_style):
126      - The comparttment view, as often used by Classes
127      - A compartment view, but with a little stereotype icon in the right corner
128      - One big icon, as used by Actors and sometimes interfaces.
129
130     To support this behavior a few helper methods are defined which can be
131     called/overridden:
132      - update_compartment_icon (box-style with small icon (see ComponentItem))
133      - update_icon (does nothing by default, an impl. should be provided by
134                     subclasses (see ActorItem))
135     """
136
137     # Do not use preset drawing style
138     DRAW_NONE = 0
139     # Draw the famous box style
140     DRAW_COMPARTMENT = 1
141     # Draw compartment with little icon in upper right corner
142     DRAW_COMPARTMENT_ICON = 2
143     # Draw as icon
144     DRAW_ICON = 3
145
146     __style__ = {
147         'min-size': (100, 50),
148         'icon-size': (20, 20),
149         'from-padding': (7, 2, 7, 2),
150         'compartment-padding': (5, 5, 5, 5), # (top, right, bottom, left)
151         'compartment-vspacing': 3,
152         'name-padding': (10, 10, 10, 10),
153         'stereotype-padding': (10, 10, 2, 10),
154     }
155     # Default size for small icons
156     ICON_WIDTH    = 15
157     ICON_HEIGHT   = 25
158     ICON_MARGIN_X = 10
159     ICON_MARGIN_Y = 10
160
161     def __init__(self, id=None):
162         NamedItem.__init__(self, id)
163         self._compartments = []
164
165         self._drawing_style = ClassifierItem.DRAW_NONE
166
167
168     def save(self, save_func):
169         # Store the show- properties *before* the width/height properties,
170         # otherwise the classes will unintentionally grow due to "visible"
171         # attributes or operations.
172         self.save_property(save_func, 'drawing-style')
173         NamedItem.save(self, save_func)
174
175     def postload(self):
176         NamedItem.postload(self)
177         self.on_subject_notify__isAbstract(self.subject)
178
179     @observed
180     def set_drawing_style(self, style):
181         """
182         Set the drawing style for this classifier: DRAW_COMPARTMENT,
183         DRAW_COMPARTMENT_ICON or DRAW_ICON.
184         """
185         if style != self._drawing_style:
186             self._drawing_style = style
187             self.request_update()
188 #            if self.canvas:
189 #                request_resolve = self.canvas.solver.request_resolve
190 #                for h in self._handles:
191 #                    request_resolve(h.x)
192 #                    request_resolve(h.y)
193
194         if self._drawing_style == self.DRAW_COMPARTMENT:
195             self.draw       = self.draw_compartment
196             self.pre_update = self.pre_update_compartment
197             self.post_update = self.post_update_compartment
198
199         elif self._drawing_style == self.DRAW_COMPARTMENT_ICON:
200             self.draw       = self.draw_compartment_icon
201             self.pre_update = self.pre_update_compartment_icon
202             self.post_update     = self.post_update_compartment_icon
203
204         elif self._drawing_style == self.DRAW_ICON:
205             self.draw       = self.draw_icon
206             self.pre_update = self.pre_update_icon
207             self.post_update     = self.post_update_icon
208
209
210     drawing_style = reversible_property(lambda self: self._drawing_style, set_drawing_style)
211
212
213     def create_compartment(self, name):
214         """
215         Create a new compartment. Compartments contain data such as
216         attributes and operations.
217
218         It is common to create compartments during the construction of the
219         diagram item. Their visibility can be toggled by Compartment.visible.
220         """
221         c = Compartment(name, self)
222         self._compartments.append(c)
223         return c
224
225     compartments = property(lambda s: s._compartments)
226
227     def sync_uml_elements(self, elements, compartment, creator=None):
228         """
229         This method synchronized a list of elements with the items
230         in a compartment. A creator-function should be passed which is used
231         for creating new compartment items.
232
233         @elements: the list of attributes or operations in the model
234         @compartment: our local representation
235         @creator: factory method for creating new attr. or oper.'s
236         """
237         # extract the UML elements from the compartment
238         local_elements = [f.subject for f in compartment]
239
240         # map local element with compartment element
241         mapping = dict(zip(local_elements, compartment))
242
243         to_add = [el for el in elements if el not in local_elements]
244
245         # sync local elements with elements
246         del compartment[:]
247
248         for el in elements:
249             if el in to_add:
250                 creator(el)
251             else:
252                 compartment.append(mapping[el])
253
254         #log.debug('elements order in model: %s' % [f.name for f in elements])
255         #log.debug('elements order in diagram: %s' % [f.subject.name for f in compartment])
256         assert tuple([f.subject for f in compartment]) == tuple(elements)
257
258         self.request_update()
259
260
261     def on_subject_notify(self, pspec, notifiers=()):
262         #log.debug('Class.on_subject_notify(%s, %s)' % (pspec, notifiers))
263         NamedItem.on_subject_notify(self, pspec, ('isAbstract',) + notifiers)
264         # Create already existing attributes and operations:
265         if self.subject:
266             self.on_subject_notify__isAbstract(self.subject)
267         self.request_update()
268
269     def on_subject_notify__isAbstract(self, subject, pspec=None):
270         self._name.font = subject.isAbstract \
271                 and font.FONT_ABSTRACT_NAME or font.FONT_NAME
272         self.request_update()
273
274
275     def pre_update_compartment_icon(self, context):
276         self.pre_update_compartment(context)
277         # icon width plus right margin
278         self.min_width = max(self.min_width,
279                 self._header_size[0] + self.ICON_WIDTH + 10)
280
281     def pre_update_icon(self, context):
282         super(ClassifierItem, self).pre_update(context)
283
284     def pre_update_compartment(self, context):
285         """
286         Update state for box-style presentation.
287
288         Calculate minimal size, which is based on header and comparments
289         sizes.
290         """
291         super(ClassifierItem, self).pre_update(context)
292
293         for comp in self._compartments:
294             comp.pre_update(context)
295
296         sizes = [comp.get_size() for comp in self._compartments]
297         sizes.append((self.min_width, self._header_size[1]))
298
299         self.min_width = max(size[0] for size in sizes)
300         h = sum(size[1] for size in sizes)
301         self.min_height = max(self.style.min_size[1], h)
302
303
304     def post_update_compartment_icon(self, context):
305         """
306         Update state for box-style w/ small icon.
307         """
308         super(ClassifierItem, self).post_update(context)
309
310     def post_update_icon(self, context):
311         """
312         Update state for icon-only presentation.
313         """
314         super(ClassifierItem, self).post_update(context)
315
316     def post_update_compartment(self, context):
317         super(ClassifierItem, self).post_update(context)
318
319         assert self.width >= self.min_width, 'failed %s >= %s' % (self.width, self.min_width)
320         assert self.height >= self.min_height, 'failed %s >= %s' % (self.height, self.min_height)
321
322
323     def get_icon_pos(self):
324         """
325         Get icon position.
326         """
327         return self.width - self.ICON_MARGIN_X - self.ICON_WIDTH, \
328             self.ICON_MARGIN_Y
329
330
331     def draw_compartment(self, context):
332         super(ClassifierItem, self).draw(context)
333
334         cr = context.cairo
335
336         cr.rectangle(0, 0, self.width, self.height)
337         cr.stroke()
338
339         # make room for name, stereotype, etc.
340         y = self._header_size[1]
341         cr.translate(0, y)
342
343         if self._drawing_style == self.DRAW_COMPARTMENT_ICON:
344             width = self.width - self.ICON_WIDTH
345         else:
346             width = self.width
347
348         # draw compartments
349         for comp in self._compartments:
350             if not comp.visible:
351                 continue
352             cr.save()
353             cr.move_to(0, 0)
354             cr.line_to(self.width, 0)
355             cr.stroke()
356             try:
357                 comp.draw(context)
358             finally:
359                 cr.restore()
360             cr.translate(0, comp.height)
361
362
363     def item_at(self, x, y):
364         """
365         Find the composite item (attribute or operation) for the
366         classifier.
367         """
368
369         if self.drawing_style not in (self.DRAW_COMPARTMENT, self.DRAW_COMPARTMENT_ICON):
370             return self
371
372         header_height = self._header_size[1]
373
374         compartments = [ comp for comp in self.compartments if comp.visible]
375
376         # Edit is in name compartment -> edit name
377         if y < header_height or not len(compartments):
378             return self
379
380         padding = self.style.compartment_padding
381         vspacing = self.style.compartment_vspacing
382        
383         # place offset at top of first comparement
384         y -= header_height
385         y += vspacing / 2.0
386         for comp in compartments:
387             item = comp.item_at(x, y)
388             if item:
389                 return item
390             y -= comp.height
391         return None
392
393 # vim:sw=4:et
Note: See TracBrowser for help on using the browser.