Refactor AssociationItem, so ends do not need to be Presentation elements

This commit is contained in:
Arjan Molenaar 2021-01-08 23:45:10 +01:00
parent 956181f277
commit 649806d493
No known key found for this signature in database
GPG Key ID: BF977B918996CB13
7 changed files with 64 additions and 70 deletions

View File

@ -13,12 +13,13 @@ Plan:
from math import atan2, pi
from typing import Optional
from gaphas.connector import Handle
from gaphas.geometry import Rectangle, distance_rectangle_point
from gaphor import UML
from gaphor.core.modeling.presentation import Presentation, Transient
from gaphor.core.modeling.properties import attribute
from gaphor.core.modeling.properties import association, attribute
from gaphor.core.styling import Style
from gaphor.diagram.presentation import LinePresentation, Named
from gaphor.diagram.shapes import (
@ -106,19 +107,14 @@ class AssociationItem(LinePresentation[UML.Association], Named):
def save(self, save_func):
super().save(save_func)
if self._head_end.subject:
save_func("head-subject", self._head_end.subject)
if self._tail_end.subject:
save_func("tail-subject", self._tail_end.subject)
def load(self, name, value):
# end_head and end_tail were used in an older Gaphor version
if name in ("head_end", "head_subject", "head-subject"):
self._head_end.subject = value
elif name in ("tail_end", "tail_subject", "tail-subject"):
self._tail_end.subject = value
else:
super().load(name, value)
if name in ("head_end", "head-subject"):
name = "head_subject"
elif name in ("tail_end", "tail-subject"):
name = "tail_subject"
super().load(name, value)
def postload(self):
super().postload()
@ -126,13 +122,9 @@ class AssociationItem(LinePresentation[UML.Association], Named):
self._tail_end.set_text()
head_end = property(lambda self: self._head_end)
tail_end = property(lambda self: self._tail_end)
def unlink(self):
self._head_end.unlink()
self._tail_end.unlink()
super().unlink()
head_subject = association("head_subject", UML.Property, upper=1)
tail_subject = association("tail_subject", UML.Property, upper=1)
def invert_direction(self):
"""Invert the direction of the association, this is done by swapping
@ -157,8 +149,8 @@ class AssociationItem(LinePresentation[UML.Association], Named):
handles = self.handles()
# Update line endings:
head_subject = self._head_end.subject
tail_subject = self._tail_end.subject
head_subject = self.head_subject
tail_subject = self.tail_subject
# Update line ends using the aggregation and isNavigable values:
if head_subject and tail_subject:
@ -183,7 +175,7 @@ class AssociationItem(LinePresentation[UML.Association], Named):
else:
self.draw_tail = draw_default_tail
if self.show_direction:
inverted = self.tail_end.subject is self.subject.memberEnd[0]
inverted = self.tail_subject is self.subject.memberEnd[0]
pos, angle = get_center_pos(self.handles(), inverted)
self._dir_pos = pos
self._dir_angle = angle
@ -328,7 +320,7 @@ def draw_tail_navigable(context):
cr.line_to(15, 6)
class AssociationEnd(Presentation[UML.Property]):
class AssociationEnd:
"""An association end represents one end of an association. An association
has two ends. An association end has two labels: one for the name and one
for the multiplicity (and maybe one for tagged values in the future).
@ -337,8 +329,7 @@ class AssociationEnd(Presentation[UML.Property]):
be recreated by the owning Association.
"""
def __init__(self, owner, end=None):
super().__init__(diagram=owner.diagram, id=Transient)
def __init__(self, owner: AssociationItem, end: Optional[str] = None):
self._canvas = None
self._owner = owner
self._end = end
@ -358,15 +349,19 @@ class AssociationEnd(Presentation[UML.Property]):
name_bounds = property(lambda s: s._name_bounds)
@property
def owner(self):
def owner(self) -> AssociationItem: # type: ignore[override]
"""Override Element.owner."""
return self._owner
@property
def owner_handle(self):
def owner_handle(self) -> Handle:
# handle(event) is the event handler method
return self._owner.head if self is self._owner.head_end else self._owner.tail
@property
def subject(self) -> Optional[UML.Property]: # type: ignore[override]
return getattr(self.owner, f"{self._end}_subject") # type:ignore[no-any-return]
def request_update(self):
self._owner.request_update()

View File

@ -102,19 +102,18 @@ class AssociationConnect(UnaryRelationshipConnect):
return True
line = self.line
subject = line.subject
is_head = handle is line.head
end = line.head_end if is_head else line.tail_end
assert end.subject
def is_connection_allowed(h):
def is_connection_allowed(p):
end = p.head_end if is_head else p.tail_end
h = end.owner_handle
if h is handle:
return True
connected = self.get_connected(h)
return (not connected) or connected.subject is element.subject
return all(
is_connection_allowed(p.owner_handle) for p in end.subject.presentation
)
return all(is_connection_allowed(p) for p in subject.presentation)
def connect_subject(self, handle):
element = self.element
@ -129,14 +128,14 @@ class AssociationConnect(UnaryRelationshipConnect):
if not line.subject:
relation = UML.model.create_association(c1.subject, c2.subject)
relation.package = element.diagram.namespace
line.head_end.subject = relation.memberEnd[0]
line.tail_end.subject = relation.memberEnd[1]
line.head_subject = relation.memberEnd[0]
line.tail_subject = relation.memberEnd[1]
# Set subject last so that event handlers can trigger
line.subject = relation
line.head_end.subject.type = c1.subject # type: ignore[assignment]
line.tail_end.subject.type = c2.subject # type: ignore[assignment]
line.head_subject.type = c1.subject # type: ignore[assignment]
line.tail_subject.type = c2.subject # type: ignore[assignment]
def reconnect(self, handle, port):
line = self.line

View File

@ -23,11 +23,11 @@ def create_association(
assoc.memberEnd.append(assoc_item.model.create(UML.Property))
assoc.memberEnd.append(assoc_item.model.create(UML.Property))
assoc_item.head_end.subject = assoc.memberEnd[0]
assoc_item.tail_end.subject = assoc.memberEnd[1]
assoc_item.head_subject = assoc.memberEnd[0]
assoc_item.tail_subject = assoc.memberEnd[1]
UML.model.set_navigability(assoc, assoc_item.head_end.subject, True)
assoc_item.head_end.subject.aggregation = association_type.value
UML.model.set_navigability(assoc, assoc_item.head_subject, True)
assoc_item.head_subject.aggregation = association_type.value
def composite_association_config(assoc_item: diagramitems.AssociationItem) -> None:

View File

@ -19,8 +19,8 @@ class AssociationItemTestCase(TestCase):
self.connect(self.assoc, self.assoc.tail, self.class2)
assert isinstance(self.assoc.subject, UML.Association)
assert self.assoc.head_end.subject is not None
assert self.assoc.tail_end.subject is not None
assert self.assoc.head_subject is not None
assert self.assoc.tail_subject is not None
assert not self.assoc.show_direction
@ -32,8 +32,8 @@ class AssociationItemTestCase(TestCase):
self.connect(self.assoc, self.assoc.head, self.class1)
self.connect(self.assoc, self.assoc.tail, self.class2)
assert self.assoc.head_end.subject is self.assoc.subject.memberEnd[0]
assert self.assoc.tail_end.subject is self.assoc.subject.memberEnd[1]
assert self.assoc.head_subject is self.assoc.subject.memberEnd[0]
assert self.assoc.tail_subject is self.assoc.subject.memberEnd[1]
def test_invert_direction(self):
self.connect(self.assoc, self.assoc.head, self.class1)
@ -42,8 +42,8 @@ class AssociationItemTestCase(TestCase):
self.assoc.invert_direction()
assert self.assoc.subject.memberEnd
assert self.assoc.head_end.subject is self.assoc.subject.memberEnd[1]
assert self.assoc.tail_end.subject is self.assoc.subject.memberEnd[0]
assert self.assoc.head_subject is self.assoc.subject.memberEnd[1]
assert self.assoc.tail_subject is self.assoc.subject.memberEnd[0]
def test_association_end_updates(self):
"""Test association end navigability connected to a class."""
@ -61,8 +61,8 @@ class AssociationItemTestCase(TestCase):
assert a.subject.memberEnd, a.subject.memberEnd
assert a.subject.memberEnd[0] is a.head_end.subject
assert a.subject.memberEnd[1] is a.tail_end.subject
assert a.subject.memberEnd[0] is a.head_subject
assert a.subject.memberEnd[1] is a.tail_subject
assert a.subject.memberEnd[0].name is None
a.subject.memberEnd[0].name = "blah"

View File

@ -26,8 +26,8 @@ def clone(create):
def _clone(item):
new = create(type(item))
new.subject = item.subject
new.head_end.subject = item.head_end.subject
new.tail_end.subject = item.tail_end.subject
new.head_subject = item.head_subject
new.tail_subject = item.tail_subject
return new
return _clone
@ -50,15 +50,15 @@ def test_association_item_connect(connected_association, element_factory):
# Diagram, Class *2, Property *2, Association
assert len(element_factory.lselect()) == 6
assert asc.head_end.subject is not None
assert asc.tail_end.subject is not None
assert asc.head_subject is not None
assert asc.tail_subject is not None
def test_association_item_reconnect(connected_association, create):
asc, c1, c2 = connected_association
c3 = create(ClassItem, UML.Class)
UML.model.set_navigability(asc.subject, asc.tail_end.subject, True)
UML.model.set_navigability(asc.subject, asc.tail_subject, True)
a = asc.subject
@ -69,7 +69,7 @@ def test_association_item_reconnect(connected_association, create):
assert c1.subject in ends
assert c3.subject in ends
assert c2.subject not in ends
assert asc.tail_end.subject.navigability is True
assert asc.tail_subject.navigability is True
def test_disconnect_should_disconnect_model(connected_association):
@ -103,19 +103,19 @@ def test_disconnect_of_navigable_end_should_remove_owner_relationship(
):
asc, c1, c2 = connected_association
UML.model.set_navigability(asc.subject, asc.head_end.subject, True)
UML.model.set_navigability(asc.subject, asc.head_subject, True)
assert asc.head_end.subject in c2.subject.ownedAttribute
assert asc.head_subject in c2.subject.ownedAttribute
disconnect(asc, asc.head)
assert asc.subject
assert len(asc.subject.memberEnd) == 2
assert asc.subject.memberEnd[0].type is None
assert asc.head_end.subject not in c2.subject.ownedAttribute
assert asc.tail_end.subject not in c1.subject.ownedAttribute
assert asc.head_end.subject.type is None
assert asc.tail_end.subject.type is None
assert asc.head_subject not in c2.subject.ownedAttribute
assert asc.tail_subject not in c1.subject.ownedAttribute
assert asc.head_subject.type is None
assert asc.tail_subject.type is None
def test_allow_reconnect_for_single_presentation(connected_association, create):

View File

@ -42,8 +42,8 @@ class DiagramItemConnectorTestCase(TestCase):
self.connect(a, a.tail, ci2)
assert a.subject
assert a.head_end.subject
assert a.tail_end.subject
assert a.head_subject
assert a.tail_subject
the_association = a.subject

View File

@ -84,8 +84,8 @@ def test_delete_copied_associations(class_and_association_with_copy, event_manag
assert a.subject.memberEnd[1].type
assert a.subject.memberEnd[0].type is c.subject
assert a.subject.memberEnd[1].type is c.subject
assert a.subject.memberEnd[0] is a.head_end.subject
assert a.subject.memberEnd[1] is a.tail_end.subject
assert a.subject.memberEnd[0] is a.head_subject
assert a.subject.memberEnd[1] is a.tail_subject
assert a.subject.memberEnd[0] in a.subject.memberEnd[1].type.ownedAttribute
# Delete the copy and all is fine
@ -97,8 +97,8 @@ def test_delete_copied_associations(class_and_association_with_copy, event_manag
assert a.subject.memberEnd[1].type
assert a.subject.memberEnd[0].type is c.subject
assert a.subject.memberEnd[1].type is c.subject
assert a.subject.memberEnd[0] is a.head_end.subject
assert a.subject.memberEnd[1] is a.tail_end.subject
assert a.subject.memberEnd[0] is a.head_subject
assert a.subject.memberEnd[1] is a.tail_subject
assert a.subject.memberEnd[0] in a.subject.memberEnd[1].type.ownedAttribute
@ -110,8 +110,8 @@ def test_delete_original_association(class_and_association_with_copy, event_mana
assert aa.subject.memberEnd[1].type
assert aa.subject.memberEnd[0].type is c.subject
assert aa.subject.memberEnd[1].type is c.subject
assert aa.subject.memberEnd[0] is aa.head_end.subject
assert aa.subject.memberEnd[1] is aa.tail_end.subject
assert aa.subject.memberEnd[0] is aa.head_subject
assert aa.subject.memberEnd[1] is aa.tail_subject
assert aa.subject.memberEnd[0] in aa.subject.memberEnd[1].type.ownedAttribute
# Now, when the original is deleted, the model is changed and made invalid
@ -123,6 +123,6 @@ def test_delete_original_association(class_and_association_with_copy, event_mana
assert aa.subject.memberEnd[1].type
assert aa.subject.memberEnd[0].type is c.subject
assert aa.subject.memberEnd[1].type is c.subject
assert aa.subject.memberEnd[0] is aa.head_end.subject
assert aa.subject.memberEnd[1] is aa.tail_end.subject
assert aa.subject.memberEnd[0] is aa.head_subject
assert aa.subject.memberEnd[1] is aa.tail_subject
assert aa.subject.memberEnd[0] in aa.subject.memberEnd[1].type.ownedAttribute