| 1 |
''' |
|---|
| 2 |
AssociationItem -- Graphical representation of an association. |
|---|
| 3 |
''' |
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
from __future__ import generators |
|---|
| 11 |
|
|---|
| 12 |
import gobject |
|---|
| 13 |
import pango |
|---|
| 14 |
import diacanvas |
|---|
| 15 |
import diacanvas.shape |
|---|
| 16 |
import diacanvas.geometry |
|---|
| 17 |
import gaphor |
|---|
| 18 |
import gaphor.UML as UML |
|---|
| 19 |
from gaphor.diagram import initialize_item |
|---|
| 20 |
|
|---|
| 21 |
from diagramitem import DiagramItem |
|---|
| 22 |
from relationship import RelationshipItem |
|---|
| 23 |
|
|---|
| 24 |
class AssociationItem(RelationshipItem, diacanvas.CanvasAbstractGroup): |
|---|
| 25 |
"""AssociationItem represents associations. |
|---|
| 26 |
An AssociationItem has two AssociationEnd items. Each AssociationEnd item |
|---|
| 27 |
represents a Property (with Property.association == my association). |
|---|
| 28 |
""" |
|---|
| 29 |
__gproperties__ = { |
|---|
| 30 |
'head': (gobject.TYPE_OBJECT, 'head', |
|---|
| 31 |
'AssociationEnd held by the head end of the association', |
|---|
| 32 |
gobject.PARAM_READABLE), |
|---|
| 33 |
'tail': (gobject.TYPE_OBJECT, 'tail', |
|---|
| 34 |
'AssociationEnd held by the tail end of the association', |
|---|
| 35 |
gobject.PARAM_READABLE), |
|---|
| 36 |
'head-subject': (gobject.TYPE_PYOBJECT, 'head-subject', |
|---|
| 37 |
'subject held by the head end of the association', |
|---|
| 38 |
gobject.PARAM_READWRITE), |
|---|
| 39 |
'tail-subject': (gobject.TYPE_PYOBJECT, 'tail-subject', |
|---|
| 40 |
'subject held by the tail end of the association', |
|---|
| 41 |
gobject.PARAM_READWRITE), |
|---|
| 42 |
} |
|---|
| 43 |
|
|---|
| 44 |
association_popup_menu = ( |
|---|
| 45 |
'separator', |
|---|
| 46 |
'Side _A', ( |
|---|
| 47 |
'Head_isNavigable', |
|---|
| 48 |
'separator', |
|---|
| 49 |
'Head_AggregationNone', |
|---|
| 50 |
'Head_AggregationShared', |
|---|
| 51 |
'Head_AggregationComposite'), |
|---|
| 52 |
'Side _B', ( |
|---|
| 53 |
'Tail_isNavigable', |
|---|
| 54 |
'separator', |
|---|
| 55 |
'Tail_AggregationNone', |
|---|
| 56 |
'Tail_AggregationShared', |
|---|
| 57 |
'Tail_AggregationComposite') |
|---|
| 58 |
) |
|---|
| 59 |
|
|---|
| 60 |
def __init__(self, id=None): |
|---|
| 61 |
RelationshipItem.__init__(self, id) |
|---|
| 62 |
|
|---|
| 63 |
|
|---|
| 64 |
|
|---|
| 65 |
self._head_end = AssociationEnd() |
|---|
| 66 |
self._head_end.set_child_of(self) |
|---|
| 67 |
self._tail_end = AssociationEnd() |
|---|
| 68 |
self._tail_end.set_child_of(self) |
|---|
| 69 |
|
|---|
| 70 |
def save (self, save_func): |
|---|
| 71 |
RelationshipItem.save(self, save_func) |
|---|
| 72 |
if self._head_end.subject: |
|---|
| 73 |
save_func('head_subject', self._head_end.subject) |
|---|
| 74 |
if self._tail_end.subject: |
|---|
| 75 |
save_func('tail_subject', self._tail_end.subject) |
|---|
| 76 |
|
|---|
| 77 |
def load (self, name, value): |
|---|
| 78 |
|
|---|
| 79 |
if name in ( 'head_end', 'head_subject' ): |
|---|
| 80 |
|
|---|
| 81 |
self._head_end.load('subject', value) |
|---|
| 82 |
elif name in ( 'tail_end', 'tail_subject' ): |
|---|
| 83 |
|
|---|
| 84 |
self._tail_end.load('subject', value) |
|---|
| 85 |
else: |
|---|
| 86 |
RelationshipItem.load(self, name, value) |
|---|
| 87 |
|
|---|
| 88 |
def postload(self): |
|---|
| 89 |
RelationshipItem.postload(self) |
|---|
| 90 |
self._head_end.postload() |
|---|
| 91 |
self._tail_end.postload() |
|---|
| 92 |
|
|---|
| 93 |
def do_set_property (self, pspec, value): |
|---|
| 94 |
if pspec.name == 'head-subject': |
|---|
| 95 |
self._head_end.subject = value |
|---|
| 96 |
elif pspec.name == 'tail-subject': |
|---|
| 97 |
self._tail_end.subject = value |
|---|
| 98 |
else: |
|---|
| 99 |
RelationshipItem.do_set_property(self, pspec, value) |
|---|
| 100 |
|
|---|
| 101 |
def do_get_property(self, pspec): |
|---|
| 102 |
if pspec.name == 'head': |
|---|
| 103 |
return self._head_end |
|---|
| 104 |
if pspec.name == 'tail': |
|---|
| 105 |
return self._tail_end |
|---|
| 106 |
elif pspec.name == 'head-subject': |
|---|
| 107 |
return self._head_end.subject |
|---|
| 108 |
elif pspec.name == 'tail-subject': |
|---|
| 109 |
return self._tail_end.subject |
|---|
| 110 |
else: |
|---|
| 111 |
return RelationshipItem.do_get_property(self, pspec) |
|---|
| 112 |
|
|---|
| 113 |
head_end = property(lambda self: self._head_end) |
|---|
| 114 |
|
|---|
| 115 |
tail_end = property(lambda self: self._tail_end) |
|---|
| 116 |
|
|---|
| 117 |
def on_subject_notify(self, pspec, notifiers=()): |
|---|
| 118 |
RelationshipItem.on_subject_notify(self, pspec, |
|---|
| 119 |
notifiers + ('ownedEnd',)) |
|---|
| 120 |
|
|---|
| 121 |
def on_subject_notify__ownedEnd(self, subject, pspec): |
|---|
| 122 |
self.request_update() |
|---|
| 123 |
|
|---|
| 124 |
def on_update (self, affine): |
|---|
| 125 |
"""Update the shapes and sub-items of the association.""" |
|---|
| 126 |
|
|---|
| 127 |
head_subject = self._head_end.subject |
|---|
| 128 |
tail_subject = self._tail_end.subject |
|---|
| 129 |
if head_subject and tail_subject: |
|---|
| 130 |
|
|---|
| 131 |
|
|---|
| 132 |
if head_subject.aggregation == intern('composite'): |
|---|
| 133 |
self.set(has_head=1, head_a=20, head_b=10, head_c=6, head_d=6, |
|---|
| 134 |
head_fill_color=diacanvas.color(0,0,0,255)) |
|---|
| 135 |
elif not head_subject.owningAssociation: |
|---|
| 136 |
|
|---|
| 137 |
self.set(has_head=1, head_a=0, head_b=15, head_c=6, head_d=6) |
|---|
| 138 |
else: |
|---|
| 139 |
self.set(has_head=0) |
|---|
| 140 |
|
|---|
| 141 |
if tail_subject.aggregation == intern('composite'): |
|---|
| 142 |
self.set(has_tail=1, tail_a=20, tail_b=10, tail_c=6, tail_d=6, |
|---|
| 143 |
tail_fill_color=diacanvas.color(0,0,0,255)) |
|---|
| 144 |
elif not tail_subject.owningAssociation: |
|---|
| 145 |
|
|---|
| 146 |
self.set(has_tail=1, tail_a=0, tail_b=15, tail_c=6, tail_d=6) |
|---|
| 147 |
else: |
|---|
| 148 |
self.set(has_tail=0) |
|---|
| 149 |
|
|---|
| 150 |
RelationshipItem.on_update(self, affine) |
|---|
| 151 |
|
|---|
| 152 |
handles = self.handles |
|---|
| 153 |
|
|---|
| 154 |
self._head_end.update_labels(handles[0].get_pos_i(), |
|---|
| 155 |
handles[1].get_pos_i()) |
|---|
| 156 |
|
|---|
| 157 |
|
|---|
| 158 |
self._tail_end.update_labels(handles[-1].get_pos_i(), |
|---|
| 159 |
handles[-2].get_pos_i()) |
|---|
| 160 |
|
|---|
| 161 |
self.update_child(self._head_end, affine) |
|---|
| 162 |
self.update_child(self._tail_end, affine) |
|---|
| 163 |
|
|---|
| 164 |
|
|---|
| 165 |
b1 = self.bounds |
|---|
| 166 |
b2 = self._head_end.get_bounds(self._head_end.affine) |
|---|
| 167 |
b3 = self._tail_end.get_bounds(self._tail_end.affine) |
|---|
| 168 |
self.set_bounds((min(b1[0], b2[0], b3[0]), min(b1[1], b2[1], b3[1]), |
|---|
| 169 |
max(b1[2], b2[2], b3[2]), max(b1[3], b2[3], b3[3]))) |
|---|
| 170 |
|
|---|
| 171 |
|
|---|
| 172 |
|
|---|
| 173 |
def allow_connect_handle(self, handle, connecting_to): |
|---|
| 174 |
"""This method is called by a canvas item if the user tries to connect |
|---|
| 175 |
this object's handle. allow_connect_handle() checks if the line is |
|---|
| 176 |
allowed to be connected. In this case that means that one end of the |
|---|
| 177 |
line should be connected to a Class or Actor. |
|---|
| 178 |
Returns: TRUE if connection is allowed, FALSE otherwise. |
|---|
| 179 |
""" |
|---|
| 180 |
|
|---|
| 181 |
|
|---|
| 182 |
if isinstance(connecting_to.subject, (UML.Class, UML.Actor)): |
|---|
| 183 |
return True |
|---|
| 184 |
return False |
|---|
| 185 |
|
|---|
| 186 |
def confirm_connect_handle (self, handle): |
|---|
| 187 |
"""This method is called after a connection is established. This method |
|---|
| 188 |
sets the internal state of the line and updates the data model. |
|---|
| 189 |
""" |
|---|
| 190 |
|
|---|
| 191 |
|
|---|
| 192 |
c1 = self.handles[0].connected_to |
|---|
| 193 |
c2 = self.handles[-1].connected_to |
|---|
| 194 |
if c1 and c2: |
|---|
| 195 |
head_type = c1.subject |
|---|
| 196 |
tail_type = c2.subject |
|---|
| 197 |
|
|---|
| 198 |
|
|---|
| 199 |
if self.subject: |
|---|
| 200 |
end1 = self.subject.memberEnd[0] |
|---|
| 201 |
end2 = self.subject.memberEnd[1] |
|---|
| 202 |
if (end1.type is head_type and end2.type is tail_type) \ |
|---|
| 203 |
or (end2.type is head_type and end1.type is tail_type): |
|---|
| 204 |
return |
|---|
| 205 |
|
|---|
| 206 |
|
|---|
| 207 |
|
|---|
| 208 |
Association = UML.Association |
|---|
| 209 |
for assoc in gaphor.resource(UML.ElementFactory).itervalues(): |
|---|
| 210 |
if isinstance(assoc, Association): |
|---|
| 211 |
|
|---|
| 212 |
end1 = assoc.memberEnd[0] |
|---|
| 213 |
end2 = assoc.memberEnd[1] |
|---|
| 214 |
if (end1.type is head_type and end2.type is tail_type) \ |
|---|
| 215 |
or (end2.type is head_type and end1.type is tail_type): |
|---|
| 216 |
|
|---|
| 217 |
|
|---|
| 218 |
for item in assoc.presentation: |
|---|
| 219 |
if item.canvas is self.canvas: |
|---|
| 220 |
break |
|---|
| 221 |
else: |
|---|
| 222 |
|
|---|
| 223 |
self.subject = assoc |
|---|
| 224 |
if (end1.type is head_type and end2.type is tail_type): |
|---|
| 225 |
self._head_end.subject = end1 |
|---|
| 226 |
self._tail_end.subject = end2 |
|---|
| 227 |
else: |
|---|
| 228 |
self._head_end.subject = end2 |
|---|
| 229 |
self._tail_end.subject = end1 |
|---|
| 230 |
return |
|---|
| 231 |
else: |
|---|
| 232 |
|
|---|
| 233 |
|
|---|
| 234 |
element_factory = gaphor.resource(UML.ElementFactory) |
|---|
| 235 |
relation = element_factory.create(UML.Association) |
|---|
| 236 |
head_end = element_factory.create(UML.Property) |
|---|
| 237 |
head_end.lowerValue = element_factory.create(UML.LiteralSpecification) |
|---|
| 238 |
tail_end = element_factory.create(UML.Property) |
|---|
| 239 |
tail_end.lowerValue = element_factory.create(UML.LiteralSpecification) |
|---|
| 240 |
relation.package = self.canvas.diagram.namespace |
|---|
| 241 |
relation.memberEnd = head_end |
|---|
| 242 |
relation.memberEnd = tail_end |
|---|
| 243 |
head_end.type = tail_end.class_ = head_type |
|---|
| 244 |
tail_end.type = head_end.class_ = tail_type |
|---|
| 245 |
|
|---|
| 246 |
|
|---|
| 247 |
|
|---|
| 248 |
|
|---|
| 249 |
|
|---|
| 250 |
|
|---|
| 251 |
|
|---|
| 252 |
self.subject = relation |
|---|
| 253 |
self._head_end.subject = head_end |
|---|
| 254 |
self._tail_end.subject = tail_end |
|---|
| 255 |
|
|---|
| 256 |
def confirm_disconnect_handle (self, handle, was_connected_to): |
|---|
| 257 |
|
|---|
| 258 |
if self.subject: |
|---|
| 259 |
|
|---|
| 260 |
|
|---|
| 261 |
del self._head_end.subject |
|---|
| 262 |
del self._tail_end.subject |
|---|
| 263 |
del self.subject |
|---|
| 264 |
|
|---|
| 265 |
|
|---|
| 266 |
|
|---|
| 267 |
def on_groupable_add(self, item): |
|---|
| 268 |
return 0 |
|---|
| 269 |
|
|---|
| 270 |
def on_groupable_remove(self, item): |
|---|
| 271 |
'''Do not allow the name to be removed.''' |
|---|
| 272 |
return 1 |
|---|
| 273 |
|
|---|
| 274 |
def on_groupable_iter(self): |
|---|
| 275 |
return iter([self._head_end, self._tail_end]) |
|---|
| 276 |
|
|---|
| 277 |
def get_popup_menu(self): |
|---|
| 278 |
if self.subject: |
|---|
| 279 |
return self.popup_menu + self.association_popup_menu |
|---|
| 280 |
else: |
|---|
| 281 |
return self.popup_menu |
|---|
| 282 |
|
|---|
| 283 |
|
|---|
| 284 |
class AssociationEnd(diacanvas.CanvasItem, diacanvas.CanvasEditable, DiagramItem): |
|---|
| 285 |
"""An association end represents one end of an association. An association |
|---|
| 286 |
has two ends. An association end has two labels: one for the name and |
|---|
| 287 |
one for the multiplicity (and maybe one for tagged values in the future). |
|---|
| 288 |
|
|---|
| 289 |
An AsociationEnd has no ID, hence it will not be stored, but it will be |
|---|
| 290 |
recreated by the owning Association. |
|---|
| 291 |
|
|---|
| 292 |
TODO: |
|---|
| 293 |
- add on_point() and let it return min(distance(_name), distance(_mult)) or |
|---|
| 294 |
the first 20-30 units of the line, for association end popup menu. |
|---|
| 295 |
""" |
|---|
| 296 |
__gproperties__ = DiagramItem.__gproperties__ |
|---|
| 297 |
___gproperties__ = { |
|---|
| 298 |
'name': (gobject.TYPE_STRING, 'name', '', '', gobject.PARAM_READWRITE), |
|---|
| 299 |
'mult': (gobject.TYPE_STRING, 'mult', '', '', gobject.PARAM_READWRITE) |
|---|
| 300 |
} |
|---|
| 301 |
|
|---|
| 302 |
__gsignals__ = DiagramItem.__gsignals__ |
|---|
| 303 |
|
|---|
| 304 |
FONT='sans 10' |
|---|
| 305 |
|
|---|
| 306 |
def __init__(self, id=None): |
|---|
| 307 |
self.__gobject_init__() |
|---|
| 308 |
DiagramItem.__init__(self, id) |
|---|
| 309 |
self.set_flags(diacanvas.COMPOSITE) |
|---|
| 310 |
|
|---|
| 311 |
font = pango.FontDescription(AssociationEnd.FONT) |
|---|
| 312 |
self._name = diacanvas.shape.Text() |
|---|
| 313 |
self._name.set_font_description(font) |
|---|
| 314 |
self._name.set_wrap_mode(diacanvas.shape.WRAP_NONE) |
|---|
| 315 |
self._name.set_markup(False) |
|---|
| 316 |
self._name_border = diacanvas.shape.Path() |
|---|
| 317 |
self._name_border.set_color(diacanvas.color(128,128,128)) |
|---|
| 318 |
self._name_border.set_line_width(1.0) |
|---|
| 319 |
|
|---|
| 320 |
self._mult = diacanvas.shape.Text() |
|---|
| 321 |
self._mult.set_font_description(font) |
|---|
| 322 |
self._mult.set_wrap_mode(diacanvas.shape.WRAP_NONE) |
|---|
| 323 |
self._mult.set_markup(False) |
|---|
| 324 |
self._mult_border = diacanvas.shape.Path() |
|---|
| 325 |
self._mult_border.set_color(diacanvas.color(128,128,128)) |
|---|
| 326 |
self._mult_border.set_line_width(1.0) |
|---|
| 327 |
|
|---|
| 328 |
self._name_bounds = self._mult_bounds = (0, 0, 0, 0) |
|---|
| 329 |
|
|---|
| 330 |
|
|---|
| 331 |
connect = DiagramItem.connect |
|---|
| 332 |
disconnect = DiagramItem.disconnect |
|---|
| 333 |
notify = DiagramItem.notify |
|---|
| 334 |
|
|---|
| 335 |
def postload(self): |
|---|
| 336 |
DiagramItem.postload(self) |
|---|
| 337 |
|
|---|
| 338 |
|
|---|
| 339 |
def set_text(self): |
|---|
| 340 |
"""Set the text on the association end. |
|---|
| 341 |
""" |
|---|
| 342 |
if self.subject: |
|---|
| 343 |
n, m = self.subject.render() |
|---|
| 344 |
self._name.set_text(n) |
|---|
| 345 |
self._mult.set_text(m) |
|---|
| 346 |
self.request_update() |
|---|
| 347 |
|
|---|
| 348 |
def set_navigable(self, navigable): |
|---|
| 349 |
"""Change the AsociationEnd's navigability. |
|---|
| 350 |
|
|---|
| 351 |
A warning is issued if the subject or opposite property is missing. |
|---|
| 352 |
""" |
|---|
| 353 |
subject = self.subject |
|---|
| 354 |
if subject and subject.opposite: |
|---|
| 355 |
opposite = subject.opposite |
|---|
| 356 |
if navigable: |
|---|
| 357 |
|
|---|
| 358 |
if subject.owningAssociation: |
|---|
| 359 |
del subject.owningAssociation |
|---|
| 360 |
subject.class_ = opposite.type |
|---|
| 361 |
else: |
|---|
| 362 |
if subject.class_: |
|---|
| 363 |
del subject.class_ |
|---|
| 364 |
subject.owningAssociation = subject.association |
|---|
| 365 |
else: |
|---|
| 366 |
log.warning('AssociationEnd.set_navigable: %s missing' % \ |
|---|
| 367 |
(subject and 'subject' or 'opposite Property')) |
|---|
| 368 |
|
|---|
| 369 |
def update_labels(self, p1, p2): |
|---|
| 370 |
"""Update label placement for association's name and |
|---|
| 371 |
multiplicity label. p1 is the line end and p2 is the last |
|---|
| 372 |
but one point of the line. |
|---|
| 373 |
""" |
|---|
| 374 |
ofs = 5 |
|---|
| 375 |
|
|---|
| 376 |
name_dx = 0.0 |
|---|
| 377 |
name_dy = 0.0 |
|---|
| 378 |
mult_dx = 0.0 |
|---|
| 379 |
mult_dy = 0.0 |
|---|
| 380 |
|
|---|
| 381 |
dx = float(p2[0]) - float(p1[0]) |
|---|
| 382 |
dy = float(p2[1]) - float(p1[1]) |
|---|
| 383 |
|
|---|
| 384 |
name_w, name_h = map(max, self._name.to_pango_layout(True).get_pixel_size(), (10, 10)) |
|---|
| 385 |
mult_w, mult_h = map(max, self._mult.to_pango_layout(True).get_pixel_size(), (10, 10)) |
|---|
| 386 |
|
|---|
| 387 |
if dy == 0: |
|---|
| 388 |
rc = 1000.0 |
|---|
| 389 |
else: |
|---|
| 390 |
rc = dx / dy |
|---|
| 391 |
abs_rc = abs(rc) |
|---|
| 392 |
h = dx > 0 |
|---|
| 393 |
v = dy > 0 |
|---|
| 394 |
|
|---|
| 395 |
if abs_rc > 6: |
|---|
| 396 |
|
|---|
| 397 |
if h: |
|---|
| 398 |
name_dx = ofs |
|---|
| 399 |
name_dy = -ofs - name_h |
|---|
| 400 |
mult_dx = ofs |
|---|
| 401 |
mult_dy = ofs |
|---|
| 402 |
else: |
|---|
| 403 |
name_dx = -ofs - name_w |
|---|
| 404 |
name_dy = -ofs - name_h |
|---|
| 405 |
mult_dx = -ofs - mult_w |
|---|
| 406 |
mult_dy = ofs |
|---|
| 407 |
elif 0 <= abs_rc <= 0.2: |
|---|
| 408 |
|
|---|
| 409 |
if v: |
|---|
| 410 |
name_dx = -ofs - name_w |
|---|
| 411 |
name_dy = ofs |
|---|
| 412 |
mult_dx = ofs |
|---|
| 413 |
mult_dy = ofs |
|---|
| 414 |
else: |
|---|
| 415 |
name_dx = -ofs - name_w |
|---|
| 416 |
name_dy = -ofs - name_h |
|---|
| 417 |
mult_dx = ofs |
|---|
| 418 |
mult_dy = -ofs - mult_h |
|---|
| 419 |
else: |
|---|
| 420 |
r = abs_rc < 1.0 |
|---|
| 421 |
align_left = (h and not r) or (r and not h) |
|---|
| 422 |
align_bottom = (v and not r) or (r and not v) |
|---|
| 423 |
if align_left: |
|---|
| 424 |
name_dx = ofs |
|---|
| 425 |
mult_dx = ofs |
|---|
| 426 |
else: |
|---|
| 427 |
name_dx = -ofs - name_w |
|---|
| 428 |
mult_dx = -ofs - mult_w |
|---|
| 429 |
if align_bottom: |
|---|
| 430 |
name_dy = -ofs - name_h |
|---|
| 431 |
mult_dy = -ofs - name_h - mult_h |
|---|
| 432 |
else: |
|---|
| 433 |
name_dy = ofs |
|---|
| 434 |
mult_dy = ofs + mult_h |
|---|
| 435 |
|
|---|
| 436 |
self._name_bounds = (p1[0] + name_dx, |
|---|
| 437 |
p1[1] + name_dy, |
|---|
| 438 |
p1[0] + name_dx + name_w, |
|---|
| 439 |
p1[1] + name_dy + name_h) |
|---|
| 440 |
self._name.set_pos((p1[0] + name_dx, p1[1] + |
|---|