root/gaphor/trunk/gaphor/diagram/textelement.py

Revision 2290, 10.7 kB (checked in by arj..@yirdis.nl, 8 months ago)

set default style to normal. make name style for classifiers and package bold.

Fixes #118.

Line 
1 """
2 Support for editable text, a part of a diagram item, i.e. name of named
3 item, guard of flow item, etc.
4 """
5
6 from gaphor.diagram.style import Style
7 from gaphor.diagram.style import ALIGN_CENTER, ALIGN_TOP
8 import gaphor.diagram.font as font
9
10 from gaphas.geometry import distance_rectangle_point, Rectangle
11 from gaphas.util import text_extents, text_align, text_multiline, \
12     text_set_font
13
14
15 def swap(list, el1, el2):
16     """
17     Swap two elements on the list.
18     """
19     i1 = list.index(el1)
20     i2 = list.index(el2)
21     list[i1] = el2
22     list[i2] = el1
23
24
25 class EditableTextSupport(object):
26     """
27     Editable text support to allow display and edit text parts of a diagram
28     item.
29
30     Attributes:
31      - _texts:       list of diagram item text elements
32      - _text_groups: grouping information of text elements (None - ungrouped)
33     """
34     def __init__(self):
35         self._texts = []
36         self._text_groups = { None: [] }
37         self._text_groups_sizes = {}
38
39     def postload(self):
40         super(EditableTextSupport, self).postload()
41
42     def texts(self):
43         """
44         Return list of diagram item text elements.
45         """
46         return self._texts
47
48
49     def add_text(self, attr, style=None, pattern=None, visible=None,
50             editable=False, font=font.FONT):
51         """
52         Create and add a text element.
53
54         For parameters description and more information see TextElement
55         class documentation.
56
57         If style information contains 'text-align-group' data, then text
58         element is grouped.
59
60         Returns created text element.
61         """
62         txt = TextElement(attr, style=style, pattern=pattern,
63                 visible=visible, editable=editable, font=font)
64         self._texts.append(txt)
65
66         # try to group text element
67         gname = style and style.get('text-align-group') or None
68         if gname not in self._text_groups:
69             self._text_groups[gname] = []
70         group = self._text_groups[gname]
71         group.append(txt)
72
73         return txt
74
75
76     def remove_text(self, txt):
77         """
78         Remove a text element from diagram item.
79
80         Parameters:
81         - txt: text to be removed
82         """
83         # remove from align group
84         style = txt.style
85         if style and hasattr(style, 'text_align_group'):
86             gname = style.text_align_group
87         else:
88             gname = None
89
90         group = self._text_groups[gname]
91         group.remove(txt)
92
93         # remove text element from diagram item
94         self._texts.remove(txt)
95
96
97     def swap_texts(self, txt1, txt2):
98         """
99         Swap two text elements.
100         """
101         swap(self._texts, txt1, txt2)
102
103         style = txt1.style
104         if style and hasattr(style, 'text_align_group'):
105             gname = style.text_align_group
106         else:
107             gname = None
108
109         group = self._text_groups[gname]
110         swap(group, txt1, txt2)
111
112
113     def _get_visible_texts(self, texts):
114         """
115         Get list of visible texts.
116         """
117         return [txt for txt in texts if txt.is_visible()]
118
119
120     def _get_text_groups(self):
121         """
122         Get text groups.
123         """
124         tg = self._text_groups
125         groups = self._text_groups
126         return ((name, tg[name]) for name in groups if name)
127
128
129     def _set_text_sizes(self, context, texts):
130         """
131         Calculate size for every text in the list.
132
133         Parameters:
134          - context: cairo context
135          - texts:   list of texts
136         """
137         cr = context.cairo
138         for txt in texts:
139             extents = text_extents(cr, txt.text, font=txt.font, multiline=True)
140             txt.bounds.width, txt.bounds.height = extents
141
142
143     def _set_text_group_size(self, context, name, texts):
144         """
145         Calculate size of a group.
146
147         Parameters:
148          - context: cairo context
149          - name:    group name
150          - texts:   list of group texts
151         """
152         cr = context.cairo
153
154         texts = self._get_visible_texts(texts)
155
156         if not texts:
157             self._text_groups_sizes[name] = (0, 0)
158             return
159
160         # find maximum width and total height
161         width = max(txt.bounds.width for txt in texts)
162         height = sum(txt.bounds.height for txt in texts)
163         self._text_groups_sizes[name] = width, height
164
165
166     def pre_update(self, context):
167         """
168         Calculate sizes of text elements and text groups.
169         """
170         cr = context.cairo
171
172         # calculate sizes of text groups
173         for name, texts in self._get_text_groups():
174             self._set_text_sizes(context, texts)
175             self._set_text_group_size(context, name, texts)
176
177         # calculate sizes of ungrouped texts
178         texts = self._text_groups[None]
179         texts = self._get_visible_texts(texts)
180         self._set_text_sizes(context, texts)
181
182
183     def _text_group_align(self, context, name, texts):
184         """
185         Align group of text elements making vertical stack of strings.
186
187         Parameters:
188          - context: cairo context
189          - name:    group name
190          - texts:   list of group texts
191         """
192         cr = context.cairo
193
194         texts = self._get_visible_texts(texts)
195
196         if not texts:
197             return
198
199         # align according to style of last text in the group
200         style = texts[-1]._style
201         extents = self._text_groups_sizes[name]
202         x, y = self.text_align(extents, style.text_align,
203                 style.text_padding, style.text_outside)
204
205         max_hint = 0
206         if style.text_align_str:
207             for txt in texts:
208                 txt._hint = self._get_text_align_hint(cr, txt)
209                 max_hint = max(max_hint, txt._hint)
210
211         # stack texts
212         dy = 0
213         dw = extents[0]
214         for txt in texts:
215             bounds = txt.bounds
216             width, height = bounds.width, bounds.height
217             # center stacked texts
218             if max_hint:
219                 txt.bounds.x = x + max_hint - txt._hint
220             else:
221                 txt.bounds.x = x + (dw - width) / 2.0
222             txt.bounds.y = y + dy
223             dy += height
224
225
226     def _get_text_align_hint(self, cr, txt):
227         """
228         Calculate hint value for text element depending on
229         ``text_align_str`` style property.
230         """
231         style = txt.style
232         chunks = txt.text.split(style.text_align_str, 1)
233         hint = 0
234         if len(chunks) > 1:
235             hint, _ = text_extents(cr, chunks[0], font=txt.font)
236         return hint
237
238
239     def post_update(self, context):
240         """
241         Calculate position and sizes of all text elements of a diagram
242         item.
243         """
244         cr = context.cairo
245
246         # align groups of text elements
247         for name, texts in self._get_text_groups():
248             assert name in self._text_groups_sizes, 'No text group "%s"' % name
249             self._text_group_align(context, name, texts)
250
251         # align ungrouped text elements
252         texts = self._get_visible_texts(self._text_groups[None])
253         for txt in texts:
254             style = txt._style
255             extents = txt.bounds.width, txt.bounds.height
256             x, y = self.text_align(extents, style.text_align,
257                     style.text_padding, style.text_outside)
258
259             bounds = txt.bounds # fixme: gaphor rectangle problem
260             width, height = bounds.width, bounds.height # fixme: gaphor rectangle problem
261             txt.bounds.x = x
262             txt.bounds.y = y
263
264
265     def point(self, x, y):
266         """
267         Return the distance to the nearest editable and visible text
268         element.
269         """
270         def distances():
271             yield 10000.0
272             for txt in self._texts:
273                 if txt.is_visible() and txt.editable:
274                     yield distance_rectangle_point(txt.bounds, (x, y))
275         return min(distances())
276
277
278     def draw(self, context):
279         """
280         Draw all text elements of a diagram item.
281         """
282         cr = context.cairo
283         cr.save()
284         for txt in self._get_visible_texts(self._texts):
285             bounds = txt.bounds
286             x, y = bounds.x, bounds.y
287             width, height = bounds.width, bounds.height
288
289             if self.subject:
290                 text_set_font(cr, txt.font)
291                 text_multiline(cr, x, y, txt.text)
292                 cr.stroke()
293
294             if self.subject and txt.editable \
295                     and (context.hovered or context.focused):
296
297                 width = max(15, width)
298                 height = max(10, height)
299                 cr.set_line_width(0.5)
300                 cr.rectangle(x, y, width, height)
301                 cr.stroke()
302
303         cr.restore()
304
305
306
307 class TextElement(object):
308     """
309     Representation of an editable text, which is part of a diagram item.
310
311     Text element is aligned according to style information.
312         
313     It also displays and allows to edit value of an attribute of UML
314     class (DiagramItem.subject). Attribute name can be recursive, all
315     below attribute names are valid:
316      - name (named item name)
317      - guard.value (flow item guard)
318
319     Attributes and properties:
320      - attr:     name of displayed and edited UML class attribute
321      - bounds:   text bounds
322      - _style:   text style (i.e. align information, padding)
323      - text:     rendered string to be displayed
324      - pattern:  print pattern of text
325      - editable: True if text should be editable
326      - font:     text font
327
328     See also EditableTextSupport.add_text.
329     """
330
331     bounds = property(lambda self: self._bounds)
332
333     def __init__(self, attr, style=None, pattern=None, visible=None,
334             editable=False, font=font.FONT):
335         """
336         Create new text element with bounds (0, 0, 10, 10) and empty text.
337
338         Parameters:
339          - visible: function, which evaluates to True/False if text should
340                     be visible
341         """
342         super(TextElement, self).__init__()
343
344         self._bounds = Rectangle(0, 0, width=10, height=0)
345
346         # create default style for a text element
347         self._style = Style()
348         self._style.add('text-padding', (2, 2, 2, 2))
349         self._style.add('text-align', (ALIGN_CENTER, ALIGN_TOP))
350         self._style.add('text-outside', False)
351         self._style.add('text-align-str', None)
352         if style:
353             self._style.update(style)
354
355         self.attr = attr
356         self._text = ''
357
358         if visible:
359             self.is_visible = visible
360
361         if pattern:
362             self._pattern = pattern
363         else:
364             self._pattern = '%s'
365
366         self.editable = editable
367         self.font = font
368
369
370     def _set_text(self, value):
371         """
372         Render text value using pattern.
373         """
374         self._text = value and self._pattern % value or ''
375
376
377     text = property(lambda s: s._text, _set_text)
378
379     style = property(lambda s: s._style)
380
381
382     def is_visible(self):
383         """
384         Display text by default.
385         """
386         return True
387
388
389 # vim:sw=4:et:ai
Note: See TracBrowser for help on using the browser.