Refactor AssociationItem, so ends do not need to be Presentation elements
This commit is contained in:
parent
956181f277
commit
649806d493
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user