Merge branch 'master' into css-selectors
# Conflicts: # poetry.lock
This commit is contained in:
commit
4559d549da
@ -1,2 +1,4 @@
|
||||
import gaphor.SysML.blocks.connectors
|
||||
from gaphor.SysML.blocks.block import BlockItem
|
||||
from gaphor.SysML.blocks.property import PropertyItem
|
||||
from gaphor.SysML.blocks.proxyport import ProxyPortItem
|
||||
|
42
gaphor/SysML/blocks/connectors.py
Normal file
42
gaphor/SysML/blocks/connectors.py
Normal file
@ -0,0 +1,42 @@
|
||||
from gaphas.connector import Handle, Port
|
||||
|
||||
from gaphor.diagram.connectors import BaseConnector, Connector
|
||||
from gaphor.SysML import sysml
|
||||
from gaphor.SysML.blocks.block import BlockItem
|
||||
from gaphor.SysML.blocks.proxyport import ProxyPortItem
|
||||
|
||||
|
||||
@Connector.register(BlockItem, ProxyPortItem)
|
||||
class BlockProxyPortConnector:
|
||||
def __init__(self, block: BlockItem, proxy_port: ProxyPortItem,) -> None:
|
||||
assert block.canvas is proxy_port.canvas
|
||||
self.block = block
|
||||
self.proxy_port = proxy_port
|
||||
|
||||
def allow(self, handle: Handle, port: Port) -> bool:
|
||||
return True
|
||||
|
||||
def connect(self, handle: Handle, port: Port) -> bool:
|
||||
"""
|
||||
Connect and reconnect at model level.
|
||||
|
||||
Returns `True` if a connection is established.
|
||||
"""
|
||||
proxy_port = self.proxy_port
|
||||
if not proxy_port.subject:
|
||||
proxy_port.subject = proxy_port.model.create(sysml.ProxyPort)
|
||||
proxy_port.subject.encapsulatedClassifier = self.block.subject
|
||||
|
||||
# This raises the item in the item hierarchy
|
||||
assert proxy_port.canvas
|
||||
proxy_port.canvas.reparent(proxy_port, self.block)
|
||||
|
||||
return True
|
||||
|
||||
def disconnect(self, handle: Handle) -> None:
|
||||
proxy_port = self.proxy_port
|
||||
if proxy_port.subject and proxy_port.canvas:
|
||||
subject = proxy_port.subject
|
||||
del proxy_port.subject
|
||||
proxy_port.canvas.reparent(proxy_port, None)
|
||||
subject.unlink()
|
119
gaphor/SysML/blocks/proxyport.py
Normal file
119
gaphor/SysML/blocks/proxyport.py
Normal file
@ -0,0 +1,119 @@
|
||||
import ast
|
||||
from typing import Optional
|
||||
|
||||
from gaphas.connector import Handle, LinePort
|
||||
from gaphas.geometry import Rectangle, distance_rectangle_point
|
||||
from gaphas.item import Item
|
||||
|
||||
from gaphor.core.modeling import Presentation
|
||||
from gaphor.diagram.presentation import Named, postload_connect
|
||||
from gaphor.diagram.shapes import (
|
||||
Box,
|
||||
DrawContext,
|
||||
EditableText,
|
||||
IconBox,
|
||||
SizeContext,
|
||||
Text,
|
||||
TextAlign,
|
||||
VerticalAlign,
|
||||
draw_border,
|
||||
)
|
||||
from gaphor.diagram.support import represents
|
||||
from gaphor.SysML import sysml
|
||||
from gaphor.UML.modelfactory import stereotypes_str
|
||||
|
||||
|
||||
def text_position(position):
|
||||
return {
|
||||
"text-align": TextAlign.LEFT
|
||||
if position == "left"
|
||||
else (TextAlign.RIGHT if position == "right" else TextAlign.CENTER),
|
||||
"vertical-align": VerticalAlign.TOP
|
||||
if position == "top"
|
||||
else (VerticalAlign.BOTTOM if position == "bottom" else VerticalAlign.MIDDLE),
|
||||
}
|
||||
|
||||
|
||||
@represents(sysml.ProxyPort)
|
||||
class ProxyPortItem(Presentation[sysml.ProxyPort], Item, Named):
|
||||
def __init__(self, id=None, model=None):
|
||||
super().__init__(id, model)
|
||||
|
||||
h1 = Handle(connectable=True)
|
||||
self._handles.append(h1)
|
||||
# self._ports.append(LinePort(h1.pos, h1.pos))
|
||||
|
||||
self._last_connected_side = None
|
||||
self.watch("subject[NamedElement].name")
|
||||
|
||||
def update_shapes(self):
|
||||
self.shape = IconBox(
|
||||
Box(style={"background-color": (1, 1, 1, 1)}, draw=draw_border),
|
||||
Text(text=lambda: stereotypes_str(self.subject, ("proxy",))),
|
||||
EditableText(text=lambda: self.subject and self.subject.name or ""),
|
||||
style=text_position(self.connected_side()),
|
||||
)
|
||||
self.request_update()
|
||||
|
||||
def connected_side(self) -> Optional[str]:
|
||||
if not self.canvas:
|
||||
return None
|
||||
|
||||
cinfo = self.canvas.get_connection(self._handles[0])
|
||||
|
||||
return cinfo.connected.port_side(cinfo.port) if cinfo else None
|
||||
|
||||
def dimensions(self):
|
||||
return Rectangle(-8, -8, 16, 16)
|
||||
|
||||
def point(self, pos):
|
||||
return distance_rectangle_point(self.dimensions(), pos)
|
||||
|
||||
def setup_canvas(self):
|
||||
super().setup_canvas()
|
||||
self.subscribe_all()
|
||||
# Invoke here, since we do not receive events, unless we're attached to a canvas
|
||||
self.update_shapes()
|
||||
|
||||
def teardown_canvas(self):
|
||||
self.unsubscribe_all()
|
||||
super().teardown_canvas()
|
||||
|
||||
def save(self, save_func):
|
||||
save_func("matrix", tuple(self.matrix))
|
||||
|
||||
assert self.canvas
|
||||
c = self.canvas.get_connection(self.handles()[0])
|
||||
if c:
|
||||
save_func("connection", c.connected)
|
||||
|
||||
super().save(save_func)
|
||||
|
||||
def load(self, name, value):
|
||||
if name == "matrix":
|
||||
self.matrix = ast.literal_eval(value)
|
||||
elif name == "connection":
|
||||
self._load_connection = value
|
||||
else:
|
||||
super().load(name, value)
|
||||
|
||||
def postload(self):
|
||||
super().postload()
|
||||
if hasattr(self, "_load_connection"):
|
||||
postload_connect(self, self.handles()[0], self._load_connection)
|
||||
del self._load_connection
|
||||
|
||||
self.update_shapes()
|
||||
|
||||
def pre_update(self, context):
|
||||
side = self.connected_side()
|
||||
if self._last_connected_side != side:
|
||||
self._last_connected_side = side
|
||||
self.update_shapes()
|
||||
|
||||
self.shape.size(SizeContext.from_context(context, self.style))
|
||||
|
||||
def draw(self, context):
|
||||
self.shape.draw(
|
||||
DrawContext.from_context(context, self.style), self.dimensions()
|
||||
)
|
0
gaphor/SysML/blocks/tests/__init__.py
Normal file
0
gaphor/SysML/blocks/tests/__init__.py
Normal file
1
gaphor/SysML/blocks/tests/conftest.py
Normal file
1
gaphor/SysML/blocks/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from gaphor.SysML.tests.fixtures import diagram, element_factory, event_manager
|
45
gaphor/SysML/blocks/tests/test_connectors.py
Normal file
45
gaphor/SysML/blocks/tests/test_connectors.py
Normal file
@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
|
||||
from gaphor.diagram.connectors import Connector
|
||||
from gaphor.SysML import sysml
|
||||
from gaphor.SysML.blocks.block import BlockItem
|
||||
from gaphor.SysML.blocks.connectors import BlockProxyPortConnector
|
||||
from gaphor.SysML.blocks.proxyport import ProxyPortItem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def block_item(diagram, element_factory):
|
||||
return diagram.create(BlockItem, subject=element_factory.create(sysml.Block))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def proxy_port_item(diagram):
|
||||
return diagram.create(ProxyPortItem)
|
||||
|
||||
|
||||
def test_connection_is_allowed(diagram, block_item, proxy_port_item):
|
||||
connector = Connector(block_item, proxy_port_item)
|
||||
|
||||
assert isinstance(connector, BlockProxyPortConnector)
|
||||
assert connector.allow(proxy_port_item.handles()[0], block_item.ports()[0])
|
||||
|
||||
|
||||
def test_connect_proxy_port_to_block(diagram, block_item, proxy_port_item):
|
||||
connector = Connector(block_item, proxy_port_item)
|
||||
|
||||
connected = connector.connect(proxy_port_item.handles()[0], block_item.ports()[0])
|
||||
|
||||
assert connected
|
||||
assert proxy_port_item.subject
|
||||
assert proxy_port_item.subject.encapsulatedClassifier is block_item.subject
|
||||
assert proxy_port_item.subject in block_item.subject.ownedPort
|
||||
|
||||
|
||||
def test_disconnect_proxy_port_to_block(diagram, block_item, proxy_port_item):
|
||||
connector = Connector(block_item, proxy_port_item)
|
||||
connector.connect(proxy_port_item.handles()[0], block_item.ports()[0])
|
||||
|
||||
connector.disconnect(proxy_port_item.handles()[0])
|
||||
|
||||
assert proxy_port_item.subject is None
|
||||
assert proxy_port_item.canvas
|
@ -1 +1 @@
|
||||
from gaphor.diagram.tests.fixtures import diagram, element_factory, event_manager
|
||||
from gaphor.SysML.tests.fixtures import diagram, element_factory, event_manager
|
||||
|
@ -1,10 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from gaphor.core.modeling import ElementFactory
|
||||
from gaphor.core.modeling.elementdispatcher import ElementDispatcher
|
||||
from gaphor.diagram.tests.fixtures import allow, connect, disconnect
|
||||
from gaphor.SysML import sysml
|
||||
from gaphor.SysML.modelinglanguage import SysMLModelingLanguage
|
||||
from gaphor.SysML.requirements.relationships import (
|
||||
DeriveReqtItem,
|
||||
RefineItem,
|
||||
@ -13,28 +10,6 @@ from gaphor.SysML.requirements.relationships import (
|
||||
VerifyItem,
|
||||
)
|
||||
from gaphor.SysML.requirements.requirement import RequirementItem
|
||||
from gaphor.UML.modelinglanguage import UMLModelingLanguage
|
||||
|
||||
|
||||
class MockModelingLanguage:
|
||||
def __init__(self):
|
||||
self._modeling_languages = [UMLModelingLanguage(), SysMLModelingLanguage()]
|
||||
|
||||
def lookup_element(self, name):
|
||||
return self.first(lambda provider: provider.lookup_element(name))
|
||||
|
||||
def first(self, predicate):
|
||||
for provider in self._modeling_languages:
|
||||
type = predicate(provider)
|
||||
if type:
|
||||
return type
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def element_factory(event_manager):
|
||||
return ElementFactory(
|
||||
event_manager, ElementDispatcher(event_manager, MockModelingLanguage())
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -1 +1 @@
|
||||
from gaphor.diagram.tests.fixtures import diagram, element_factory, event_manager
|
||||
from gaphor.SysML.tests.fixtures import diagram, element_factory, event_manager
|
||||
|
28
gaphor/SysML/tests/fixtures.py
Normal file
28
gaphor/SysML/tests/fixtures.py
Normal file
@ -0,0 +1,28 @@
|
||||
import pytest
|
||||
|
||||
from gaphor.core.modeling import ElementFactory
|
||||
from gaphor.core.modeling.elementdispatcher import ElementDispatcher
|
||||
from gaphor.diagram.tests.fixtures import diagram, event_manager
|
||||
from gaphor.SysML.modelinglanguage import SysMLModelingLanguage
|
||||
from gaphor.UML.modelinglanguage import UMLModelingLanguage
|
||||
|
||||
|
||||
class MockModelingLanguage:
|
||||
def __init__(self):
|
||||
self._modeling_languages = [UMLModelingLanguage(), SysMLModelingLanguage()]
|
||||
|
||||
def lookup_element(self, name):
|
||||
return self.first(lambda provider: provider.lookup_element(name))
|
||||
|
||||
def first(self, predicate):
|
||||
for provider in self._modeling_languages:
|
||||
type = predicate(provider)
|
||||
if type:
|
||||
return type
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def element_factory(event_manager): # noqa: F811
|
||||
return ElementFactory(
|
||||
event_manager, ElementDispatcher(event_manager, MockModelingLanguage())
|
||||
)
|
@ -1,5 +1,7 @@
|
||||
"""The action definition for the SysML toolbox."""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from gaphas.item import SE
|
||||
|
||||
import gaphor.SysML.diagramitems as sysml_items
|
||||
@ -9,13 +11,12 @@ from gaphor.core import gettext
|
||||
from gaphor.diagram.diagramtoolbox import ToolboxDefinition, ToolDef
|
||||
from gaphor.diagram.diagramtools import PlacementTool
|
||||
from gaphor.SysML import sysml
|
||||
from gaphor.UML.toolbox import namespace_config
|
||||
|
||||
|
||||
def namespace_config(new_item):
|
||||
subject = new_item.subject
|
||||
diagram = new_item.canvas.diagram
|
||||
subject.package = diagram.namespace
|
||||
subject.name = f"New{type(subject).__name__}"
|
||||
class AssociationType(Enum):
|
||||
COMPOSITE = "composite"
|
||||
SHARED = "shared"
|
||||
|
||||
|
||||
def initial_pseudostate_config(new_item):
|
||||
@ -31,6 +32,28 @@ def metaclass_config(new_item):
|
||||
new_item.subject.name = "Class"
|
||||
|
||||
|
||||
def create_association(
|
||||
assoc_item: uml_items.AssociationItem, association_type: AssociationType
|
||||
) -> None:
|
||||
assoc = assoc_item.subject
|
||||
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]
|
||||
|
||||
UML.model.set_navigability(assoc, assoc_item.head_end.subject, True)
|
||||
assoc_item.head_end.subject.aggregation = association_type.value
|
||||
|
||||
|
||||
def composite_association_config(assoc_item: uml_items.AssociationItem) -> None:
|
||||
create_association(assoc_item, AssociationType.COMPOSITE)
|
||||
|
||||
|
||||
def shared_association_config(assoc_item: uml_items.AssociationItem) -> None:
|
||||
create_association(assoc_item, AssociationType.SHARED)
|
||||
|
||||
|
||||
# Actions: ((section (name, label, icon_name, shortcut)), ...)
|
||||
sysml_toolbox_actions: ToolboxDefinition = (
|
||||
(
|
||||
@ -108,6 +131,28 @@ sysml_toolbox_actions: ToolboxDefinition = (
|
||||
),
|
||||
handle_index=SE,
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-composite-association",
|
||||
gettext("Composite Association"),
|
||||
"gaphor-composite-association-symbolic",
|
||||
"<Shift>Z",
|
||||
PlacementTool.new_item_factory(
|
||||
uml_items.AssociationItem,
|
||||
UML.Association,
|
||||
config_func=composite_association_config,
|
||||
),
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-shared-association",
|
||||
gettext("Shared Association"),
|
||||
"gaphor-shared-association-symbolic",
|
||||
"<Shift>Q",
|
||||
PlacementTool.new_item_factory(
|
||||
uml_items.AssociationItem,
|
||||
UML.Association,
|
||||
config_func=shared_association_config,
|
||||
),
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-association",
|
||||
gettext("Association"),
|
||||
@ -135,18 +180,11 @@ sysml_toolbox_actions: ToolboxDefinition = (
|
||||
PlacementTool.new_item_factory(uml_items.ConnectorItem),
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-connector",
|
||||
"toolbox-proxy-port",
|
||||
gettext("Proxy Port"),
|
||||
"gaphor-proxyport-symbolic",
|
||||
"<Shift>Y",
|
||||
PlacementTool.new_item_factory(uml_items.ConnectorItem),
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-property",
|
||||
gettext("Property"),
|
||||
"gaphor-property-symbolic",
|
||||
"<Shift>M",
|
||||
PlacementTool.new_item_factory(sysml_items.PropertyItem),
|
||||
PlacementTool.new_item_factory(sysml_items.ProxyPortItem),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Classes related (dependency, implementation) adapter connections."""
|
||||
|
||||
from gaphas import Handle
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.connectors import (
|
||||
Connector,
|
||||
@ -109,28 +111,39 @@ class AssociationConnect(UnaryRelationshipConnect):
|
||||
c1 = self.get_connected(line.head)
|
||||
c2 = self.get_connected(line.tail)
|
||||
if c1 and c2:
|
||||
head_type = c1.subject
|
||||
tail_type = c2.subject
|
||||
|
||||
# First check if we do not already contain the right subject:
|
||||
if line.subject:
|
||||
if not line.subject:
|
||||
relation = UML.model.create_association(c1.subject, c2.subject)
|
||||
relation.package = element.canvas.diagram.namespace
|
||||
line.head_end.subject = relation.memberEnd[0]
|
||||
line.tail_end.subject = relation.memberEnd[1]
|
||||
|
||||
# Set subject last so that event handlers can trigger
|
||||
line.subject = relation
|
||||
|
||||
else:
|
||||
assert isinstance(line.subject, UML.Association)
|
||||
end1 = line.subject.memberEnd[0]
|
||||
end2 = line.subject.memberEnd[1]
|
||||
if (end1.type is head_type and end2.type is tail_type) or (
|
||||
end2.type is head_type and end1.type is tail_type
|
||||
if (end1.type is c1.subject and end2.type is c2.subject) or (
|
||||
end2.type is c1.subject and end1.type is c2.subject
|
||||
):
|
||||
return
|
||||
|
||||
# Create new association
|
||||
relation = UML.model.create_association(head_type, tail_type)
|
||||
relation.package = element.canvas.diagram.namespace
|
||||
|
||||
line.head_end.subject = relation.memberEnd[0]
|
||||
line.tail_end.subject = relation.memberEnd[1]
|
||||
|
||||
# Do subject itself last, so event handlers can trigger
|
||||
line.subject = relation
|
||||
line.subject.memberEnd[0].type = c1.subject
|
||||
line.subject.memberEnd[1].type = c2.subject
|
||||
UML.model.set_navigability(
|
||||
line.subject,
|
||||
line.head_end.subject,
|
||||
line.subject.memberEnd[0].navigability,
|
||||
)
|
||||
line.head_end.subject.aggregation = line.subject.memberEnd[0].aggregation
|
||||
UML.model.set_navigability(
|
||||
line.subject,
|
||||
line.tail_end.subject,
|
||||
line.subject.memberEnd[1].navigability,
|
||||
)
|
||||
line.tail_end.subject.aggregation = line.subject.memberEnd[1].aggregation
|
||||
|
||||
def reconnect(self, handle, port):
|
||||
line = self.line
|
||||
@ -152,24 +165,14 @@ class AssociationConnect(UnaryRelationshipConnect):
|
||||
oend.subject.type = c.subject
|
||||
UML.model.set_navigability(line.subject, oend.subject, nav)
|
||||
|
||||
def disconnect_subject(self, handle):
|
||||
def disconnect_subject(self, handle: Handle) -> None:
|
||||
"""Disconnect the type of each member end.
|
||||
|
||||
On connect, we pair association member ends with the element they
|
||||
connect to. On disconnect, we remove this relation.
|
||||
"""
|
||||
Disconnect model element.
|
||||
Disconnect property (memberEnd) too, in case of end of life for
|
||||
Extension
|
||||
"""
|
||||
opposite = self.line.opposite(handle)
|
||||
c1 = self.get_connected(handle)
|
||||
c2 = self.get_connected(opposite)
|
||||
if c1 and c2:
|
||||
old: UML.Association = self.line.subject
|
||||
del self.line.subject
|
||||
del self.line.head_end.subject
|
||||
del self.line.tail_end.subject
|
||||
if old and len(old.presentation) == 0:
|
||||
for e in list(old.memberEnd):
|
||||
e.unlink()
|
||||
old.unlink()
|
||||
for e in list(self.line.subject.memberEnd):
|
||||
e.type = None
|
||||
|
||||
|
||||
@Connector.register(Named, ImplementationItem)
|
||||
|
@ -438,7 +438,8 @@ class AssociationPropertyPage(PropertyPageBase):
|
||||
def construct_end(self, builder, end_name, end):
|
||||
subject = end.subject
|
||||
title = builder.get_object(f"{end_name}-title")
|
||||
title.set_text(f"{end_name.title()} (: {subject.type.name})")
|
||||
if subject.type:
|
||||
title.set_text(f"{end_name.title()} (: {subject.type.name})")
|
||||
|
||||
self.update_end_name(builder, end_name, subject)
|
||||
|
||||
|
@ -348,6 +348,6 @@ class AssociationConnectorTestCase(TestCase):
|
||||
assert asc.subject is not None
|
||||
|
||||
self.disconnect(asc, asc.head)
|
||||
|
||||
# after disconnection: one diagram and two classes
|
||||
self.assertEqual(3, len(list(self.element_factory.select())))
|
||||
self.disconnect(asc, asc.tail)
|
||||
assert c1 is not self.get_connected(asc.head)
|
||||
assert c2 is not self.get_connected(asc.tail)
|
||||
|
@ -10,22 +10,6 @@ from gaphor.UML.interactions.lifeline import LifelineItem
|
||||
from gaphor.UML.interactions.message import MessageItem
|
||||
|
||||
|
||||
def reparent(canvas, item, new_parent):
|
||||
old_parent = canvas.get_parent(item)
|
||||
|
||||
if old_parent:
|
||||
canvas.reparent(item, None)
|
||||
m = canvas.get_matrix_i2c(old_parent)
|
||||
item.matrix *= m
|
||||
old_parent.request_update()
|
||||
|
||||
if new_parent:
|
||||
canvas.reparent(item, new_parent)
|
||||
m = canvas.get_matrix_c2i(new_parent)
|
||||
item.matrix *= m
|
||||
new_parent.request_update()
|
||||
|
||||
|
||||
def get_connected(item, handle) -> Optional[Presentation[Element]]:
|
||||
"""
|
||||
Get item connected to a handle.
|
||||
@ -251,7 +235,7 @@ class LifelineExecutionSpecificationConnect(BaseConnector):
|
||||
|
||||
canvas = self.canvas
|
||||
if canvas.get_parent(self.line) is not self.element:
|
||||
reparent(canvas, self.line, self.element)
|
||||
canvas.reparent(self.line, self.element)
|
||||
|
||||
for cinfo in canvas.get_connections(connected=self.line):
|
||||
Connector(self.line, cinfo.item).connect(cinfo.handle, cinfo.port)
|
||||
@ -267,7 +251,7 @@ class LifelineExecutionSpecificationConnect(BaseConnector):
|
||||
|
||||
if canvas.get_parent(self.line) is self.element:
|
||||
new_parent = canvas.get_parent(self.element)
|
||||
reparent(canvas, self.line, new_parent)
|
||||
canvas.reparent(self.line, new_parent)
|
||||
|
||||
for cinfo in canvas.get_connections(connected=self.line):
|
||||
Connector(self.line, cinfo.item).disconnect(cinfo.handle)
|
||||
@ -291,7 +275,7 @@ class ExecutionSpecificationExecutionSpecificationConnect(BaseConnector):
|
||||
assert connected_item
|
||||
Connector(connected_item, self.line).connect(handle, None)
|
||||
|
||||
reparent(self.canvas, self.line, self.element)
|
||||
self.canvas.reparent(self.line, self.element)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -269,17 +269,12 @@ def set_navigability(assoc, end, nav):
|
||||
"""
|
||||
Set navigability of an association end (property).
|
||||
|
||||
There are three possible values for ``nav`` parameter
|
||||
|
||||
True
|
||||
association end is navigable
|
||||
False
|
||||
association end is not navigable
|
||||
None
|
||||
association end navigability is unknown
|
||||
|
||||
There are two ways of specifying that an end is navigable
|
||||
There are three possible values for ``nav`` parameter:
|
||||
1. True - association end is navigable
|
||||
2. False - association end is not navigable
|
||||
3. None - association end navigability is unknown
|
||||
|
||||
There are two ways of specifying that an end is navigable:
|
||||
- an end is in Association.navigableOwnedEnd collection
|
||||
- an end is class (interface) attribute (stored in Class.ownedAttribute
|
||||
collection)
|
||||
@ -291,7 +286,7 @@ def set_navigability(assoc, end, nav):
|
||||
|
||||
There two association ends A.x and B.y, A.x is navigable.
|
||||
|
||||
Therefore navigable association ends are constructed in following way
|
||||
Therefore, we construct navigable association ends in the following way:
|
||||
|
||||
- if A is a class or an interface, then A.x is an attribute owned by A
|
||||
- if A is other classifier, then association is more general
|
||||
@ -302,7 +297,7 @@ def set_navigability(assoc, end, nav):
|
||||
- when A and B are instances of Node class, then it is a
|
||||
communication path
|
||||
|
||||
Therefore navigable association end may be stored as one of
|
||||
Therefore, we store the navigable association end as one of the following:
|
||||
- {Class,Interface}.ownedAttribute due to their capabilities of
|
||||
editing owned members
|
||||
- Association.navigableOwnedEnd
|
||||
|
@ -119,6 +119,20 @@ uml_toolbox_actions: ToolboxDefinition = (
|
||||
),
|
||||
handle_index=SE,
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-composite-association",
|
||||
gettext("Composite Association"),
|
||||
"gaphor-composite-association-symbolic",
|
||||
"<Shift>Z",
|
||||
PlacementTool.new_item_factory(diagramitems.AssociationItem),
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-shared-association",
|
||||
gettext("Shared Association"),
|
||||
"gaphor-shared-association-symbolic",
|
||||
"<Shift>Q",
|
||||
PlacementTool.new_item_factory(diagramitems.AssociationItem),
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-association",
|
||||
gettext("Association"),
|
||||
|
@ -635,7 +635,7 @@ class FinalState(State):
|
||||
class Port(Property):
|
||||
isBehavior: attribute[int]
|
||||
isService: attribute[int]
|
||||
encapsulatedClassifier: relation_many[EncapsulatedClassifer]
|
||||
encapsulatedClassifier: relation_one[EncapsulatedClassifer]
|
||||
|
||||
|
||||
class Deployment(Dependency):
|
||||
@ -1205,7 +1205,7 @@ StateMachine.extendedStateMachine = association(
|
||||
)
|
||||
ConnectorEnd.partWithPort = association("partWithPort", Property, upper=1)
|
||||
Port.encapsulatedClassifier = association(
|
||||
"encapsulatedClassifier", EncapsulatedClassifer, opposite="ownedPort"
|
||||
"encapsulatedClassifier", EncapsulatedClassifer, upper=1, opposite="ownedPort"
|
||||
)
|
||||
EncapsulatedClassifer.ownedPort = association(
|
||||
"ownedPort", Port, composite=True, opposite="encapsulatedClassifier"
|
||||
|
@ -69,6 +69,22 @@ class DiagramCanvas(gaphas.Canvas):
|
||||
|
||||
return list(filter(expression, self.get_all_items()))
|
||||
|
||||
def reparent(self, item, parent):
|
||||
"""A more fancy version of the reparent method."""
|
||||
old_parent = self.get_parent(item)
|
||||
|
||||
if old_parent:
|
||||
super().reparent(item, None)
|
||||
m = self.get_matrix_i2c(old_parent)
|
||||
item.matrix *= m
|
||||
old_parent.request_update()
|
||||
|
||||
if parent:
|
||||
super().reparent(item, parent)
|
||||
m = self.get_matrix_c2i(parent)
|
||||
item.matrix *= m
|
||||
parent.request_update()
|
||||
|
||||
|
||||
class Diagram(PackageableElement):
|
||||
"""Diagrams may contain model elements and can be owned by a Package.
|
||||
|
@ -57,7 +57,9 @@ class DiagramItemConnector(ItemConnector):
|
||||
# reconnect only constraint - leave model intact
|
||||
log.debug("performing reconnect constraint")
|
||||
constraint = sink.port.constraint(item.canvas, item, handle, sink.item)
|
||||
item.canvas.reconnect_item(item, handle, constraint=constraint)
|
||||
item.canvas.reconnect_item(
|
||||
item, handle, sink.port, constraint=constraint
|
||||
)
|
||||
elif cinfo:
|
||||
# first disconnect but disable disconnection handle as
|
||||
# reconnection is going to happen
|
||||
|
@ -91,10 +91,15 @@ class ElementPresentation(Presentation[S], gaphas.Element):
|
||||
width: int
|
||||
height: int
|
||||
|
||||
_port_sides = ("top", "right", "bottom", "left")
|
||||
|
||||
def __init__(self, id=None, model=None, shape=None):
|
||||
super().__init__(id, model)
|
||||
self._shape = shape
|
||||
|
||||
def port_side(self, port):
|
||||
return self._port_sides[self._ports.index(port)]
|
||||
|
||||
def _set_shape(self, shape):
|
||||
self._shape = shape
|
||||
self.request_update()
|
||||
|
@ -239,21 +239,47 @@ class IconBox:
|
||||
max(min_height, height + padding_top + padding_bottom),
|
||||
)
|
||||
|
||||
def child_pos(self, style: Style, bounding_box: Rectangle) -> Rectangle:
|
||||
if not self.sizes:
|
||||
return Rectangle()
|
||||
|
||||
text_align = style.get("text-align", TextAlign.CENTER)
|
||||
vertical_align = style.get("vertical-align", VerticalAlign.BOTTOM)
|
||||
vertical_spacing = style.get("vertical-spacing", 0) # should be margin?
|
||||
|
||||
ws, hs = list(zip(*self.sizes))
|
||||
max_w = max(ws)
|
||||
total_h = sum(hs)
|
||||
|
||||
if text_align == TextAlign.CENTER:
|
||||
x = bounding_box.x + (bounding_box.width - max_w) / 2
|
||||
elif text_align == TextAlign.LEFT:
|
||||
x = bounding_box.x - max_w - vertical_spacing
|
||||
elif text_align == TextAlign.RIGHT:
|
||||
x = bounding_box.x + bounding_box.width + vertical_spacing
|
||||
|
||||
if vertical_align == VerticalAlign.BOTTOM:
|
||||
y = bounding_box.y + bounding_box.height + vertical_spacing
|
||||
elif vertical_align == VerticalAlign.MIDDLE:
|
||||
y = bounding_box.y + (bounding_box.height - total_h) / 2
|
||||
elif vertical_align == VerticalAlign.TOP:
|
||||
y = bounding_box.y - total_h - vertical_spacing
|
||||
return Rectangle(x, y, max_w, total_h,)
|
||||
|
||||
def draw(self, context: DrawContext, bounding_box: Rectangle):
|
||||
style = combined_style(context.style, self._inline_style)
|
||||
new_context = replace(context, style=style)
|
||||
padding_top, padding_right, padding_bottom, padding_left = style["padding"]
|
||||
vertical_spacing = style["vertical-spacing"]
|
||||
x = bounding_box.x + padding_left
|
||||
y = bounding_box.y + padding_top
|
||||
w = bounding_box.width - padding_right - padding_left
|
||||
h = bounding_box.height - padding_top - padding_bottom
|
||||
self.icon.draw(new_context, Rectangle(x, y, w, h))
|
||||
y = y + bounding_box.height + vertical_spacing
|
||||
|
||||
cx, cy, max_w, total_h = self.child_pos(style, bounding_box)
|
||||
for c, (cw, ch) in zip(self.children, self.sizes):
|
||||
mw = max(w, cw)
|
||||
c.draw(context, Rectangle(x - (mw - w) / 2, y, mw, ch))
|
||||
y += ch
|
||||
c.draw(context, Rectangle(cx + (max_w - cw) / 2, cy, cw, ch))
|
||||
cy += ch
|
||||
|
||||
|
||||
class Text:
|
||||
|
@ -29,6 +29,17 @@ def test_creation(diagram):
|
||||
assert p.subject is None
|
||||
|
||||
|
||||
def test_element_sides(diagram):
|
||||
p = diagram.create(StubElement)
|
||||
|
||||
port_top, port_right, port_bottom, port_left = p.ports()
|
||||
|
||||
assert p.port_side(port_top) == "top"
|
||||
assert p.port_side(port_right) == "right"
|
||||
assert p.port_side(port_bottom) == "bottom"
|
||||
assert p.port_side(port_left) == "left"
|
||||
|
||||
|
||||
def test_element_saving(element_factory, diagram):
|
||||
subject = element_factory.create(UML.Class)
|
||||
p = diagram.create(StubElement, subject=subject)
|
||||
|
@ -2,7 +2,14 @@ import cairo
|
||||
import pytest
|
||||
from gaphas.geometry import Rectangle
|
||||
|
||||
from gaphor.diagram.shapes import Box, DrawContext, IconBox, Text
|
||||
from gaphor.diagram.shapes import (
|
||||
Box,
|
||||
DrawContext,
|
||||
IconBox,
|
||||
Text,
|
||||
TextAlign,
|
||||
VerticalAlign,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -71,6 +78,70 @@ def test_draw_icon_box(context):
|
||||
assert box_drawn == bounding_box
|
||||
|
||||
|
||||
def test_icon_box_child_placement_center_bottom(context):
|
||||
style = {"text-align": TextAlign.CENTER, "vertical-align": VerticalAlign.BOTTOM}
|
||||
|
||||
text = Text(text="some text")
|
||||
shape = IconBox(Box(), text,)
|
||||
shape.size(context)
|
||||
|
||||
w, h = shape.sizes[0]
|
||||
bounding_box = Rectangle(0, 0, 10, 20)
|
||||
|
||||
x, y, _, _ = shape.child_pos(style, bounding_box)
|
||||
|
||||
assert x == (bounding_box.width - w) / 2
|
||||
assert y == bounding_box.height
|
||||
|
||||
|
||||
def test_icon_box_child_placement_right_middle(context):
|
||||
style = {"text-align": TextAlign.RIGHT, "vertical-align": VerticalAlign.MIDDLE}
|
||||
|
||||
text = Text(text="some text")
|
||||
shape = IconBox(Box(), text,)
|
||||
shape.size(context)
|
||||
|
||||
w, h = shape.sizes[0]
|
||||
bounding_box = Rectangle(0, 0, 10, 20)
|
||||
|
||||
x, y, _, _ = shape.child_pos(style, bounding_box)
|
||||
|
||||
assert x == bounding_box.width
|
||||
assert y == (bounding_box.height - h) / 2
|
||||
|
||||
|
||||
def test_icon_box_child_placement_left_middle(context):
|
||||
style = {"text-align": TextAlign.LEFT, "vertical-align": VerticalAlign.MIDDLE}
|
||||
|
||||
text = Text(text="some text")
|
||||
shape = IconBox(Box(), text,)
|
||||
shape.size(context)
|
||||
|
||||
w, h = shape.sizes[0]
|
||||
bounding_box = Rectangle(0, 0, 10, 20)
|
||||
|
||||
x, y, _, _ = shape.child_pos(style, bounding_box)
|
||||
|
||||
assert x == -w
|
||||
assert y == (bounding_box.height - h) / 2
|
||||
|
||||
|
||||
def test_icon_box_child_placement_center_top(context):
|
||||
style = {"text-align": TextAlign.CENTER, "vertical-align": VerticalAlign.TOP}
|
||||
|
||||
text = Text(text="some text")
|
||||
shape = IconBox(Box(), text,)
|
||||
shape.size(context)
|
||||
|
||||
w, h = shape.sizes[0]
|
||||
bounding_box = Rectangle(0, 0, 10, 20)
|
||||
|
||||
x, y, _, _ = shape.child_pos(style, bounding_box)
|
||||
|
||||
assert x == (bounding_box.width - w) / 2
|
||||
assert y == -h
|
||||
|
||||
|
||||
def test_text_has_width(context, fixed_text_size):
|
||||
text = Text(lambda: "some text")
|
||||
|
||||
|
@ -66,11 +66,9 @@ class DiagramExport(Service, ActionProvider):
|
||||
|
||||
if sloppiness:
|
||||
view.painter = FreeHandPainter(ItemPainter(), sloppiness)
|
||||
view.bounding_box_painter = FreeHandPainter(
|
||||
BoundingBoxPainter(), sloppiness
|
||||
)
|
||||
else:
|
||||
view.painter = ItemPainter()
|
||||
view.bounding_box_painter = BoundingBoxPainter(view.painter)
|
||||
|
||||
def render(self, canvas, new_surface):
|
||||
view = View(canvas)
|
||||
|
@ -128,7 +128,7 @@ def main(argv=sys.argv[1:]):
|
||||
|
||||
view = View(diagram.canvas)
|
||||
view.painter = ItemPainter()
|
||||
view.bounding_box_painter = BoundingBoxPainter()
|
||||
view.bounding_box_painter = BoundingBoxPainter(view.painter)
|
||||
|
||||
tmpsurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0)
|
||||
tmpcr = cairo.Context(tmpsurface)
|
||||
|
@ -292,10 +292,10 @@ class DiagramPage:
|
||||
|
||||
if sloppiness:
|
||||
item_painter = FreeHandPainter(ItemPainter(), sloppiness=sloppiness)
|
||||
box_painter = FreeHandPainter(BoundingBoxPainter(), sloppiness=sloppiness)
|
||||
else:
|
||||
item_painter = ItemPainter()
|
||||
box_painter = BoundingBoxPainter()
|
||||
|
||||
box_painter = BoundingBoxPainter(item_painter)
|
||||
|
||||
view.painter = (
|
||||
PainterChain()
|
||||
|
@ -15,6 +15,8 @@ ICONS=diagram \
|
||||
package \
|
||||
interface \
|
||||
association \
|
||||
composite-association \
|
||||
shared-association \
|
||||
generalization \
|
||||
dependency \
|
||||
implementation \
|
||||
|
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg4268"
|
||||
version="1.1"
|
||||
viewBox="0 0 4.2923115 4.2333181"
|
||||
height="15.999942"
|
||||
width="16.22291">
|
||||
<defs
|
||||
id="defs4262">
|
||||
<marker
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="DiamondL"
|
||||
style="overflow:visible">
|
||||
<path
|
||||
id="path1602"
|
||||
d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
|
||||
transform="scale(0.8)" />
|
||||
</marker>
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata4265">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path
|
||||
id="path28"
|
||||
d="M 0.81445312 1.8613281 A 0.2645835 0.2645835 0 0 0 0.5703125 2.0488281 L 0.01171875 3.8925781 A 0.26460996 0.26460996 0 0 0 0.33398438 4.2246094 L 2.1933594 3.7246094 A 0.2645835 0.2645835 0 0 0 2.3808594 3.4003906 A 0.2645835 0.2645835 0 0 0 2.0566406 3.2128906 L 0.65625 3.5898438 L 1.0761719 2.2011719 A 0.2645835 0.2645835 0 0 0 0.90039062 1.8730469 A 0.2645835 0.2645835 0 0 0 0.81445312 1.8613281 z "
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1" />
|
||||
<path
|
||||
id="path24"
|
||||
d="M 1.7226562 2.1953125 L 0.51171875 3.4160156 L 0.88671875 3.7890625 L 2.0996094 2.5683594 L 1.7226562 2.1953125 z "
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1" />
|
||||
<g
|
||||
style="opacity:1"
|
||||
id="g16">
|
||||
<path
|
||||
id="path18"
|
||||
d="M 1.911067,2.3812459 3.76315,2.1166619 4.027733,0.2645788 2.17565,0.5291622 Z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1.0;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;" />
|
||||
<path
|
||||
id="path20"
|
||||
d="M 4.0058594 0 A 0.26460996 0.26460996 0 0 0 3.9902344 0.001953125 L 2.1386719 0.26757812 A 0.26460996 0.26460996 0 0 0 1.9140625 0.4921875 L 1.6484375 2.34375 A 0.26460996 0.26460996 0 0 0 1.9492188 2.6425781 L 3.8007812 2.3789062 A 0.26460996 0.26460996 0 0 0 4.0253906 2.1542969 L 4.2890625 0.30273438 A 0.26460996 0.26460996 0 0 0 4.0058594 0 z M 3.7148438 0.578125 L 3.5292969 1.8828125 L 2.2246094 2.0683594 L 2.4101562 0.76367188 L 3.7148438 0.578125 z "
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1.0;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.6 KiB |
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16.22291"
|
||||
height="15.99994"
|
||||
viewBox="0 0 4.2923115 4.2333176"
|
||||
version="1.1"
|
||||
id="svg4268">
|
||||
<defs
|
||||
id="defs4262">
|
||||
<marker
|
||||
style="overflow:visible"
|
||||
id="DiamondL"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto">
|
||||
<path
|
||||
transform="scale(0.8)"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
|
||||
d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
|
||||
id="path1602" />
|
||||
</marker>
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata4265">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path
|
||||
id="path26"
|
||||
d="M 0.81445312 1.8613281 A 0.2645835 0.2645835 0 0 0 0.5703125 2.0488281 L 0.01171875 3.8925781 A 0.26460996 0.26460996 0 0 0 0.33398438 4.2246094 L 2.1933594 3.7246094 A 0.2645835 0.2645835 0 0 0 2.3808594 3.4003906 A 0.2645835 0.2645835 0 0 0 2.0566406 3.2128906 L 0.65625 3.5898438 L 1.0761719 2.2011719 A 0.2645835 0.2645835 0 0 0 0.90039062 1.8730469 A 0.2645835 0.2645835 0 0 0 0.81445312 1.8613281 z "
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1" />
|
||||
<path
|
||||
id="path22"
|
||||
d="M 1.7226562 2.1953125 L 0.51171875 3.4160156 L 0.88671875 3.7890625 L 2.0996094 2.5683594 L 1.7226562 2.1953125 z "
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1" />
|
||||
<path
|
||||
id="path18"
|
||||
d="M 4.0058594 0 A 0.26460996 0.26460996 0 0 0 3.9902344 0.001953125 L 2.1386719 0.26757812 A 0.26460996 0.26460996 0 0 0 1.9140625 0.4921875 L 1.6484375 2.34375 A 0.26460996 0.26460996 0 0 0 1.9492188 2.6425781 L 3.8007812 2.3789062 A 0.26460996 0.26460996 0 0 0 4.0253906 2.1542969 L 4.2890625 0.30273438 A 0.26460996 0.26460996 0 0 0 4.0058594 0 z M 3.7148438 0.578125 L 3.5292969 1.8828125 L 2.2246094 2.0683594 L 2.4101562 0.76367188 L 3.7148438 0.578125 z "
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1" />
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 114 KiB |
@ -38419,6 +38419,12 @@ some elements to have a stereotype</val>
|
||||
<type>
|
||||
<ref refid="DCE:8BD30E4E-8352-11DD-8EDA-000D936B094A"/>
|
||||
</type>
|
||||
<upperValue>
|
||||
<val>1</val>
|
||||
</upperValue>
|
||||
<upperValue>
|
||||
<val>1</val>
|
||||
</upperValue>
|
||||
</Property>
|
||||
<Property id="DCE:89E367C0-8353-11DD-8EDA-000D936B094A">
|
||||
<aggregation>
|
||||
|
@ -35,7 +35,7 @@ classifiers = [
|
||||
python = "^3.7"
|
||||
pycairo = "^1.18"
|
||||
PyGObject = "^3.30"
|
||||
gaphas = "^2.0"
|
||||
gaphas = "^2.1"
|
||||
importlib_metadata = "^1.4"
|
||||
typing_extensions = "^3.7.4"
|
||||
generic = "^1.0.0"
|
||||
|
@ -59,10 +59,10 @@ def test_class_association_undo_redo(element_factory, undo_manager):
|
||||
assert 0 == len(diagram.canvas.solver.constraints)
|
||||
|
||||
ci1 = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
||||
assert 2 == len(diagram.canvas.solver.constraints)
|
||||
assert 6 == len(diagram.canvas.solver.constraints)
|
||||
|
||||
ci2 = diagram.create(ClassItem, subject=element_factory.create(UML.Class))
|
||||
assert 4 == len(diagram.canvas.solver.constraints)
|
||||
assert 12 == len(diagram.canvas.solver.constraints)
|
||||
|
||||
a = diagram.create(AssociationItem)
|
||||
|
||||
@ -71,7 +71,7 @@ def test_class_association_undo_redo(element_factory, undo_manager):
|
||||
|
||||
# Diagram, Association, 2x Class, Property, LiteralSpecification
|
||||
assert 6 == len(element_factory.lselect())
|
||||
assert 6 == len(diagram.canvas.solver.constraints)
|
||||
assert 14 == len(diagram.canvas.solver.constraints)
|
||||
|
||||
@transactional
|
||||
def delete_class():
|
||||
@ -97,11 +97,11 @@ def test_class_association_undo_redo(element_factory, undo_manager):
|
||||
assert None is get_connected(a.tail)
|
||||
|
||||
for i in range(3):
|
||||
assert 3 == len(diagram.canvas.solver.constraints)
|
||||
assert 7 == len(diagram.canvas.solver.constraints)
|
||||
|
||||
undo_manager.undo_transaction()
|
||||
|
||||
assert 6 == len(diagram.canvas.solver.constraints)
|
||||
assert 14 == len(diagram.canvas.solver.constraints)
|
||||
|
||||
assert ci1 == get_connected(a.head)
|
||||
assert ci2 == get_connected(a.tail)
|
||||
|
Loading…
x
Reference in New Issue
Block a user