| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 212 |
dy = 0 |
|---|
| 213 |
dw = extents[0] |
|---|
| 214 |
for txt in texts: |
|---|
| 215 |
bounds = txt.bounds |
|---|
| 216 |
width, height = bounds.width, bounds.height |
|---|
| 217 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|---|
| 260 |
width, height = bounds.width, bounds.height |
|---|
| 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_NAME): |
|---|
| 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 |
|
|---|
| 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 |
|
|---|