Merge branch 'master' into css-selectors

# Conflicts:
#	poetry.lock
This commit is contained in:
Dan Yeaw 2020-07-03 09:58:22 -04:00
commit 4559d549da
No known key found for this signature in database
GPG Key ID: 77A923EF537B61A4
34 changed files with 2512 additions and 1955 deletions

View File

@ -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

View 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()

View 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()
)

View File

View File

@ -0,0 +1 @@
from gaphor.SysML.tests.fixtures import diagram, element_factory, event_manager

View 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

View File

@ -1 +1 @@
from gaphor.diagram.tests.fixtures import diagram, element_factory, event_manager
from gaphor.SysML.tests.fixtures import diagram, element_factory, event_manager

View File

@ -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(

View File

@ -1 +1 @@
from gaphor.diagram.tests.fixtures import diagram, element_factory, event_manager
from gaphor.SysML.tests.fixtures import diagram, element_factory, event_manager

View 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())
)

View File

@ -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),
),
),
),

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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"),

View File

@ -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"

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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)

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -15,6 +15,8 @@ ICONS=diagram \
package \
interface \
association \
composite-association \
shared-association \
generalization \
dependency \
implementation \

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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)