Merge pull request #273 from gaphor/interaction-updates
Sequence diagram updates
This commit is contained in:
commit
3603850ef0
11
Makefile
11
Makefile
@ -6,6 +6,9 @@ help: ## Show this help
|
||||
dist: translate ## Build application distribution (requires Poetry)
|
||||
poetry build
|
||||
|
||||
test: ## Run all but slow tests (requires PyTest)
|
||||
pytest -m "not slow"
|
||||
|
||||
docs: ## Generate documentation (requirss Sphinx)
|
||||
$(MAKE) -C docs html
|
||||
|
||||
@ -15,9 +18,9 @@ icons: ## Generate icons from stensil (requires Inkscape)
|
||||
translate: ## Translate and update .po and .mo files (requires PyBabel)
|
||||
$(MAKE) -C po
|
||||
|
||||
model: gaphor/UML/uml2.py ## Generate Python model files from Gaphor models (requires Black)
|
||||
model: gaphor/UML/uml2.py ## Generate Python model files from Gaphor models (requires Black, MyPy)
|
||||
|
||||
gaphor/UML/uml2.py: gaphor/UML/uml2.gaphor utils/model/gen_uml.py utils/model/override.py utils/model/writer.py
|
||||
utils/model/build_uml.py && black $@
|
||||
gaphor/UML/uml2.py: gaphor/UML/uml2.gaphor gaphor/UML/uml2.override utils/model/gen_uml.py utils/model/override.py utils/model/writer.py
|
||||
utils/model/build_uml.py && black $@ && mypy gaphor/UML
|
||||
|
||||
.PHONY: help dist docs icons translate model
|
||||
.PHONY: help dist test docs icons translate model
|
||||
|
292
examples/sequence-diagram.gaphor
Normal file
292
examples/sequence-diagram.gaphor
Normal file
@ -0,0 +1,292 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<gaphor xmlns="http://gaphor.sourceforge.net/model" version="3.0" gaphor-version="1.2.0rc2">
|
||||
<Package id="88ff97d6-5c0c-11ea-8042-9771210c7122">
|
||||
<name>
|
||||
<val>New model</val>
|
||||
</name>
|
||||
<ownedDiagram>
|
||||
<reflist>
|
||||
<ref refid="88ff97d7-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</ownedDiagram>
|
||||
</Package>
|
||||
<Diagram id="88ff97d7-5c0c-11ea-8042-9771210c7122">
|
||||
<name>
|
||||
<val>main</val>
|
||||
</name>
|
||||
<package>
|
||||
<ref refid="88ff97d6-5c0c-11ea-8042-9771210c7122"/>
|
||||
</package>
|
||||
<canvas>
|
||||
<item id="8afbf869-5c0c-11ea-8042-9771210c7122" type="LifelineItem">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 141.0, 205.5)</val>
|
||||
</matrix>
|
||||
<width>
|
||||
<val>100.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>50.0</val>
|
||||
</height>
|
||||
<subject>
|
||||
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
|
||||
</subject>
|
||||
<lifetime-length>
|
||||
<val>172.0</val>
|
||||
</lifetime-length>
|
||||
<item id="8ed9440e-5c0c-11ea-8042-9771210c7122" type="ExecutionSpecificationItem">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 50.0, 75.0)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(0.0, 0.0), (0.0, 100.5)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="8afbf869-5c0c-11ea-8042-9771210c7122"/>
|
||||
</head-connection>
|
||||
<subject>
|
||||
<ref refid="8ed9440f-5c0c-11ea-8042-9771210c7122"/>
|
||||
</subject>
|
||||
</item>
|
||||
</item>
|
||||
<item id="91554be3-5c0c-11ea-8042-9771210c7122" type="LifelineItem">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 416.0, 205.5)</val>
|
||||
</matrix>
|
||||
<width>
|
||||
<val>100.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>50.0</val>
|
||||
</height>
|
||||
<subject>
|
||||
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
|
||||
</subject>
|
||||
<lifetime-length>
|
||||
<val>171.5</val>
|
||||
</lifetime-length>
|
||||
<item id="9f6d722c-5c0c-11ea-8042-9771210c7122" type="ExecutionSpecificationItem">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 50.0, 92.0)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(0.0, 0.0), (0.0, 74.5)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="91554be3-5c0c-11ea-8042-9771210c7122"/>
|
||||
</head-connection>
|
||||
<subject>
|
||||
<ref refid="d4ed0b38-5c0c-11ea-8042-9771210c7122"/>
|
||||
</subject>
|
||||
</item>
|
||||
</item>
|
||||
<item id="a2af211a-5c0c-11ea-8042-9771210c7122" type="MessageItem">
|
||||
<subject>
|
||||
<ref refid="a2af211b-5c0c-11ea-8042-9771210c7122"/>
|
||||
</subject>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 197.0, 308.0)</val>
|
||||
</matrix>
|
||||
<orthogonal>
|
||||
<val>0</val>
|
||||
</orthogonal>
|
||||
<horizontal>
|
||||
<val>0</val>
|
||||
</horizontal>
|
||||
<points>
|
||||
<val>[(0.0, 0.0), (263.0, 2.0)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="8ed9440e-5c0c-11ea-8042-9771210c7122"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="9f6d722c-5c0c-11ea-8042-9771210c7122"/>
|
||||
</tail-connection>
|
||||
</item>
|
||||
<item id="a632aa2a-5c0c-11ea-8042-9771210c7122" type="MessageItem">
|
||||
<subject>
|
||||
<ref refid="a632aa2b-5c0c-11ea-8042-9771210c7122"/>
|
||||
</subject>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 460.0, 360.5)</val>
|
||||
</matrix>
|
||||
<orthogonal>
|
||||
<val>0</val>
|
||||
</orthogonal>
|
||||
<horizontal>
|
||||
<val>0</val>
|
||||
</horizontal>
|
||||
<points>
|
||||
<val>[(0.0, 0.0), (-263.0, -2.5)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="9f6d722c-5c0c-11ea-8042-9771210c7122"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="8ed9440e-5c0c-11ea-8042-9771210c7122"/>
|
||||
</tail-connection>
|
||||
</item>
|
||||
</canvas>
|
||||
</Diagram>
|
||||
<Lifeline id="8afbf868-5c0c-11ea-8042-9771210c7122">
|
||||
<coveredBy>
|
||||
<reflist>
|
||||
<ref refid="8ed94410-5c0c-11ea-8042-9771210c7122"/>
|
||||
<ref refid="8ed94411-5c0c-11ea-8042-9771210c7122"/>
|
||||
<ref refid="d4ed0b3c-5c0c-11ea-8042-9771210c7122"/>
|
||||
<ref refid="d4ed0b3d-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</coveredBy>
|
||||
<name>
|
||||
<val>Caller</val>
|
||||
</name>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="8afbf869-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</Lifeline>
|
||||
<BehaviorExecutionSpecification id="8ed9440f-5c0c-11ea-8042-9771210c7122">
|
||||
<executionOccurrenceSpecification>
|
||||
<reflist>
|
||||
<ref refid="8ed94410-5c0c-11ea-8042-9771210c7122"/>
|
||||
<ref refid="8ed94411-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</executionOccurrenceSpecification>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="8ed9440e-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</BehaviorExecutionSpecification>
|
||||
<ExecutionOccurrenceSpecification id="8ed94410-5c0c-11ea-8042-9771210c7122">
|
||||
<covered>
|
||||
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
|
||||
</covered>
|
||||
<execution>
|
||||
<ref refid="8ed9440f-5c0c-11ea-8042-9771210c7122"/>
|
||||
</execution>
|
||||
</ExecutionOccurrenceSpecification>
|
||||
<ExecutionOccurrenceSpecification id="8ed94411-5c0c-11ea-8042-9771210c7122">
|
||||
<covered>
|
||||
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
|
||||
</covered>
|
||||
<execution>
|
||||
<ref refid="8ed9440f-5c0c-11ea-8042-9771210c7122"/>
|
||||
</execution>
|
||||
</ExecutionOccurrenceSpecification>
|
||||
<Lifeline id="91554be2-5c0c-11ea-8042-9771210c7122">
|
||||
<coveredBy>
|
||||
<reflist>
|
||||
<ref refid="d4ed0b39-5c0c-11ea-8042-9771210c7122"/>
|
||||
<ref refid="d4ed0b3a-5c0c-11ea-8042-9771210c7122"/>
|
||||
<ref refid="d4ed0b3b-5c0c-11ea-8042-9771210c7122"/>
|
||||
<ref refid="d4ed0b3e-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</coveredBy>
|
||||
<name>
|
||||
<val>Callee</val>
|
||||
</name>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="91554be3-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</Lifeline>
|
||||
<Message id="a2af211b-5c0c-11ea-8042-9771210c7122">
|
||||
<name>
|
||||
<val>call()</val>
|
||||
</name>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="a2af211a-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
<receiveEvent>
|
||||
<ref refid="d4ed0b3e-5c0c-11ea-8042-9771210c7122"/>
|
||||
</receiveEvent>
|
||||
<sendEvent>
|
||||
<ref refid="d4ed0b3d-5c0c-11ea-8042-9771210c7122"/>
|
||||
</sendEvent>
|
||||
</Message>
|
||||
<Message id="a632aa2b-5c0c-11ea-8042-9771210c7122">
|
||||
<messageSort>
|
||||
<val>reply</val>
|
||||
</messageSort>
|
||||
<name>
|
||||
<val></val>
|
||||
</name>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="a632aa2a-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
<receiveEvent>
|
||||
<ref refid="d4ed0b3c-5c0c-11ea-8042-9771210c7122"/>
|
||||
</receiveEvent>
|
||||
<sendEvent>
|
||||
<ref refid="d4ed0b3b-5c0c-11ea-8042-9771210c7122"/>
|
||||
</sendEvent>
|
||||
</Message>
|
||||
<BehaviorExecutionSpecification id="d4ed0b38-5c0c-11ea-8042-9771210c7122">
|
||||
<executionOccurrenceSpecification>
|
||||
<reflist>
|
||||
<ref refid="d4ed0b39-5c0c-11ea-8042-9771210c7122"/>
|
||||
<ref refid="d4ed0b3a-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</executionOccurrenceSpecification>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="9f6d722c-5c0c-11ea-8042-9771210c7122"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</BehaviorExecutionSpecification>
|
||||
<ExecutionOccurrenceSpecification id="d4ed0b39-5c0c-11ea-8042-9771210c7122">
|
||||
<covered>
|
||||
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
|
||||
</covered>
|
||||
<execution>
|
||||
<ref refid="d4ed0b38-5c0c-11ea-8042-9771210c7122"/>
|
||||
</execution>
|
||||
</ExecutionOccurrenceSpecification>
|
||||
<ExecutionOccurrenceSpecification id="d4ed0b3a-5c0c-11ea-8042-9771210c7122">
|
||||
<covered>
|
||||
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
|
||||
</covered>
|
||||
<execution>
|
||||
<ref refid="d4ed0b38-5c0c-11ea-8042-9771210c7122"/>
|
||||
</execution>
|
||||
</ExecutionOccurrenceSpecification>
|
||||
<MessageOccurrenceSpecification id="d4ed0b3b-5c0c-11ea-8042-9771210c7122">
|
||||
<covered>
|
||||
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
|
||||
</covered>
|
||||
<sendMessage>
|
||||
<ref refid="a632aa2b-5c0c-11ea-8042-9771210c7122"/>
|
||||
</sendMessage>
|
||||
</MessageOccurrenceSpecification>
|
||||
<MessageOccurrenceSpecification id="d4ed0b3c-5c0c-11ea-8042-9771210c7122">
|
||||
<covered>
|
||||
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
|
||||
</covered>
|
||||
<receiveMessage>
|
||||
<ref refid="a632aa2b-5c0c-11ea-8042-9771210c7122"/>
|
||||
</receiveMessage>
|
||||
</MessageOccurrenceSpecification>
|
||||
<MessageOccurrenceSpecification id="d4ed0b3d-5c0c-11ea-8042-9771210c7122">
|
||||
<covered>
|
||||
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
|
||||
</covered>
|
||||
<sendMessage>
|
||||
<ref refid="a2af211b-5c0c-11ea-8042-9771210c7122"/>
|
||||
</sendMessage>
|
||||
</MessageOccurrenceSpecification>
|
||||
<MessageOccurrenceSpecification id="d4ed0b3e-5c0c-11ea-8042-9771210c7122">
|
||||
<covered>
|
||||
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
|
||||
</covered>
|
||||
<receiveMessage>
|
||||
<ref refid="a2af211b-5c0c-11ea-8042-9771210c7122"/>
|
||||
</receiveMessage>
|
||||
</MessageOccurrenceSpecification>
|
||||
</gaphor>
|
@ -169,7 +169,8 @@ class collection(Generic[T]):
|
||||
|
||||
self.object.handle(AssociationUpdated(self.object, self.property))
|
||||
return True
|
||||
except IndexError:
|
||||
return False
|
||||
except ValueError:
|
||||
except (IndexError, ValueError):
|
||||
return False
|
||||
|
||||
def order(self, key):
|
||||
self.items.sort(key=key)
|
||||
|
@ -111,7 +111,7 @@ relation = Union[relation_one, relation_many]
|
||||
T = TypeVar("T")
|
||||
|
||||
Lower = Union[Literal[0], Literal[1], Literal[2]]
|
||||
Upper = Union[Literal[1], Literal["*"]]
|
||||
Upper = Union[Literal[1], Literal[2], Literal["*"]]
|
||||
|
||||
|
||||
class umlproperty:
|
||||
@ -381,7 +381,9 @@ class association(umlproperty):
|
||||
|
||||
def _set_one(self, obj, value, from_opposite, do_notify) -> None:
|
||||
if not (isinstance(value, self.type) or (value is None)):
|
||||
raise AttributeError(f"Value should be of type {self.type.__name__}")
|
||||
raise AttributeError(
|
||||
f"Value should be of type {self.type.__name__}, got a {type(value)} instead"
|
||||
)
|
||||
|
||||
old = self._get(obj)
|
||||
|
||||
@ -616,7 +618,7 @@ class derived(umlproperty, Generic[T]):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return f"<derived {self.name}: {str(list(map(str, self.subsets)))[1:-1]}>"
|
||||
return f"<derived {self.name}: {self.type} {str(list(map(str, self.subsets)))[1:-1]}>"
|
||||
|
||||
def _update(self, obj):
|
||||
"""
|
||||
@ -841,7 +843,7 @@ class redefine(umlproperty):
|
||||
self.name = name
|
||||
self._name = "_" + name
|
||||
self.type = type
|
||||
self.original = original
|
||||
self.original: Union[association, derived] = original
|
||||
self.upper = original.upper
|
||||
self.lower = original.lower
|
||||
|
||||
@ -872,32 +874,20 @@ class redefine(umlproperty):
|
||||
def __str__(self) -> str:
|
||||
return f"<redefine {self.name}[{self.lower}..{self.upper}]: {self.type.__name__} = {str(self.original)}>"
|
||||
|
||||
def __get__(self, obj, class_=None):
|
||||
# No longer needed
|
||||
if not obj:
|
||||
return self
|
||||
return self.original.__get__(obj, class_)
|
||||
|
||||
def __set__(self, obj, value: T) -> None:
|
||||
# No longer needed
|
||||
if not isinstance(value, self.type):
|
||||
raise AttributeError(f"Value should be of type {self.type.__name__}")
|
||||
self.original.__set__(obj, value)
|
||||
|
||||
def __delete__(self, obj, value=None):
|
||||
# No longer needed
|
||||
self.original.__delete__(obj, value)
|
||||
|
||||
def _get(self, obj):
|
||||
return self.original._get(obj)
|
||||
|
||||
def _set(self, obj, value, from_opposite=False):
|
||||
def _set(self, obj, value, from_opposite=False, do_notify=True):
|
||||
if not (isinstance(value, self.type) or (self.upper == 1 and value is None)):
|
||||
raise AttributeError(
|
||||
f"Value should be of type {self.type.__name__}, got a {type(value)} instead"
|
||||
)
|
||||
assert isinstance(self.original, association)
|
||||
return self.original._set(obj, value, from_opposite)
|
||||
return self.original._set(obj, value, from_opposite, do_notify)
|
||||
|
||||
def _del(self, obj, value, from_opposite=False):
|
||||
def _del(self, obj, value, from_opposite=False, do_notify=True):
|
||||
assert isinstance(self.original, association)
|
||||
return self.original._del(obj, value, from_opposite)
|
||||
return self.original._del(obj, value, from_opposite, do_notify)
|
||||
|
||||
def propagate(self, event):
|
||||
if event.property is self.original and isinstance(
|
||||
|
@ -285,7 +285,7 @@ def test_realization(factory):
|
||||
# Tests for interaction messages.
|
||||
|
||||
|
||||
def test_create(factory):
|
||||
def test_interaction_messages_cloning(factory):
|
||||
"""Test message creation."""
|
||||
m = factory.create(UML.Message)
|
||||
send = factory.create(UML.MessageOccurrenceSpecification)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -165,3 +165,11 @@ override StructuredClassifier.part: property
|
||||
StructuredClassifier.part = property(lambda self: tuple(a for a in self.ownedAttribute if a.isComposite), doc="""
|
||||
Properties owned by a classifier by composition.
|
||||
""")
|
||||
%%
|
||||
override ExecutionSpecification.start(ExecutionSpecification.executionOccurrenceSpecification): relation_one[ExecutionOccurrenceSpecification]
|
||||
ExecutionSpecification.start = derived(ExecutionSpecification, 'start', OccurrenceSpecification, 0, 1,
|
||||
lambda obj: [eos for i, eos in enumerate(obj.executionOccurrenceSpecification) if i == 0])
|
||||
%%
|
||||
override ExecutionSpecification.finish(ExecutionSpecification.executionOccurrenceSpecification): relation_one[ExecutionOccurrenceSpecification]
|
||||
ExecutionSpecification.finish = derived(ExecutionSpecification, 'finish', OccurrenceSpecification, 0, 1,
|
||||
lambda obj: [eos for i, eos in enumerate(obj.executionOccurrenceSpecification) if i == 1])
|
||||
|
@ -529,6 +529,7 @@ class ValuePin(InputPin):
|
||||
|
||||
class Action(ExecutableNode):
|
||||
effect: attribute[str]
|
||||
interaction: relation_one[Interaction]
|
||||
output: relation_many[OutputPin]
|
||||
context_: relation_one[Classifier]
|
||||
input: relation_many[InputPin]
|
||||
@ -561,6 +562,7 @@ class ActivityGroup(Element):
|
||||
class Constraint(PackageableElement):
|
||||
constrainedElement: relation_many[Element]
|
||||
specification: attribute[str]
|
||||
stateInvariant: relation_one[StateInvariant]
|
||||
owningState: relation_one[State]
|
||||
context: derivedunion[Namespace]
|
||||
|
||||
@ -575,22 +577,17 @@ class Interaction(Behavior, InteractionFragment):
|
||||
fragment: relation_many[InteractionFragment]
|
||||
lifeline: relation_many[Lifeline]
|
||||
message: relation_many[Message]
|
||||
|
||||
|
||||
class ExecutionOccurence(InteractionFragment):
|
||||
finish: relation_one[OccurrenceSpecification]
|
||||
start: relation_one[OccurrenceSpecification]
|
||||
behavior: relation_many[Behavior]
|
||||
action: relation_many[Action]
|
||||
|
||||
|
||||
class StateInvariant(InteractionFragment):
|
||||
invariant: relation_one[Constraint]
|
||||
covered: relation_one[Lifeline] # type: ignore[assignment]
|
||||
|
||||
|
||||
class Lifeline(NamedElement):
|
||||
coveredBy: relation_many[InteractionFragment]
|
||||
interaction: relation_one[Interaction]
|
||||
discriminator: attribute[str]
|
||||
parse: Callable[[Lifeline, str], None]
|
||||
render: Callable[[Lifeline], str]
|
||||
|
||||
@ -599,10 +596,10 @@ class Message(NamedElement):
|
||||
messageKind: property
|
||||
messageSort: enumeration
|
||||
argument: attribute[str]
|
||||
signature: relation_one[NamedElement]
|
||||
sendEvent: relation_one[MessageEnd]
|
||||
receiveEvent: relation_one[MessageEnd]
|
||||
interaction: relation_one[Interaction]
|
||||
signature: relation_one[NamedElement]
|
||||
|
||||
|
||||
class MessageEnd(NamedElement):
|
||||
@ -611,15 +608,11 @@ class MessageEnd(NamedElement):
|
||||
|
||||
|
||||
class OccurrenceSpecification(InteractionFragment):
|
||||
toAfter: relation_many[GeneralOrdering]
|
||||
toBefore: relation_many[GeneralOrdering]
|
||||
finishExec: relation_many[ExecutionOccurence]
|
||||
startExec: relation_many[ExecutionOccurence]
|
||||
covered: relation_one[Lifeline] # type: ignore[assignment]
|
||||
|
||||
|
||||
class GeneralOrdering(NamedElement):
|
||||
before: relation_one[OccurrenceSpecification]
|
||||
after: relation_one[OccurrenceSpecification]
|
||||
interactionFragment: relation_one[InteractionFragment]
|
||||
|
||||
|
||||
class Connector(Feature):
|
||||
@ -766,38 +759,6 @@ class Event(PackageableElement):
|
||||
pass
|
||||
|
||||
|
||||
class ExecutionEvent(Event):
|
||||
pass
|
||||
|
||||
|
||||
class CreationEvent(Event):
|
||||
pass
|
||||
|
||||
|
||||
class MessageEvent(Event):
|
||||
pass
|
||||
|
||||
|
||||
class DestructionEvent(Event):
|
||||
pass
|
||||
|
||||
|
||||
class SendOperationEvent(MessageEvent):
|
||||
operation: relation_one[Operation]
|
||||
|
||||
|
||||
class SendSignalEvent(MessageEvent):
|
||||
signal: relation_one[Signal]
|
||||
|
||||
|
||||
class ReceiveOperationEvent(MessageEvent):
|
||||
operation: relation_one[Operation]
|
||||
|
||||
|
||||
class ReceiveSignalEvent(MessageEvent):
|
||||
signal: relation_one[Signal]
|
||||
|
||||
|
||||
class Signal(Classifier):
|
||||
ownedAttribute: relation_many[Property]
|
||||
|
||||
@ -806,6 +767,24 @@ class Reception(BehavioralFeature):
|
||||
signal: relation_one[Signal]
|
||||
|
||||
|
||||
class ExecutionSpecification(InteractionFragment):
|
||||
executionOccurrenceSpecification: relation_many[ExecutionOccurrenceSpecification]
|
||||
start: relation_one[ExecutionOccurrenceSpecification]
|
||||
finish: relation_one[ExecutionOccurrenceSpecification]
|
||||
|
||||
|
||||
class ExecutionOccurrenceSpecification(OccurrenceSpecification):
|
||||
execution: relation_one[ExecutionSpecification]
|
||||
|
||||
|
||||
class ActionExecutionSpecification(ExecutionSpecification):
|
||||
action: relation_one[Action]
|
||||
|
||||
|
||||
class BehaviorExecutionSpecification(ExecutionSpecification):
|
||||
behavior: relation_one[Behavior]
|
||||
|
||||
|
||||
# class 'ValueSpecification' has been stereotyped as 'SimpleAttribute'
|
||||
# class 'InstanceValue' has been stereotyped as 'SimpleAttribute' too
|
||||
# class 'Expression' has been stereotyped as 'SimpleAttribute' too
|
||||
@ -1185,8 +1164,11 @@ InteractionFragment.enclosingInteraction = association(
|
||||
Interaction.fragment = association(
|
||||
"fragment", InteractionFragment, opposite="enclosingInteraction"
|
||||
)
|
||||
Constraint.stateInvariant = association(
|
||||
"stateInvariant", StateInvariant, upper=1, opposite="invariant"
|
||||
)
|
||||
StateInvariant.invariant = association(
|
||||
"invariant", Constraint, lower=1, upper=1, composite=True
|
||||
"invariant", Constraint, lower=1, upper=1, composite=True, opposite="stateInvariant"
|
||||
)
|
||||
Lifeline.coveredBy = association("coveredBy", InteractionFragment, opposite="covered")
|
||||
InteractionFragment.covered = association(
|
||||
@ -1198,11 +1180,8 @@ Lifeline.interaction = association(
|
||||
Interaction.lifeline = association(
|
||||
"lifeline", Lifeline, composite=True, opposite="interaction"
|
||||
)
|
||||
# 'Lifeline.discriminator' is a simple attribute
|
||||
Lifeline.discriminator = attribute("discriminator", str)
|
||||
# 'Message.argument' is a simple attribute
|
||||
Message.argument = attribute("argument", str)
|
||||
Message.signature = association("signature", NamedElement, upper=1)
|
||||
MessageEnd.sendMessage = association(
|
||||
"sendMessage", Message, upper=1, opposite="sendEvent"
|
||||
)
|
||||
@ -1221,34 +1200,6 @@ Message.interaction = association(
|
||||
Interaction.message = association(
|
||||
"message", Message, composite=True, opposite="interaction"
|
||||
)
|
||||
InteractionFragment.generalOrdering = association(
|
||||
"generalOrdering", GeneralOrdering, composite=True
|
||||
)
|
||||
GeneralOrdering.before = association(
|
||||
"before", OccurrenceSpecification, lower=1, upper=1, opposite="toAfter"
|
||||
)
|
||||
OccurrenceSpecification.toAfter = association(
|
||||
"toAfter", GeneralOrdering, opposite="before"
|
||||
)
|
||||
GeneralOrdering.after = association(
|
||||
"after", OccurrenceSpecification, lower=1, upper=1, opposite="toBefore"
|
||||
)
|
||||
OccurrenceSpecification.toBefore = association(
|
||||
"toBefore", GeneralOrdering, opposite="after"
|
||||
)
|
||||
ExecutionOccurence.finish = association(
|
||||
"finish", OccurrenceSpecification, lower=1, upper=1, opposite="finishExec"
|
||||
)
|
||||
OccurrenceSpecification.finishExec = association(
|
||||
"finishExec", ExecutionOccurence, opposite="finish"
|
||||
)
|
||||
ExecutionOccurence.start = association(
|
||||
"start", OccurrenceSpecification, lower=1, upper=1, opposite="startExec"
|
||||
)
|
||||
OccurrenceSpecification.startExec = association(
|
||||
"startExec", ExecutionOccurence, opposite="start"
|
||||
)
|
||||
ExecutionOccurence.behavior = association("behavior", Behavior)
|
||||
StructuredClassifier.ownedConnector = association(
|
||||
"ownedConnector", Connector, composite=True
|
||||
)
|
||||
@ -1343,10 +1294,33 @@ Signal.ownedAttribute = association("ownedAttribute", Property, composite=True)
|
||||
Reception.signal = association("signal", Signal, upper=1)
|
||||
Class.ownedReception = association("ownedReception", Reception, composite=True)
|
||||
Interface.ownedReception = association("ownedReception", Reception, composite=True)
|
||||
SendOperationEvent.operation = association("operation", Operation, lower=1, upper=1)
|
||||
SendSignalEvent.signal = association("signal", Signal, lower=1, upper=1)
|
||||
ReceiveOperationEvent.operation = association("operation", Operation, lower=1, upper=1)
|
||||
ReceiveSignalEvent.signal = association("signal", Signal, lower=1, upper=1)
|
||||
Action.interaction = association("interaction", Interaction, upper=1, opposite="action")
|
||||
Interaction.action = association(
|
||||
"action", Action, composite=True, opposite="interaction"
|
||||
)
|
||||
Message.signature = association("signature", NamedElement, upper=1)
|
||||
InteractionFragment.generalOrdering = association(
|
||||
"generalOrdering", GeneralOrdering, composite=True, opposite="interactionFragment"
|
||||
)
|
||||
GeneralOrdering.interactionFragment = association(
|
||||
"interactionFragment", InteractionFragment, upper=1, opposite="generalOrdering"
|
||||
)
|
||||
ExecutionSpecification.executionOccurrenceSpecification = association(
|
||||
"executionOccurrenceSpecification",
|
||||
ExecutionOccurrenceSpecification,
|
||||
upper=2,
|
||||
composite=True,
|
||||
opposite="execution",
|
||||
)
|
||||
ExecutionOccurrenceSpecification.execution = association(
|
||||
"execution",
|
||||
ExecutionSpecification,
|
||||
lower=1,
|
||||
upper=1,
|
||||
opposite="executionOccurrenceSpecification",
|
||||
)
|
||||
ActionExecutionSpecification.action = association("action", Action, lower=1, upper=1)
|
||||
BehaviorExecutionSpecification.behavior = association("behavior", Behavior, upper=1)
|
||||
# 96: override NamedElement.qualifiedName(NamedElement.namespace): derived[List[str]]
|
||||
# defined in uml2overrides.py
|
||||
|
||||
@ -1463,6 +1437,7 @@ NamedElement.namespace = derivedunion(
|
||||
Parameter.ownerFormalParam,
|
||||
Property.useCase,
|
||||
Property.actor,
|
||||
InteractionFragment.enclosingInteraction,
|
||||
Lifeline.interaction,
|
||||
Message.interaction,
|
||||
Region.stateMachine,
|
||||
@ -1503,7 +1478,7 @@ Namespace.ownedMember = derivedunion(
|
||||
BehavioredClassifier.ownedBehavior,
|
||||
UseCase.ownedAttribute,
|
||||
Actor.ownedAttribute,
|
||||
StateInvariant.invariant,
|
||||
Interaction.fragment,
|
||||
Interaction.lifeline,
|
||||
Interaction.message,
|
||||
StateMachine.region,
|
||||
@ -1664,7 +1639,10 @@ Element.owner = derivedunion(
|
||||
PackageImport.importingNamespace,
|
||||
PackageMerge.mergingPackage,
|
||||
NamedElement.namespace,
|
||||
Constraint.stateInvariant,
|
||||
Pseudostate.state,
|
||||
Action.interaction,
|
||||
GeneralOrdering.interactionFragment,
|
||||
)
|
||||
Element.ownedElement = derivedunion(
|
||||
Element,
|
||||
@ -1687,8 +1665,7 @@ Element.ownedElement = derivedunion(
|
||||
Activity.edge,
|
||||
Activity.node,
|
||||
Action.output,
|
||||
Interaction.fragment,
|
||||
InteractionFragment.generalOrdering,
|
||||
StateInvariant.invariant,
|
||||
Connector.end,
|
||||
State.entry,
|
||||
State.exit,
|
||||
@ -1697,6 +1674,8 @@ Element.ownedElement = derivedunion(
|
||||
State.statevariant,
|
||||
Transition.guard,
|
||||
DeploymentTarget.deployment,
|
||||
Interaction.action,
|
||||
InteractionFragment.generalOrdering,
|
||||
)
|
||||
ConnectorEnd.definingEnd = derivedunion(ConnectorEnd, "definingEnd", Property, 0, 1)
|
||||
# 164: override StructuredClassifier.part: property
|
||||
@ -1707,6 +1686,30 @@ StructuredClassifier.part = property(
|
||||
""",
|
||||
)
|
||||
|
||||
# 169: override ExecutionSpecification.start(ExecutionSpecification.executionOccurrenceSpecification): relation_one[ExecutionOccurrenceSpecification]
|
||||
ExecutionSpecification.start = derived(
|
||||
ExecutionSpecification,
|
||||
"start",
|
||||
OccurrenceSpecification,
|
||||
0,
|
||||
1,
|
||||
lambda obj: [
|
||||
eos for i, eos in enumerate(obj.executionOccurrenceSpecification) if i == 0
|
||||
],
|
||||
)
|
||||
|
||||
# 173: override ExecutionSpecification.finish(ExecutionSpecification.executionOccurrenceSpecification): relation_one[ExecutionOccurrenceSpecification]
|
||||
ExecutionSpecification.finish = derived(
|
||||
ExecutionSpecification,
|
||||
"finish",
|
||||
OccurrenceSpecification,
|
||||
0,
|
||||
1,
|
||||
lambda obj: [
|
||||
eos for i, eos in enumerate(obj.executionOccurrenceSpecification) if i == 1
|
||||
],
|
||||
)
|
||||
|
||||
# 128: override Class.superClass: derived[Classifier]
|
||||
Class.superClass = Classifier.general
|
||||
|
||||
@ -1774,6 +1777,12 @@ Transition.redefinedTransition = redefine(
|
||||
"*",
|
||||
RedefinableElement.redefinedElement,
|
||||
)
|
||||
StateInvariant.covered = redefine(
|
||||
StateInvariant, "covered", Lifeline, 1, InteractionFragment.covered
|
||||
)
|
||||
OccurrenceSpecification.covered = redefine(
|
||||
OccurrenceSpecification, "covered", Lifeline, 1, InteractionFragment.covered
|
||||
)
|
||||
# 149: override Lifeline.parse: Callable[[Lifeline, str], None]
|
||||
# defined in uml2overrides.py
|
||||
|
||||
|
@ -209,7 +209,7 @@ def draw_decision_node(_box, context, _bounding_box):
|
||||
|
||||
|
||||
@represents(UML.ForkNode)
|
||||
class ForkNodeItem(UML.Presentation, Item):
|
||||
class ForkNodeItem(UML.Presentation[UML.ForkNode], Item):
|
||||
"""
|
||||
Representation of fork and join node.
|
||||
"""
|
||||
@ -244,6 +244,9 @@ class ForkNodeItem(UML.Presentation, Item):
|
||||
self.watch("subject.appliedStereotype.classifier.name")
|
||||
self.watch("subject[JoinNode].joinSpec")
|
||||
|
||||
self.constraint(vertical=(h1.pos, h2.pos))
|
||||
self.constraint(above=(h1.pos, h2.pos), delta=30)
|
||||
|
||||
def save(self, save_func):
|
||||
save_func("matrix", tuple(self.matrix))
|
||||
save_func("height", float(self._handles[1].pos.y))
|
||||
@ -269,28 +272,6 @@ class ForkNodeItem(UML.Presentation, Item):
|
||||
|
||||
combined = reversible_property(lambda s: s._combined, _set_combined)
|
||||
|
||||
def setup_canvas(self):
|
||||
assert self.canvas
|
||||
super().setup_canvas()
|
||||
|
||||
h1, h2 = self._handles
|
||||
cadd = self.canvas.solver.add_constraint
|
||||
c1 = EqualsConstraint(a=h1.pos.x, b=h2.pos.x)
|
||||
c2 = LessThanConstraint(smaller=h1.pos.y, bigger=h2.pos.y, delta=30)
|
||||
self.__constraints = (cadd(c1), cadd(c2))
|
||||
list(map(self.canvas.solver.add_constraint, self.__constraints))
|
||||
|
||||
def teardown_canvas(self):
|
||||
assert self.canvas
|
||||
super().teardown_canvas()
|
||||
list(map(self.canvas.solver.remove_constraint, self.__constraints))
|
||||
|
||||
def pre_update(self, context):
|
||||
cr = context.cairo
|
||||
_, h2 = self.handles()
|
||||
_, height = self.shape.size(cr)
|
||||
h2.pos.y = max(h2.pos.y, height)
|
||||
|
||||
def draw(self, context):
|
||||
h1, h2 = self.handles()
|
||||
height = h2.pos.y - h1.pos.y
|
||||
|
@ -20,7 +20,7 @@ from gaphor.diagram.actions.activitynodes import (
|
||||
)
|
||||
from gaphor.diagram.actions.flow import FlowItem
|
||||
from gaphor.diagram.actions.objectnode import ObjectNodeItem
|
||||
from gaphor.diagram.connectors import IConnect, UnaryRelationshipConnect
|
||||
from gaphor.diagram.connectors import Connector, UnaryRelationshipConnect
|
||||
|
||||
|
||||
class FlowConnect(UnaryRelationshipConnect):
|
||||
@ -77,7 +77,7 @@ class FlowConnect(UnaryRelationshipConnect):
|
||||
opposite = line.opposite(handle)
|
||||
otc = self.get_connected(opposite)
|
||||
if opposite and isinstance(otc, (ForkNodeItem, DecisionNodeItem)):
|
||||
adapter = IConnect(otc, line)
|
||||
adapter = Connector(otc, line)
|
||||
adapter.combine_nodes()
|
||||
|
||||
def disconnect_subject(self, handle):
|
||||
@ -86,15 +86,15 @@ class FlowConnect(UnaryRelationshipConnect):
|
||||
opposite = line.opposite(handle)
|
||||
otc = self.get_connected(opposite)
|
||||
if opposite and isinstance(otc, (ForkNodeItem, DecisionNodeItem)):
|
||||
adapter = IConnect(otc, line)
|
||||
adapter = Connector(otc, line)
|
||||
adapter.decombine_nodes()
|
||||
|
||||
|
||||
IConnect.register(ActionItem, FlowItem)(FlowConnect)
|
||||
IConnect.register(ActivityNodeItem, FlowItem)(FlowConnect)
|
||||
IConnect.register(ObjectNodeItem, FlowItem)(FlowConnect)
|
||||
IConnect.register(SendSignalActionItem, FlowItem)(FlowConnect)
|
||||
IConnect.register(AcceptEventActionItem, FlowItem)(FlowConnect)
|
||||
Connector.register(ActionItem, FlowItem)(FlowConnect)
|
||||
Connector.register(ActivityNodeItem, FlowItem)(FlowConnect)
|
||||
Connector.register(ObjectNodeItem, FlowItem)(FlowConnect)
|
||||
Connector.register(SendSignalActionItem, FlowItem)(FlowConnect)
|
||||
Connector.register(AcceptEventActionItem, FlowItem)(FlowConnect)
|
||||
|
||||
|
||||
class FlowForkDecisionNodeConnect(FlowConnect):
|
||||
@ -210,7 +210,7 @@ class FlowForkDecisionNodeConnect(FlowConnect):
|
||||
self.decombine_nodes()
|
||||
|
||||
|
||||
@IConnect.register(ForkNodeItem, FlowItem)
|
||||
@Connector.register(ForkNodeItem, FlowItem)
|
||||
class FlowForkNodeConnect(FlowForkDecisionNodeConnect):
|
||||
"""Connect Flow to a ForkNode."""
|
||||
|
||||
@ -218,7 +218,7 @@ class FlowForkNodeConnect(FlowForkDecisionNodeConnect):
|
||||
join_node_cls = UML.JoinNode
|
||||
|
||||
|
||||
@IConnect.register(DecisionNodeItem, FlowItem)
|
||||
@Connector.register(DecisionNodeItem, FlowItem)
|
||||
class FlowDecisionNodeConnect(FlowForkDecisionNodeConnect):
|
||||
"""Connect Flow to a DecisionNode."""
|
||||
|
||||
|
@ -6,14 +6,14 @@ from gaphor.diagram.classes.dependency import DependencyItem
|
||||
from gaphor.diagram.classes.generalization import GeneralizationItem
|
||||
from gaphor.diagram.classes.implementation import ImplementationItem
|
||||
from gaphor.diagram.connectors import (
|
||||
IConnect,
|
||||
Connector,
|
||||
RelationshipConnect,
|
||||
UnaryRelationshipConnect,
|
||||
)
|
||||
from gaphor.diagram.presentation import Classified, ElementPresentation, Named
|
||||
|
||||
|
||||
@IConnect.register(Named, DependencyItem)
|
||||
@Connector.register(Named, DependencyItem)
|
||||
class DependencyConnect(RelationshipConnect):
|
||||
"""Connect two Named elements using a Dependency."""
|
||||
|
||||
@ -68,7 +68,7 @@ class DependencyConnect(RelationshipConnect):
|
||||
line.subject = relation
|
||||
|
||||
|
||||
@IConnect.register(Classified, GeneralizationItem)
|
||||
@Connector.register(Classified, GeneralizationItem)
|
||||
class GeneralizationConnect(RelationshipConnect):
|
||||
"""Connect Classifiers with a Generalization relationship."""
|
||||
|
||||
@ -84,7 +84,7 @@ class GeneralizationConnect(RelationshipConnect):
|
||||
self.line.subject = relation
|
||||
|
||||
|
||||
@IConnect.register(Classified, AssociationItem)
|
||||
@Connector.register(Classified, AssociationItem)
|
||||
class AssociationConnect(UnaryRelationshipConnect):
|
||||
"""Connect association to classifier."""
|
||||
|
||||
@ -171,7 +171,7 @@ class AssociationConnect(UnaryRelationshipConnect):
|
||||
old.unlink()
|
||||
|
||||
|
||||
@IConnect.register(Named, ImplementationItem)
|
||||
@Connector.register(Named, ImplementationItem)
|
||||
class ImplementationConnect(RelationshipConnect):
|
||||
"""Connect Interface and a BehavioredClassifier using an Implementation."""
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from inspect import isclass
|
||||
|
||||
from gaphas.decorators import AsyncIO
|
||||
from gi.repository import Gtk
|
||||
@ -93,16 +94,36 @@ class ClassOperations(EditableTreeModel):
|
||||
return self._item.subject.ownedOperation.swap(o1, o2)
|
||||
|
||||
|
||||
def _issubclass(c, b):
|
||||
try:
|
||||
return issubclass(c, b)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
@PropertyPages.register(UML.Class)
|
||||
class ClassPropertyPage(NamedElementPropertyPage):
|
||||
"""Adapter which shows a property page for a class view."""
|
||||
"""Adapter which shows a property page for a class view.
|
||||
Also handles metaclasses.
|
||||
"""
|
||||
|
||||
subject: UML.Class
|
||||
|
||||
CLASSES = list(
|
||||
sorted(
|
||||
c
|
||||
for c in dir(UML)
|
||||
if _issubclass(getattr(UML, c), UML.Element) and c != "Stereotype"
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, subject):
|
||||
super().__init__(subject)
|
||||
|
||||
def construct(self):
|
||||
if UML.model.is_metaclass(self.subject):
|
||||
return self.construct_metaclass()
|
||||
|
||||
page = super().construct()
|
||||
|
||||
if not self.subject:
|
||||
@ -127,6 +148,45 @@ class ClassPropertyPage(NamedElementPropertyPage):
|
||||
def _on_abstract_change(self, button):
|
||||
self.subject.isAbstract = button.get_active()
|
||||
|
||||
def construct_metaclass(self):
|
||||
page = Gtk.VBox()
|
||||
|
||||
subject = self.subject
|
||||
if not subject:
|
||||
return page
|
||||
|
||||
hbox = create_hbox_label(self, page, gettext("Name"))
|
||||
model = Gtk.ListStore(str)
|
||||
for c in self.CLASSES:
|
||||
model.append([c])
|
||||
|
||||
cb = Gtk.ComboBox.new_with_model_and_entry(model)
|
||||
|
||||
completion = Gtk.EntryCompletion()
|
||||
completion.set_model(model)
|
||||
completion.set_minimum_key_length(1)
|
||||
completion.set_text_column(0)
|
||||
cb.get_child().set_completion(completion)
|
||||
|
||||
entry = cb.get_child()
|
||||
entry.set_text(subject and subject.name or "")
|
||||
hbox.pack_start(cb, True, True, 0)
|
||||
page.default = entry
|
||||
|
||||
# monitor subject.name attribute
|
||||
changed_id = entry.connect("changed", self._on_name_change)
|
||||
|
||||
def handler(event):
|
||||
if event.element is subject and event.new_value is not None:
|
||||
entry.handler_block(changed_id)
|
||||
entry.set_text(event.new_value)
|
||||
entry.handler_unblock(changed_id)
|
||||
|
||||
self.watcher.watch("name", handler).subscribe_all()
|
||||
entry.connect("destroy", self.watcher.unsubscribe_all)
|
||||
page.show_all()
|
||||
return page
|
||||
|
||||
|
||||
@PropertyPages.register(InterfaceItem)
|
||||
class InterfacePropertyPage(NamedItemPropertyPage):
|
||||
|
@ -11,10 +11,10 @@ from gaphor.diagram.classes.classconnect import DependencyConnect, Implementatio
|
||||
from gaphor.diagram.classes.dependency import DependencyItem
|
||||
from gaphor.diagram.classes.implementation import ImplementationItem
|
||||
from gaphor.diagram.classes.interface import Folded, InterfaceItem
|
||||
from gaphor.diagram.connectors import IConnect
|
||||
from gaphor.diagram.connectors import Connector
|
||||
|
||||
|
||||
@IConnect.register(InterfaceItem, ImplementationItem)
|
||||
@Connector.register(InterfaceItem, ImplementationItem)
|
||||
class ImplementationInterfaceConnect(ImplementationConnect):
|
||||
"""Connect interface item and a behaviored classifier using an
|
||||
implementation.
|
||||
@ -39,7 +39,7 @@ class ImplementationInterfaceConnect(ImplementationConnect):
|
||||
self.line.request_update()
|
||||
|
||||
|
||||
@IConnect.register(InterfaceItem, DependencyItem)
|
||||
@Connector.register(InterfaceItem, DependencyItem)
|
||||
class DependencyInterfaceConnect(DependencyConnect):
|
||||
"""Connect interface item with dependency item."""
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from gi.repository import Gtk
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.profiles.metaclasspropertypage import MetaclassNamePropertyPage
|
||||
from gaphor.diagram.classes.classespropertypages import ClassPropertyPage
|
||||
from gaphor.tests import TestCase
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ class MetaclassPropertyPageTest(TestCase):
|
||||
class_ = self.element_factory.create(UML.Class)
|
||||
|
||||
class_.name = "Class"
|
||||
editor = MetaclassNamePropertyPage(class_)
|
||||
editor = ClassPropertyPage(class_)
|
||||
page = editor.construct()
|
||||
assert page
|
||||
entry = page.get_children()[0].get_children()[1]
|
||||
@ -28,7 +28,7 @@ class MetaclassPropertyPageTest(TestCase):
|
||||
stereotype.name = "NewStereotype"
|
||||
UML.model.create_extension(metaclass, stereotype)
|
||||
|
||||
editor = MetaclassNamePropertyPage(metaclass)
|
||||
editor = ClassPropertyPage(metaclass)
|
||||
page = editor.construct()
|
||||
assert page
|
||||
combo = page.get_children()[0].get_children()[1]
|
@ -108,7 +108,7 @@ from gaphor.UML.modelfactory import stereotypes_str
|
||||
|
||||
|
||||
@represents(UML.Connector)
|
||||
class ConnectorItem(LinePresentation, Named):
|
||||
class ConnectorItem(LinePresentation[UML.Connector], Named):
|
||||
"""
|
||||
Connector item line.
|
||||
|
||||
|
@ -6,15 +6,22 @@ Implemented using interface item in assembly connector mode, see
|
||||
"""
|
||||
|
||||
import operator
|
||||
from typing import Union
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.classes.interface import Folded, InterfaceItem
|
||||
from gaphor.diagram.components.component import ComponentItem
|
||||
from gaphor.diagram.components.connector import ConnectorItem
|
||||
from gaphor.diagram.connectors import AbstractConnect, IConnect
|
||||
from gaphor.diagram.connectors import BaseConnector, Connector
|
||||
|
||||
|
||||
class ConnectorConnectBase(AbstractConnect):
|
||||
@Connector.register(ComponentItem, ConnectorItem)
|
||||
@Connector.register(InterfaceItem, ConnectorItem)
|
||||
class ConnectorConnectBase(BaseConnector):
|
||||
|
||||
element: Union[ComponentItem, InterfaceItem]
|
||||
line: ConnectorItem
|
||||
|
||||
def _get_interfaces(self, c1, c2):
|
||||
"""
|
||||
Return list of common interfaces provided by first component and
|
||||
@ -184,17 +191,3 @@ class ConnectorConnectBase(AbstractConnect):
|
||||
c = self.get_component(line)
|
||||
self.drop_uml(line, c)
|
||||
iface.request_update()
|
||||
|
||||
|
||||
@IConnect.register(ComponentItem, ConnectorItem)
|
||||
class ComponentConnectorConnect(ConnectorConnectBase):
|
||||
"""Connection of connector item to a component."""
|
||||
|
||||
|
||||
@IConnect.register(InterfaceItem, ConnectorItem)
|
||||
class InterfaceConnectorConnect(ConnectorConnectBase):
|
||||
"""Connect connector to an interface to maintain assembly connection.
|
||||
|
||||
See also `AbstractConnect` class for exception of interface item
|
||||
connections.
|
||||
"""
|
||||
|
@ -9,9 +9,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, List, Optional, Type, TypeVar, Union
|
||||
|
||||
from gaphas.canvas import Connection
|
||||
from gaphas.canvas import Canvas, Connection
|
||||
from gaphas.connector import Handle, Port
|
||||
from generic.multidispatch import FunctionDispatcher, multidispatch
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.presentation import ElementPresentation, LinePresentation
|
||||
@ -20,52 +21,21 @@ from gaphor.UML.properties import association, redefine, relation
|
||||
T = TypeVar("T", bound=UML.Element)
|
||||
|
||||
|
||||
class ConnectBase:
|
||||
"""
|
||||
This interface is used by the HandleTool to allow connecting
|
||||
lines to element items. For each specific case (Element, Line) an
|
||||
adapter could be written.
|
||||
"""
|
||||
|
||||
def __init__(self, item: ElementPresentation, line_item: LinePresentation):
|
||||
self.item = item
|
||||
self.line_item = line_item
|
||||
class ConnectorProtocol(Protocol):
|
||||
def __init__(self, element: object, line: object,) -> None:
|
||||
...
|
||||
|
||||
def allow(self, handle: Handle, port: Port) -> bool:
|
||||
"""
|
||||
Determine if a connection is allowed.
|
||||
|
||||
Do some extra checks to see if the items actually can be connected.
|
||||
"""
|
||||
return False
|
||||
...
|
||||
|
||||
def connect(self, handle: Handle, port: Port) -> bool:
|
||||
"""
|
||||
Connect a line's handle to element.
|
||||
|
||||
Note that at the moment of the connect, handle.connected_to may point
|
||||
to some other item. The implementor should do the disconnect of
|
||||
the other element themselves.
|
||||
"""
|
||||
raise NotImplementedError(f"No connector for {self.item} and {self.line_item}")
|
||||
...
|
||||
|
||||
def disconnect(self, handle: Handle) -> None:
|
||||
"""
|
||||
The true disconnect. Disconnect a handle.connected_to from an
|
||||
element. This requires that the relationship is also removed at
|
||||
model level.
|
||||
"""
|
||||
raise NotImplementedError(f"No connector for {self.item} and {self.line_item}")
|
||||
...
|
||||
|
||||
|
||||
# Work around issue https://github.com/python/mypy/issues/3135 (Class decorators are not type checked)
|
||||
# This definition, along with the the ignore below, seems to fix the behaviour for mypy at least.
|
||||
IConnect: FunctionDispatcher[Type[ConnectBase]] = multidispatch(object, object)(
|
||||
ConnectBase
|
||||
)
|
||||
|
||||
|
||||
class AbstractConnect(ConnectBase):
|
||||
class BaseConnector:
|
||||
"""
|
||||
Connection adapter for Gaphor diagram items.
|
||||
|
||||
@ -91,41 +61,29 @@ class AbstractConnect(ConnectBase):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
element: ElementPresentation[UML.Element],
|
||||
line: LinePresentation[UML.Element],
|
||||
element: UML.Presentation[UML.Element],
|
||||
line: UML.Presentation[UML.Element],
|
||||
) -> None:
|
||||
assert element.canvas == line.canvas
|
||||
assert element.canvas is line.canvas
|
||||
self.element = element
|
||||
self.line = line
|
||||
self.canvas = element.canvas
|
||||
self.canvas: Canvas = element.canvas
|
||||
|
||||
def get_connection(self, handle: Handle) -> Optional[Connection]:
|
||||
"""
|
||||
Get connection information
|
||||
"""
|
||||
assert self.canvas
|
||||
return self.canvas.get_connection(handle)
|
||||
|
||||
def get_connected(self, handle: Handle) -> Optional[UML.Presentation]:
|
||||
def get_connected(self, handle: Handle) -> Optional[UML.Presentation[UML.Element]]:
|
||||
"""
|
||||
Get item connected to a handle.
|
||||
"""
|
||||
assert self.canvas
|
||||
cinfo = self.canvas.get_connection(handle)
|
||||
if cinfo:
|
||||
return cinfo.connected # type: ignore[no-any-return] # noqa: F723
|
||||
return None
|
||||
|
||||
def get_connected_port(self, handle: Handle) -> Optional[Port]:
|
||||
"""
|
||||
Get port of item connected to connecting item via specified handle.
|
||||
"""
|
||||
assert self.canvas
|
||||
cinfo = self.canvas.get_connection(handle)
|
||||
if cinfo:
|
||||
return cinfo.port
|
||||
return None
|
||||
|
||||
def allow(self, handle: Handle, port: Port) -> bool:
|
||||
"""
|
||||
Determine if items can be connected.
|
||||
@ -152,7 +110,28 @@ class AbstractConnect(ConnectBase):
|
||||
"""Disconnect UML model level connections."""
|
||||
|
||||
|
||||
class UnaryRelationshipConnect(AbstractConnect):
|
||||
class NoConnector:
|
||||
def __init__(self, element, line,) -> None:
|
||||
pass
|
||||
|
||||
def allow(self, handle: Handle, port: Port) -> bool:
|
||||
return False
|
||||
|
||||
def connect(self, handle: Handle, port: Port) -> bool:
|
||||
return False
|
||||
|
||||
def disconnect(self, handle: Handle) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# Work around issue https://github.com/python/mypy/issues/3135 (Class decorators are not type checked)
|
||||
# This definition, along with the the ignore below, seems to fix the behaviour for mypy at least.
|
||||
Connector: FunctionDispatcher[Type[ConnectorProtocol]] = multidispatch(object, object)(
|
||||
NoConnector
|
||||
)
|
||||
|
||||
|
||||
class UnaryRelationshipConnect(BaseConnector):
|
||||
"""
|
||||
Base class for relationship connections, such as associations,
|
||||
dependencies and implementations.
|
||||
@ -164,6 +143,9 @@ class UnaryRelationshipConnect(AbstractConnect):
|
||||
on the canvas.
|
||||
"""
|
||||
|
||||
element: ElementPresentation[UML.Element]
|
||||
line: LinePresentation[UML.Element]
|
||||
|
||||
def relationship(
|
||||
self, required_type: Type[UML.Element], head: relation, tail: relation
|
||||
) -> Optional[UML.Element]:
|
||||
@ -270,8 +252,6 @@ class UnaryRelationshipConnect(AbstractConnect):
|
||||
Cause items connected to ``line`` to reconnect, allowing them to
|
||||
establish or destroy relationships at model level.
|
||||
"""
|
||||
assert self.canvas
|
||||
|
||||
line = self.line
|
||||
canvas = self.canvas
|
||||
solver = canvas.solver
|
||||
@ -281,7 +261,7 @@ class UnaryRelationshipConnect(AbstractConnect):
|
||||
for cinfo in connections or canvas.get_connections(connected=line):
|
||||
if line is cinfo.connected:
|
||||
continue
|
||||
adapter = IConnect(line, cinfo.connected)
|
||||
adapter = Connector(line, cinfo.connected)
|
||||
assert adapter, "No element to connect {} and {}".format(
|
||||
line, cinfo.connected
|
||||
)
|
||||
@ -295,8 +275,6 @@ class UnaryRelationshipConnect(AbstractConnect):
|
||||
Returns a list of (item, handle) pairs that were connected (this
|
||||
list can be used to connect items again with connect_connected_items()).
|
||||
"""
|
||||
assert self.canvas
|
||||
|
||||
line = self.line
|
||||
canvas = self.canvas
|
||||
solver = canvas.solver
|
||||
@ -305,7 +283,7 @@ class UnaryRelationshipConnect(AbstractConnect):
|
||||
solver.solve()
|
||||
connections = list(canvas.get_connections(connected=line))
|
||||
for cinfo in connections:
|
||||
adapter = IConnect(cinfo.item, cinfo.connected)
|
||||
adapter = Connector(cinfo.item, cinfo.connected)
|
||||
adapter.disconnect(cinfo.handle)
|
||||
return connections
|
||||
|
||||
|
@ -372,6 +372,16 @@ TOOLBOX_ACTIONS: Sequence[Tuple[str, Sequence[ToolDef]]] = (
|
||||
diagram.interactions.MessageItem
|
||||
),
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-execution-specification",
|
||||
gettext("Execution Specification"),
|
||||
"gaphor-execution-specification-symbolic",
|
||||
None,
|
||||
item_factory=PlacementTool.new_item_factory(
|
||||
diagram.interactions.ExecutionSpecificationItem
|
||||
),
|
||||
handle_index=0,
|
||||
),
|
||||
ToolDef(
|
||||
"toolbox-interaction",
|
||||
gettext("Interaction"),
|
||||
|
@ -9,19 +9,21 @@ Although Gaphas has quite a few useful tools, some tools need to be extended:
|
||||
|
||||
import logging
|
||||
|
||||
from gaphas.aspect import Connector, InMotion, ItemConnector
|
||||
from gaphas.aspect import Connector as ConnectorAspect
|
||||
from gaphas.aspect import InMotion as InMotionAspect
|
||||
from gaphas.aspect import ItemConnector
|
||||
from gaphas.guide import GuidedItemInMotion
|
||||
from gaphas.tool import ConnectHandleTool, HoverTool, ItemTool
|
||||
from gaphas.tool import PlacementTool as _PlacementTool
|
||||
from gaphas.tool import RubberbandTool, Tool, ToolChain
|
||||
from gi.repository import Gdk, Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
from gaphor.core import Transaction, transactional
|
||||
from gaphor.diagram.connectors import IConnect
|
||||
from gaphor.diagram.connectors import Connector
|
||||
from gaphor.diagram.event import DiagramItemPlaced
|
||||
from gaphor.diagram.grouping import Group
|
||||
from gaphor.diagram.inlineeditors import InlineEditor
|
||||
from gaphor.diagram.presentation import ElementPresentation, LinePresentation
|
||||
from gaphor.diagram.presentation import ElementPresentation, Presentation
|
||||
|
||||
# cursor to indicate grouping
|
||||
IN_CURSOR_TYPE = Gdk.CursorType.DIAMOND_CROSS
|
||||
@ -32,10 +34,10 @@ OUT_CURSOR_TYPE = Gdk.CursorType.CROSSHAIR
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@Connector.register(LinePresentation)
|
||||
@ConnectorAspect.register(Presentation)
|
||||
class DiagramItemConnector(ItemConnector):
|
||||
"""
|
||||
Handle Tool (acts on item handles) that uses the IConnect protocol
|
||||
Handle Tool (acts on item handles) that uses the Connector protocol
|
||||
to connect items to one-another.
|
||||
|
||||
It also adds handles to lines when a line is grabbed on the middle of
|
||||
@ -43,7 +45,7 @@ class DiagramItemConnector(ItemConnector):
|
||||
"""
|
||||
|
||||
def allow(self, sink):
|
||||
adapter = IConnect(sink.item, self.item)
|
||||
adapter = Connector(sink.item, self.item)
|
||||
return adapter and adapter.allow(self.handle, sink.port)
|
||||
|
||||
@transactional
|
||||
@ -65,7 +67,7 @@ class DiagramItemConnector(ItemConnector):
|
||||
elif cinfo:
|
||||
# first disconnect but disable disconnection handle as
|
||||
# reconnection is going to happen
|
||||
adapter = IConnect(sink.item, item)
|
||||
adapter = Connector(sink.item, item)
|
||||
try:
|
||||
connect = adapter.reconnect
|
||||
except AttributeError:
|
||||
@ -81,7 +83,7 @@ class DiagramItemConnector(ItemConnector):
|
||||
connect(handle, sink.port)
|
||||
else:
|
||||
# new connection
|
||||
adapter = IConnect(sink.item, item)
|
||||
adapter = Connector(sink.item, item)
|
||||
self.connect_handle(sink, callback=callback)
|
||||
adapter.connect(handle, sink.port)
|
||||
except Exception:
|
||||
@ -124,7 +126,7 @@ class DisconnectHandle:
|
||||
else:
|
||||
log.debug(f"Disconnecting {item}.{handle}")
|
||||
if cinfo:
|
||||
adapter = IConnect(cinfo.connected, item)
|
||||
adapter = Connector(cinfo.connected, item)
|
||||
adapter.disconnect(handle)
|
||||
|
||||
|
||||
@ -223,7 +225,7 @@ class PlacementTool(_PlacementTool):
|
||||
# mechanisms
|
||||
|
||||
# First make sure all matrices are updated:
|
||||
view.canvas.update_matrix(self.new_item)
|
||||
view.canvas.update_matrices([self.new_item])
|
||||
view.update_matrix(self.new_item)
|
||||
|
||||
vpos = event.x, event.y
|
||||
@ -290,7 +292,7 @@ class PlacementTool(_PlacementTool):
|
||||
return item
|
||||
|
||||
|
||||
@InMotion.register(ElementPresentation)
|
||||
@InMotionAspect.register(Presentation)
|
||||
class DropZoneInMotion(GuidedItemInMotion):
|
||||
def move(self, pos):
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@ CommentLine -- A line that connects a comment to another model element.
|
||||
|
||||
"""
|
||||
|
||||
from gaphor.diagram.connectors import IConnect
|
||||
from gaphor.diagram.connectors import Connector
|
||||
from gaphor.diagram.presentation import LinePresentation
|
||||
|
||||
|
||||
@ -18,6 +18,6 @@ class CommentLineItem(LinePresentation):
|
||||
c1 = canvas.get_connection(self.head)
|
||||
c2 = canvas.get_connection(self.tail)
|
||||
if c1 and c2:
|
||||
adapter = IConnect(c1.connected, self)
|
||||
adapter = Connector(c1.connected, self)
|
||||
adapter.disconnect(self.head)
|
||||
super().unlink()
|
||||
|
@ -3,9 +3,10 @@ Connect comments.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.connectors import AbstractConnect, IConnect
|
||||
from gaphor.diagram.connectors import BaseConnector, Connector
|
||||
from gaphor.diagram.general.comment import CommentItem
|
||||
from gaphor.diagram.general.commentline import CommentLineItem
|
||||
from gaphor.diagram.presentation import ElementPresentation, LinePresentation
|
||||
@ -13,11 +14,12 @@ from gaphor.diagram.presentation import ElementPresentation, LinePresentation
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@IConnect.register(CommentItem, CommentLineItem)
|
||||
@IConnect.register(ElementPresentation, CommentLineItem)
|
||||
class CommentLineElementConnect(AbstractConnect):
|
||||
@Connector.register(CommentItem, CommentLineItem)
|
||||
@Connector.register(ElementPresentation, CommentLineItem)
|
||||
class CommentLineElementConnect(BaseConnector):
|
||||
"""Connect a comment line to any element item."""
|
||||
|
||||
element: Union[CommentItem, ElementPresentation]
|
||||
line: CommentLineItem
|
||||
|
||||
def allow(self, handle, port):
|
||||
@ -88,6 +90,7 @@ class CommentLineElementConnect(AbstractConnect):
|
||||
if hct.subject and isinstance(oct.subject, UML.Comment):
|
||||
del oct.subject.annotatedElement[hct.subject]
|
||||
elif hct.subject and oct.subject:
|
||||
assert isinstance(hct.subject, UML.Comment)
|
||||
del hct.subject.annotatedElement[oct.subject]
|
||||
except ValueError:
|
||||
logger.debug(
|
||||
@ -97,10 +100,13 @@ class CommentLineElementConnect(AbstractConnect):
|
||||
super().disconnect(handle)
|
||||
|
||||
|
||||
@IConnect.register(LinePresentation, CommentLineItem)
|
||||
class CommentLineLineConnect(AbstractConnect):
|
||||
@Connector.register(LinePresentation, CommentLineItem)
|
||||
class CommentLineLineConnect(BaseConnector):
|
||||
"""Connect a comment line to any diagram line."""
|
||||
|
||||
element: LinePresentation
|
||||
line: CommentLineItem
|
||||
|
||||
def allow(self, handle, port):
|
||||
"""
|
||||
In addition to the normal check, both line ends may not be connected
|
||||
@ -156,12 +162,15 @@ class CommentLineLineConnect(AbstractConnect):
|
||||
and c2.subject in c1.subject.annotatedElement
|
||||
):
|
||||
del c1.subject.annotatedElement[c2.subject]
|
||||
elif c2.subject and c1.subject in c2.subject.annotatedElement:
|
||||
elif (
|
||||
isinstance(c2.subject, UML.Comment)
|
||||
and c1.subject in c2.subject.annotatedElement
|
||||
):
|
||||
del c2.subject.annotatedElement[c1.subject]
|
||||
super().disconnect(handle)
|
||||
|
||||
|
||||
@IConnect.register(CommentLineItem, LinePresentation)
|
||||
@Connector.register(CommentLineItem, LinePresentation)
|
||||
class InverseCommentLineLineConnect(CommentLineLineConnect):
|
||||
"""
|
||||
In case a line is disconnected that contains a comment-line,
|
||||
|
@ -1,3 +1,6 @@
|
||||
from gaphor.diagram.interactions.executionspecification import (
|
||||
ExecutionSpecificationItem,
|
||||
)
|
||||
from gaphor.diagram.interactions.interaction import InteractionItem
|
||||
from gaphor.diagram.interactions.lifeline import LifelineItem
|
||||
from gaphor.diagram.interactions.message import MessageItem
|
||||
@ -5,7 +8,7 @@ from gaphor.diagram.interactions.message import MessageItem
|
||||
|
||||
def _load():
|
||||
from gaphor.diagram.interactions import (
|
||||
messageconnect,
|
||||
interactionsconnect,
|
||||
interactionsgrouping,
|
||||
interactionspropertypages,
|
||||
)
|
||||
|
122
gaphor/diagram/interactions/executionspecification.py
Normal file
122
gaphor/diagram/interactions/executionspecification.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""
|
||||
An ExecutionSpecification is defined by a white recrange overlaying the lifeline
|
||||
|
||||
|
||||
|
||||
,----------.
|
||||
| lifeline |
|
||||
`----------'
|
||||
| --- Lifeline
|
||||
,+. -- ExecutionOccurrenceSpecification
|
||||
| | -- ExecutionSpecification
|
||||
`+' -- ExecutionOccurrentSpecification
|
||||
|
|
||||
|
||||
ExecutionOccurrenceSpecification.covered <--> Lifeline.coveredBy
|
||||
ExecutionOccurrenceSpecification.execution <--> ExecutionSpecification.execution
|
||||
|
||||
TODO:ExecutionSpecification is abstract. Should use either
|
||||
ActionExecutionSpecification or BehaviorExecutionSpecification.
|
||||
What's the difference?
|
||||
|
||||
Stick with BehaviorExecutionSpecification, since it has a [0..1] relation to
|
||||
behavior, whereas ActionExecutionSpecification has a [1] relation to action.
|
||||
"""
|
||||
import ast
|
||||
|
||||
from gaphas import Handle, Item
|
||||
from gaphas.connector import LinePort, Position
|
||||
from gaphas.geometry import Rectangle, distance_rectangle_point
|
||||
from gaphas.solver import WEAK
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.presentation import postload_connect
|
||||
from gaphor.diagram.shapes import Box, draw_border
|
||||
from gaphor.diagram.support import represents
|
||||
from gaphor.UML.modelfactory import stereotypes_str
|
||||
|
||||
|
||||
@represents(UML.ExecutionSpecification)
|
||||
class ExecutionSpecificationItem(UML.Presentation[UML.ExecutionSpecification], Item):
|
||||
"""
|
||||
Representation of interaction execution specification.
|
||||
"""
|
||||
|
||||
def __init__(self, id=None, model=None):
|
||||
super().__init__(id, model)
|
||||
self.bar_width = 12
|
||||
|
||||
ht, hb = Handle(), Handle()
|
||||
ht.connectable = True
|
||||
|
||||
# TODO: need better interface for this!
|
||||
self._handles.append(ht)
|
||||
self._handles.append(hb)
|
||||
|
||||
self.constraint(vertical=(ht.pos, hb.pos))
|
||||
|
||||
r = self.bar_width / 2
|
||||
nw = Position((-r, 0), strength=WEAK)
|
||||
ne = Position((r, 0), strength=WEAK)
|
||||
se = Position((r, 0), strength=WEAK)
|
||||
sw = Position((-r, 0), strength=WEAK)
|
||||
|
||||
self.constraint(horizontal=(sw, hb.pos))
|
||||
self.constraint(horizontal=(se, hb.pos))
|
||||
|
||||
self._ports.append(LinePort(nw, sw))
|
||||
self._ports.append(LinePort(ne, se))
|
||||
|
||||
self.shape = Box(style={"fill": "white"}, draw=draw_border)
|
||||
|
||||
@property
|
||||
def top(self):
|
||||
return self._handles[0]
|
||||
|
||||
@property
|
||||
def bottom(self):
|
||||
return self._handles[1]
|
||||
|
||||
def dimensions(self):
|
||||
d = self.bar_width
|
||||
pt, pb = (h.pos for h in self._handles)
|
||||
return Rectangle(pt.x - d / 2, pt.y, d, y1=pb.y)
|
||||
|
||||
def draw(self, context):
|
||||
self.shape.draw(context, self.dimensions())
|
||||
|
||||
def point(self, pos):
|
||||
return distance_rectangle_point(self.dimensions(), pos)
|
||||
|
||||
def save(self, save_func):
|
||||
def save_connection(name, handle):
|
||||
assert self.canvas
|
||||
c = self.canvas.get_connection(handle)
|
||||
if c:
|
||||
save_func(name, c.connected, reference=True)
|
||||
|
||||
points = [tuple(map(float, h.pos)) for h in self.handles()]
|
||||
|
||||
save_func("matrix", tuple(self.matrix))
|
||||
save_func("points", points)
|
||||
save_connection("head-connection", self.handles()[0])
|
||||
super().save(save_func)
|
||||
|
||||
def load(self, name, value):
|
||||
if name == "matrix":
|
||||
self.matrix = ast.literal_eval(value)
|
||||
elif name == "points":
|
||||
points = ast.literal_eval(value)
|
||||
for h, p in zip(self.handles(), points):
|
||||
h.pos = p
|
||||
elif name == "head-connection":
|
||||
self._load_head_connection = value
|
||||
else:
|
||||
super().load(name, value)
|
||||
|
||||
def postload(self):
|
||||
if hasattr(self, "_load_head_connection"):
|
||||
postload_connect(self, self.handles()[0], self._load_head_connection)
|
||||
del self._load_head_connection
|
||||
|
||||
super().postload()
|
307
gaphor/diagram/interactions/interactionsconnect.py
Normal file
307
gaphor/diagram/interactions/interactionsconnect.py
Normal file
@ -0,0 +1,307 @@
|
||||
"""Message item connection adapters."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.connectors import BaseConnector, Connector
|
||||
from gaphor.diagram.interactions.executionspecification import (
|
||||
ExecutionSpecificationItem,
|
||||
)
|
||||
from gaphor.diagram.interactions.lifeline import LifelineItem
|
||||
from gaphor.diagram.interactions.message import MessageItem
|
||||
from gaphor.diagram.presentation import ElementPresentation
|
||||
|
||||
|
||||
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[UML.Presentation[UML.Element]]:
|
||||
"""
|
||||
Get item connected to a handle.
|
||||
"""
|
||||
cinfo = item.canvas.get_connection(handle)
|
||||
if cinfo:
|
||||
return cinfo.connected # type: ignore[no-any-return] # noqa: F723
|
||||
return None
|
||||
|
||||
|
||||
def get_lifeline(item, handle):
|
||||
connected_item = get_connected(item, handle)
|
||||
if connected_item is None or isinstance(connected_item, LifelineItem):
|
||||
return connected_item
|
||||
return get_lifeline(connected_item, connected_item.handles()[0]) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def order_lifeline_covered_by(lifeline):
|
||||
canvas = lifeline.canvas
|
||||
|
||||
def y_and_occurence(connected):
|
||||
for conn in canvas.get_connections(connected=connected):
|
||||
m = canvas.get_matrix_i2c(conn.item)
|
||||
if isinstance(conn.item, ExecutionSpecificationItem):
|
||||
yield (
|
||||
m.transform_point(*conn.handle.pos)[1],
|
||||
conn.item.subject.start,
|
||||
)
|
||||
yield (
|
||||
m.transform_point(*conn.item.bottom.pos)[1],
|
||||
conn.item.subject.finish,
|
||||
)
|
||||
yield from y_and_occurence(conn.item)
|
||||
elif isinstance(conn.item, MessageItem):
|
||||
yield (
|
||||
m.transform_point(*conn.handle.pos)[1],
|
||||
conn.item.subject.sendEvent
|
||||
if conn.handle is conn.item.head
|
||||
else conn.item.subject.receiveEvent,
|
||||
)
|
||||
|
||||
keys = {o: y for y, o in y_and_occurence(lifeline)}
|
||||
lifeline.subject.coveredBy.order(keys.get)
|
||||
|
||||
|
||||
def connect_lifelines(line, send, received):
|
||||
"""
|
||||
Always create a new Message with two EventOccurrence instances.
|
||||
"""
|
||||
|
||||
def get_subject():
|
||||
if not line.subject:
|
||||
message = line.model.create(UML.Message)
|
||||
message.name = "call()"
|
||||
line.subject = message
|
||||
return line.subject
|
||||
|
||||
if send:
|
||||
message = get_subject()
|
||||
if not message.sendEvent:
|
||||
event = message.model.create(UML.MessageOccurrenceSpecification)
|
||||
event.sendMessage = message
|
||||
event.covered = send.subject
|
||||
order_lifeline_covered_by(send)
|
||||
|
||||
if received:
|
||||
message = get_subject()
|
||||
if not message.receiveEvent:
|
||||
event = message.model.create(UML.MessageOccurrenceSpecification)
|
||||
event.receiveMessage = message
|
||||
event.covered = received.subject
|
||||
order_lifeline_covered_by(received)
|
||||
|
||||
|
||||
def disconnect_lifelines(line, send, received):
|
||||
"""
|
||||
Disconnect lifeline and set appropriate kind of message item. If
|
||||
there are no lifelines connected on both ends, then remove the message
|
||||
from the data model.
|
||||
"""
|
||||
if not line.subject:
|
||||
return
|
||||
|
||||
if send:
|
||||
event = line.subject.receiveEvent
|
||||
if event:
|
||||
event.unlink()
|
||||
|
||||
if received:
|
||||
event = line.subject.sendEvent
|
||||
if event:
|
||||
event.unlink()
|
||||
|
||||
# one is disconnected and one is about to be disconnected,
|
||||
# so destroy the message
|
||||
if not send or not received:
|
||||
# Both ends are disconnected:
|
||||
message = line.subject
|
||||
del line.subject
|
||||
if not message.presentation:
|
||||
message.unlink()
|
||||
|
||||
|
||||
@Connector.register(LifelineItem, MessageItem)
|
||||
class MessageLifelineConnect(BaseConnector):
|
||||
"""Connect lifeline with a message.
|
||||
|
||||
A message can connect to both the lifeline's head (the rectangle)
|
||||
or the lifetime line. In case it's added to the head, the message
|
||||
is considered to be part of a communication diagram. If the message is
|
||||
added to a lifetime line, it's considered a sequence diagram.
|
||||
"""
|
||||
|
||||
element: LifelineItem
|
||||
line: MessageItem
|
||||
|
||||
def allow(self, handle, port):
|
||||
"""
|
||||
Glue to lifeline's head or lifetime. If lifeline's lifetime is
|
||||
visible then disallow connection to lifeline's head.
|
||||
"""
|
||||
element = self.element
|
||||
lifetime = element.lifetime
|
||||
line = self.line
|
||||
opposite = line.opposite(handle)
|
||||
|
||||
ol = self.get_connected(opposite)
|
||||
if isinstance(ol, LifelineItem):
|
||||
opposite_is_visible = ol.lifetime.visible
|
||||
# connect lifetimes if both are visible or both invisible
|
||||
return not (lifetime.visible ^ opposite_is_visible)
|
||||
|
||||
return not (lifetime.visible ^ (port is element.lifetime.port))
|
||||
|
||||
def connect(self, handle, port):
|
||||
line = self.line
|
||||
send = self.get_connected(line.head)
|
||||
received = self.get_connected(line.tail)
|
||||
connect_lifelines(line, send, received)
|
||||
|
||||
lifetime = self.element.lifetime
|
||||
# if connected to head, then make lifetime invisible
|
||||
if port is lifetime.port:
|
||||
lifetime.min_length = lifetime.MIN_LENGTH_VISIBLE
|
||||
else:
|
||||
lifetime.visible = False
|
||||
lifetime.connectable = False
|
||||
return True
|
||||
|
||||
def disconnect(self, handle):
|
||||
line = self.line
|
||||
send: Optional[UML.Presentation[UML.Element]] = get_connected(line, line.head)
|
||||
received = self.get_connected(line.tail)
|
||||
lifeline = self.element
|
||||
lifetime = lifeline.lifetime
|
||||
|
||||
# if a message is delete message, then disconnection causes
|
||||
# lifeline to be no longer destroyed (note that there can be
|
||||
# only one delete message connected to lifeline)
|
||||
if received and line.subject.messageSort == "deleteMessage":
|
||||
assert isinstance(received, LifelineItem)
|
||||
received.is_destroyed = False
|
||||
received.request_update()
|
||||
|
||||
disconnect_lifelines(line, send, received)
|
||||
|
||||
if len(list(self.canvas.get_connections(connected=lifeline))) == 1:
|
||||
# after disconnection count of connected items will be
|
||||
# zero, so allow connections to lifeline's lifetime
|
||||
lifetime.connectable = True
|
||||
lifetime.min_length = lifetime.MIN_LENGTH
|
||||
|
||||
|
||||
@Connector.register(ExecutionSpecificationItem, MessageItem)
|
||||
class ExecutionSpecificationMessageConnect(BaseConnector):
|
||||
|
||||
element: ExecutionSpecificationItem
|
||||
line: MessageItem
|
||||
|
||||
def connect(self, handle, _port):
|
||||
line = self.line
|
||||
send = get_lifeline(line, line.head)
|
||||
received = get_lifeline(line, line.tail)
|
||||
connect_lifelines(line, send, received)
|
||||
return True
|
||||
|
||||
def disconnect(self, handle):
|
||||
line = self.line
|
||||
send = get_lifeline(line, line.head)
|
||||
received = get_lifeline(line, line.tail)
|
||||
disconnect_lifelines(line, send, received)
|
||||
|
||||
|
||||
@Connector.register(LifelineItem, ExecutionSpecificationItem)
|
||||
class LifelineExecutionSpecificationConnect(BaseConnector):
|
||||
|
||||
element: LifelineItem
|
||||
line: ExecutionSpecificationItem
|
||||
|
||||
def allow(self, handle, port):
|
||||
lifetime = self.element.lifetime
|
||||
return lifetime.visible
|
||||
|
||||
def connect(self, handle, port):
|
||||
lifeline = self.element.subject
|
||||
exec_spec: UML.ExecutionSpecification = self.line.subject
|
||||
model = self.element.model
|
||||
if not exec_spec:
|
||||
exec_spec = model.create(UML.BehaviorExecutionSpecification)
|
||||
self.line.subject = exec_spec
|
||||
|
||||
start_occurence: UML.ExecutionOccurrenceSpecification = model.create(
|
||||
UML.ExecutionOccurrenceSpecification
|
||||
)
|
||||
start_occurence.covered = lifeline
|
||||
start_occurence.execution = exec_spec
|
||||
|
||||
finish_occurence: UML.ExecutionOccurrenceSpecification = model.create(
|
||||
UML.ExecutionOccurrenceSpecification
|
||||
)
|
||||
finish_occurence.covered = lifeline
|
||||
finish_occurence.execution = exec_spec
|
||||
|
||||
canvas = self.canvas
|
||||
if canvas.get_parent(self.line) is not self.element:
|
||||
reparent(canvas, self.line, self.element)
|
||||
|
||||
for cinfo in canvas.get_connections(connected=self.line):
|
||||
Connector(self.line, cinfo.item).connect(cinfo.handle, cinfo.port)
|
||||
return True
|
||||
|
||||
def disconnect(self, handle):
|
||||
exec_spec: Optional[UML.ExecutionSpecification] = self.line.subject
|
||||
del self.line.subject
|
||||
if exec_spec:
|
||||
exec_spec.unlink()
|
||||
|
||||
canvas = self.canvas
|
||||
|
||||
if canvas.get_parent(self.line) is self.element:
|
||||
new_parent = canvas.get_parent(self.element)
|
||||
reparent(canvas, self.line, new_parent)
|
||||
|
||||
for cinfo in canvas.get_connections(connected=self.line):
|
||||
Connector(self.line, cinfo.item).disconnect(cinfo.handle)
|
||||
|
||||
|
||||
@Connector.register(ExecutionSpecificationItem, ExecutionSpecificationItem)
|
||||
class ExecutionSpecificationExecutionSpecificationConnect(BaseConnector):
|
||||
|
||||
element: ExecutionSpecificationItem
|
||||
line: ExecutionSpecificationItem
|
||||
|
||||
def connect(self, handle, _port):
|
||||
parent_exec_spec = self.element.subject
|
||||
|
||||
if not parent_exec_spec:
|
||||
# Can connect child exec spec if parent is not connected
|
||||
return True
|
||||
|
||||
connected_item: Optional[UML.Presentation[UML.Element]]
|
||||
connected_item = self.get_connected(self.element.handles()[0])
|
||||
assert connected_item
|
||||
Connector(connected_item, self.line).connect(handle, None)
|
||||
|
||||
reparent(self.canvas, self.line, self.element)
|
||||
|
||||
return True
|
||||
|
||||
def disconnect(self, handle):
|
||||
exec_spec: Optional[UML.ExecutionSpecification] = self.line.subject
|
||||
del self.line.subject
|
||||
if exec_spec and not exec_spec.presentation:
|
||||
exec_spec.unlink()
|
||||
|
||||
for cinfo in self.canvas.get_connections(connected=self.line):
|
||||
Connector(self.line, cinfo.item).disconnect(cinfo.handle)
|
@ -2,6 +2,7 @@ from gi.repository import Gtk
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.core import gettext, transactional
|
||||
from gaphor.diagram.interactions.interactionsconnect import get_lifeline
|
||||
from gaphor.diagram.interactions.message import MessageItem
|
||||
from gaphor.diagram.propertypages import (
|
||||
EditableTreeModel,
|
||||
@ -45,10 +46,7 @@ class MessagePropertyPage(NamedItemPropertyPage):
|
||||
hbox = create_hbox_label(self, page, gettext("Message sort"))
|
||||
|
||||
sort_data = self.MESSAGE_SORT
|
||||
lifeline = None
|
||||
cinfo = item.canvas.get_connection(item.tail)
|
||||
if cinfo:
|
||||
lifeline = cinfo.connected
|
||||
lifeline = get_lifeline(item, item.tail)
|
||||
|
||||
# disallow connecting two delete messages to a lifeline
|
||||
if (
|
||||
@ -79,10 +77,7 @@ class MessagePropertyPage(NamedItemPropertyPage):
|
||||
|
||||
item = self.item
|
||||
subject = item.subject
|
||||
lifeline = None
|
||||
cinfo = item.canvas.get_connection(item.tail)
|
||||
if cinfo:
|
||||
lifeline = cinfo.connected
|
||||
lifeline = get_lifeline(item, item.tail)
|
||||
|
||||
#
|
||||
# allow only one delete message to connect to lifeline's lifetime
|
||||
|
@ -49,6 +49,7 @@ See also ``lifeline`` module documentation.
|
||||
from math import atan2, pi
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.interactions.lifeline import LifelineItem
|
||||
from gaphor.diagram.presentation import LinePresentation, Named
|
||||
from gaphor.diagram.shapes import Box, EditableText, Text
|
||||
from gaphor.diagram.text import middle_segment
|
||||
@ -256,8 +257,8 @@ class MessageItem(LinePresentation[UML.Message], Named):
|
||||
c1 = canvas.get_connection(self.head)
|
||||
c2 = canvas.get_connection(self.tail)
|
||||
return (
|
||||
c1
|
||||
isinstance(c1, LifelineItem)
|
||||
and not c1.connected.lifetime.visible
|
||||
or c2
|
||||
or isinstance(c2, LifelineItem)
|
||||
and not c2.connected.lifetime.visible
|
||||
)
|
||||
|
@ -1,136 +0,0 @@
|
||||
"""Message item connection adapters."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.connectors import AbstractConnect, IConnect
|
||||
from gaphor.diagram.interactions.lifeline import LifelineItem
|
||||
from gaphor.diagram.interactions.message import MessageItem
|
||||
|
||||
|
||||
@IConnect.register(LifelineItem, MessageItem)
|
||||
class MessageLifelineConnect(AbstractConnect):
|
||||
"""Connect lifeline with a message.
|
||||
|
||||
A message can connect to both the lifeline's head (the rectangle)
|
||||
or the lifetime line. In case it's added to the head, the message
|
||||
is considered to be part of a communication diagram. If the message is
|
||||
added to a lifetime line, it's considered a sequence diagram.
|
||||
"""
|
||||
|
||||
line: MessageItem
|
||||
|
||||
def connect_lifelines(self, line, send, received):
|
||||
"""
|
||||
Always create a new Message with two EventOccurrence instances.
|
||||
"""
|
||||
|
||||
def get_subject():
|
||||
if not line.subject:
|
||||
message = line.model.create(UML.Message)
|
||||
message.name = "call()"
|
||||
line.subject = message
|
||||
return line.subject
|
||||
|
||||
if send:
|
||||
message = get_subject()
|
||||
if not message.sendEvent:
|
||||
event = message.model.create(UML.MessageOccurrenceSpecification)
|
||||
event.sendMessage = message
|
||||
event.covered = send.subject
|
||||
|
||||
if received:
|
||||
message = get_subject()
|
||||
if not message.receiveEvent:
|
||||
event = message.model.create(UML.MessageOccurrenceSpecification)
|
||||
event.receiveMessage = message
|
||||
event.covered = received.subject
|
||||
|
||||
def disconnect_lifelines(self, line):
|
||||
"""
|
||||
Disconnect lifeline and set appropriate kind of message item. If
|
||||
there are no lifelines connected on both ends, then remove the message
|
||||
from the data model.
|
||||
"""
|
||||
send = self.get_connected(line.head)
|
||||
received = self.get_connected(line.tail)
|
||||
|
||||
if send:
|
||||
event = line.subject.receiveEvent
|
||||
if event:
|
||||
event.unlink()
|
||||
|
||||
if received:
|
||||
event = line.subject.sendEvent
|
||||
if event:
|
||||
event.unlink()
|
||||
|
||||
# one is disconnected and one is about to be disconnected,
|
||||
# so destroy the message
|
||||
if not send or not received:
|
||||
# Both ends are disconnected:
|
||||
message = line.subject
|
||||
del line.subject
|
||||
if not message.presentation:
|
||||
message.unlink()
|
||||
|
||||
def allow(self, handle, port):
|
||||
"""
|
||||
Glue to lifeline's head or lifetime. If lifeline's lifetime is
|
||||
visible then disallow connection to lifeline's head.
|
||||
"""
|
||||
element = self.element
|
||||
lifetime = element.lifetime
|
||||
line = self.line
|
||||
opposite = line.opposite(handle)
|
||||
|
||||
ol = self.get_connected(opposite)
|
||||
if ol:
|
||||
assert isinstance(ol, LifelineItem)
|
||||
opposite_is_visible = ol.lifetime.visible
|
||||
# connect lifetimes if both are visible or both invisible
|
||||
return not (lifetime.visible ^ opposite_is_visible)
|
||||
|
||||
return not (lifetime.visible ^ (port is element.lifetime.port))
|
||||
|
||||
def connect(self, handle, port):
|
||||
super().connect(handle, port)
|
||||
|
||||
line = self.line
|
||||
send = self.get_connected(line.head)
|
||||
received = self.get_connected(line.tail)
|
||||
self.connect_lifelines(line, send, received)
|
||||
|
||||
lifetime = self.element.lifetime
|
||||
# if connected to head, then make lifetime invisible
|
||||
if port is lifetime.port:
|
||||
lifetime.min_length = lifetime.MIN_LENGTH_VISIBLE
|
||||
else:
|
||||
lifetime.visible = False
|
||||
lifetime.connectable = False
|
||||
|
||||
def disconnect(self, handle):
|
||||
assert self.canvas
|
||||
|
||||
super().disconnect(handle)
|
||||
|
||||
line = self.line
|
||||
received = self.get_connected(line.tail)
|
||||
lifeline = self.element
|
||||
lifetime = lifeline.lifetime
|
||||
|
||||
# if a message is delete message, then disconnection causes
|
||||
# lifeline to be no longer destroyed (note that there can be
|
||||
# only one delete message connected to lifeline)
|
||||
if received and line.subject.messageSort == "deleteMessage":
|
||||
assert isinstance(received, LifelineItem)
|
||||
received.is_destroyed = False
|
||||
received.request_update()
|
||||
|
||||
self.disconnect_lifelines(line)
|
||||
|
||||
if len(list(self.canvas.get_connections(connected=lifeline))) == 1:
|
||||
# after disconnection count of connected items will be
|
||||
# zero, so allow connections to lifeline's lifetime
|
||||
lifetime.connectable = True
|
||||
lifetime.min_length = lifetime.MIN_LENGTH
|
2
gaphor/diagram/interactions/tests/conftest.py
Normal file
2
gaphor/diagram/interactions/tests/conftest.py
Normal file
@ -0,0 +1,2 @@
|
||||
import gaphor.diagram.diagramtools
|
||||
from gaphor.diagram.tests.fixtures import diagram, element_factory, loader, saver
|
236
gaphor/diagram/interactions/tests/test_executionspecification.py
Normal file
236
gaphor/diagram/interactions/tests/test_executionspecification.py
Normal file
@ -0,0 +1,236 @@
|
||||
import pytest
|
||||
from gaphas.canvas import Canvas, Context, instant_cairo_context
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.interactions.executionspecification import (
|
||||
ExecutionSpecificationItem,
|
||||
)
|
||||
from gaphor.diagram.interactions.lifeline import LifelineItem
|
||||
from gaphor.diagram.tests.fixtures import allow, connect, disconnect
|
||||
|
||||
|
||||
def test_draw_on_canvas():
|
||||
canvas = Canvas()
|
||||
exec_spec = ExecutionSpecificationItem()
|
||||
canvas.add(exec_spec)
|
||||
cr = instant_cairo_context()
|
||||
exec_spec.draw(Context(cairo=cr))
|
||||
|
||||
|
||||
def test_allow_execution_specification_to_lifeline(diagram):
|
||||
lifeline = diagram.create(LifelineItem)
|
||||
lifeline.lifetime.visible = True
|
||||
exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
|
||||
glued = allow(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
|
||||
|
||||
assert glued
|
||||
|
||||
|
||||
def test_connect_execution_specification_to_lifeline(diagram, element_factory):
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
lifeline.lifetime.visible = True
|
||||
exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
|
||||
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
|
||||
|
||||
assert exec_spec.subject
|
||||
assert lifeline.subject
|
||||
assert exec_spec.subject.start.covered is lifeline.subject
|
||||
assert (
|
||||
exec_spec.subject.executionOccurrenceSpecification[0].covered
|
||||
is lifeline.subject
|
||||
)
|
||||
|
||||
|
||||
def test_disconnect_execution_specification_from_lifeline(diagram, element_factory):
|
||||
def elements_of_kind(type):
|
||||
return element_factory.lselect(lambda e: e.isKindOf(type))
|
||||
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
lifeline.lifetime.visible = True
|
||||
exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
|
||||
|
||||
disconnect(exec_spec, exec_spec.handles()[0])
|
||||
|
||||
assert lifeline.subject
|
||||
assert exec_spec.subject is None
|
||||
assert exec_spec.canvas
|
||||
assert elements_of_kind(UML.ExecutionSpecification) == []
|
||||
assert elements_of_kind(UML.ExecutionOccurrenceSpecification) == []
|
||||
|
||||
|
||||
def test_allow_execution_specification_to_execution_specification(diagram):
|
||||
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
child_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
|
||||
glued = allow(
|
||||
parent_exec_spec,
|
||||
parent_exec_spec.handles()[0],
|
||||
child_exec_spec,
|
||||
child_exec_spec.ports()[0],
|
||||
)
|
||||
|
||||
assert glued
|
||||
|
||||
|
||||
def test_connect_execution_specification_to_execution_specification(
|
||||
diagram, element_factory
|
||||
):
|
||||
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
child_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
|
||||
connect(
|
||||
child_exec_spec,
|
||||
child_exec_spec.handles()[0],
|
||||
parent_exec_spec,
|
||||
parent_exec_spec.ports()[0],
|
||||
)
|
||||
|
||||
assert not parent_exec_spec.subject
|
||||
assert not child_exec_spec.subject
|
||||
|
||||
|
||||
def test_connect_execution_specification_to_execution_specification_with_lifeline(
|
||||
diagram, element_factory
|
||||
):
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
lifeline.lifetime.visible = True
|
||||
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
child_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
connect(
|
||||
parent_exec_spec,
|
||||
parent_exec_spec.handles()[0],
|
||||
lifeline,
|
||||
lifeline.lifetime.port,
|
||||
)
|
||||
|
||||
connect(
|
||||
child_exec_spec,
|
||||
child_exec_spec.handles()[0],
|
||||
parent_exec_spec,
|
||||
parent_exec_spec.ports()[0],
|
||||
)
|
||||
|
||||
assert child_exec_spec.subject
|
||||
assert lifeline.subject
|
||||
assert child_exec_spec.subject.start.covered is lifeline.subject
|
||||
assert (
|
||||
child_exec_spec.subject.executionOccurrenceSpecification[0].covered
|
||||
is lifeline.subject
|
||||
)
|
||||
|
||||
|
||||
def test_connect_execution_specification_with_execution_specification_to_lifeline(
|
||||
diagram, element_factory
|
||||
):
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
lifeline.lifetime.visible = True
|
||||
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
child_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
connect(
|
||||
child_exec_spec,
|
||||
child_exec_spec.handles()[0],
|
||||
parent_exec_spec,
|
||||
parent_exec_spec.ports()[0],
|
||||
)
|
||||
|
||||
connect(
|
||||
parent_exec_spec,
|
||||
parent_exec_spec.handles()[0],
|
||||
lifeline,
|
||||
lifeline.lifetime.port,
|
||||
)
|
||||
|
||||
assert parent_exec_spec.subject
|
||||
assert child_exec_spec.subject
|
||||
assert lifeline.subject
|
||||
assert parent_exec_spec.subject.start.covered is lifeline.subject
|
||||
assert child_exec_spec.subject.start.covered is lifeline.subject
|
||||
assert (
|
||||
child_exec_spec.subject.executionOccurrenceSpecification[0].covered
|
||||
is lifeline.subject
|
||||
)
|
||||
|
||||
|
||||
def test_disconnect_execution_specification_with_execution_specification_from_lifeline(
|
||||
diagram, element_factory
|
||||
):
|
||||
def elements_of_kind(type):
|
||||
return element_factory.lselect(lambda e: e.isKindOf(type))
|
||||
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
lifeline.lifetime.visible = True
|
||||
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
child_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
grand_child_exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
connect(
|
||||
parent_exec_spec,
|
||||
parent_exec_spec.handles()[0],
|
||||
lifeline,
|
||||
lifeline.lifetime.port,
|
||||
)
|
||||
connect(
|
||||
child_exec_spec,
|
||||
child_exec_spec.handles()[0],
|
||||
parent_exec_spec,
|
||||
parent_exec_spec.ports()[0],
|
||||
)
|
||||
connect(
|
||||
grand_child_exec_spec,
|
||||
grand_child_exec_spec.handles()[0],
|
||||
child_exec_spec,
|
||||
child_exec_spec.ports()[0],
|
||||
)
|
||||
|
||||
disconnect(parent_exec_spec, parent_exec_spec.handles()[0])
|
||||
|
||||
assert lifeline.subject
|
||||
assert parent_exec_spec.subject is None
|
||||
assert child_exec_spec.subject is None
|
||||
assert grand_child_exec_spec.subject is None
|
||||
assert elements_of_kind(UML.ExecutionSpecification) == []
|
||||
assert elements_of_kind(UML.ExecutionOccurrenceSpecification) == []
|
||||
|
||||
|
||||
def test_save_and_load(diagram, element_factory, saver, loader):
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
lifeline.lifetime.visible = True
|
||||
exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
|
||||
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
|
||||
|
||||
diagram.canvas.update_now()
|
||||
|
||||
saved_data = saver()
|
||||
|
||||
loader(saved_data)
|
||||
|
||||
exec_specs = element_factory.lselect(
|
||||
lambda e: e.isKindOf(UML.ExecutionSpecification)
|
||||
)
|
||||
loaded_exec_spec = exec_specs[0].presentation[0]
|
||||
|
||||
assert len(exec_specs) == 1
|
||||
assert (
|
||||
len(
|
||||
element_factory.lselect(
|
||||
lambda e: e.isKindOf(UML.ExecutionOccurrenceSpecification)
|
||||
)
|
||||
)
|
||||
== 2
|
||||
)
|
||||
assert loaded_exec_spec.canvas.get_connection(loaded_exec_spec.handles()[0])
|
@ -4,18 +4,14 @@ Test messages.
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.interactions.message import MessageItem
|
||||
from gaphor.tests.testcase import TestCase
|
||||
|
||||
|
||||
class MessageTestCase(TestCase):
|
||||
def test_message_persistence(self):
|
||||
"""Test message saving/loading
|
||||
"""
|
||||
self.create(MessageItem, UML.Message)
|
||||
def test_message_persistence(diagram, element_factory, saver, loader):
|
||||
diagram.create(MessageItem, subject=element_factory.create(UML.Message))
|
||||
|
||||
data = self.save()
|
||||
self.load(data)
|
||||
data = saver()
|
||||
loader(data)
|
||||
new_diagram = next(element_factory.select(lambda e: isinstance(e, UML.Diagram)))
|
||||
item = new_diagram.canvas.select(lambda e: isinstance(e, MessageItem))[0]
|
||||
|
||||
item = self.diagram.canvas.select(lambda e: isinstance(e, MessageItem))[0]
|
||||
|
||||
assert item
|
||||
assert item
|
||||
|
@ -3,239 +3,299 @@ Message connection adapter tests.
|
||||
"""
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.interactions.executionspecification import (
|
||||
ExecutionSpecificationItem,
|
||||
)
|
||||
from gaphor.diagram.interactions.lifeline import LifelineItem
|
||||
from gaphor.diagram.interactions.message import MessageItem
|
||||
from gaphor.tests import TestCase
|
||||
from gaphor.diagram.tests.fixtures import allow, connect, disconnect
|
||||
|
||||
|
||||
class BasicMessageConnectionsTestCase(TestCase):
|
||||
def test_head_glue(self):
|
||||
"""Test message head glue
|
||||
"""
|
||||
ll = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
def test_head_glue(diagram):
|
||||
"""Test message head glue
|
||||
"""
|
||||
ll = diagram.create(LifelineItem)
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
# get head port
|
||||
port = ll.ports()[0]
|
||||
glued = self.allow(msg, msg.head, ll, port)
|
||||
assert glued
|
||||
|
||||
def test_invisible_lifetime_glue(self):
|
||||
"""Test message to invisible lifetime glue
|
||||
"""
|
||||
ll = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
|
||||
glued = self.allow(msg, msg.head, ll, ll.lifetime.port)
|
||||
|
||||
assert not ll.lifetime.visible
|
||||
assert not glued
|
||||
|
||||
def test_visible_lifetime_glue(self):
|
||||
"""Test message to visible lifetime glue
|
||||
"""
|
||||
ll = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
|
||||
ll.lifetime.visible = True
|
||||
|
||||
glued = self.allow(msg, msg.head, ll, ll.lifetime.port)
|
||||
assert glued
|
||||
|
||||
def test_lost_message_connection(self):
|
||||
"""Test lost message connection
|
||||
"""
|
||||
ll = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
|
||||
self.connect(msg, msg.head, ll)
|
||||
|
||||
# If one side is connected a "lost" message is created
|
||||
self.assertTrue(msg.subject is not None)
|
||||
assert msg.subject.messageKind == "lost"
|
||||
|
||||
messages = self.kindof(UML.Message)
|
||||
occurrences = self.kindof(UML.MessageOccurrenceSpecification)
|
||||
|
||||
assert 1 == len(messages)
|
||||
assert 1 == len(occurrences)
|
||||
assert messages[0] is msg.subject
|
||||
assert occurrences[0] is msg.subject.sendEvent
|
||||
|
||||
def test_found_message_connection(self):
|
||||
"""Test found message connection
|
||||
"""
|
||||
ll = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
|
||||
self.connect(msg, msg.tail, ll)
|
||||
|
||||
# If one side is connected a "found" message is created
|
||||
self.assertTrue(msg.subject is not None)
|
||||
assert msg.subject.messageKind == "found"
|
||||
|
||||
messages = self.kindof(UML.Message)
|
||||
occurrences = self.kindof(UML.MessageOccurrenceSpecification)
|
||||
|
||||
assert 1 == len(messages)
|
||||
assert 1 == len(occurrences)
|
||||
assert messages[0] is msg.subject
|
||||
assert occurrences[0] is msg.subject.receiveEvent
|
||||
|
||||
def test_complete_message_connection(self):
|
||||
"""Test complete message connection
|
||||
"""
|
||||
ll1 = self.create(LifelineItem)
|
||||
ll2 = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
|
||||
self.connect(msg, msg.head, ll1)
|
||||
self.connect(msg, msg.tail, ll2)
|
||||
|
||||
# two sides are connected - "complete" message is created
|
||||
self.assertTrue(msg.subject is not None)
|
||||
assert msg.subject.messageKind == "complete"
|
||||
|
||||
messages = self.kindof(UML.Message)
|
||||
occurences = self.kindof(UML.MessageOccurrenceSpecification)
|
||||
|
||||
assert 1 == len(messages)
|
||||
assert 2 == len(occurences)
|
||||
assert messages[0] is msg.subject
|
||||
assert msg.subject.sendEvent in occurences, f"{occurences}"
|
||||
assert msg.subject.receiveEvent in occurences, f"{occurences}"
|
||||
|
||||
def test_lifetime_connection(self):
|
||||
"""Test messages' lifetimes connection
|
||||
"""
|
||||
msg = self.create(MessageItem)
|
||||
ll1 = self.create(LifelineItem)
|
||||
ll2 = self.create(LifelineItem)
|
||||
|
||||
# make lifelines to be in sequence diagram mode
|
||||
ll1.lifetime.visible = True
|
||||
ll2.lifetime.visible = True
|
||||
assert ll1.lifetime.visible and ll2.lifetime.visible
|
||||
|
||||
# connect lifetimes with messages message to lifeline's head
|
||||
self.connect(msg, msg.head, ll1, ll1.lifetime.port)
|
||||
self.connect(msg, msg.tail, ll2, ll2.lifetime.port)
|
||||
|
||||
assert msg.subject is not None
|
||||
assert msg.subject.messageKind == "complete"
|
||||
|
||||
def test_disconnection(self):
|
||||
"""Test message disconnection
|
||||
"""
|
||||
ll1 = self.create(LifelineItem)
|
||||
ll2 = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
|
||||
self.connect(msg, msg.head, ll1)
|
||||
self.connect(msg, msg.tail, ll2)
|
||||
|
||||
# one side disconnection
|
||||
self.disconnect(msg, msg.head)
|
||||
assert msg.subject is not None, f"{msg.subject}"
|
||||
|
||||
# 2nd side disconnection
|
||||
self.disconnect(msg, msg.tail)
|
||||
assert msg.subject is None, f"{msg.subject}"
|
||||
|
||||
def test_lifetime_connectivity_on_head(self):
|
||||
"""Test lifeline's lifetime connectivity change on head connection
|
||||
"""
|
||||
ll = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
|
||||
# connect message to lifeline's head, lifeline's lifetime
|
||||
# visibility and connectivity should change
|
||||
self.connect(msg, msg.head, ll)
|
||||
assert not ll.lifetime.visible
|
||||
assert not ll.lifetime.connectable
|
||||
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
|
||||
|
||||
# ... and disconnection
|
||||
self.disconnect(msg, msg.head)
|
||||
assert ll.lifetime.connectable
|
||||
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
|
||||
|
||||
def test_lifetime_connectivity_on_lifetime(self):
|
||||
"""Test lifeline's lifetime connectivity change on lifetime connection
|
||||
"""
|
||||
ll = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
|
||||
ll.lifetime.visible = True
|
||||
|
||||
# connect message to lifeline's lifetime, lifeline's lifetime
|
||||
# visibility and connectivity should be unchanged
|
||||
self.connect(msg, msg.head, ll, ll.lifetime.port)
|
||||
assert ll.lifetime.connectable
|
||||
assert ll.lifetime.MIN_LENGTH_VISIBLE == ll.lifetime.min_length
|
||||
|
||||
# ... and disconnection
|
||||
self.disconnect(msg, msg.head)
|
||||
assert ll.lifetime.connectable
|
||||
assert ll.lifetime.visible
|
||||
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
|
||||
# get head port
|
||||
port = ll.ports()[0]
|
||||
glued = allow(msg, msg.head, ll, port)
|
||||
assert glued
|
||||
|
||||
|
||||
class DiagramModeMessageConnectionTestCase(TestCase):
|
||||
def test_message_glue_cd(self):
|
||||
"""Test gluing message on communication diagram."""
|
||||
def test_invisible_lifetime_glue(diagram):
|
||||
"""Test message to invisible lifetime glue
|
||||
"""
|
||||
ll = diagram.create(LifelineItem)
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
lifeline1 = self.create(LifelineItem)
|
||||
lifeline2 = self.create(LifelineItem)
|
||||
message = self.create(MessageItem)
|
||||
glued = allow(msg, msg.head, ll, ll.lifetime.port)
|
||||
|
||||
# make second lifeline to be in sequence diagram mode
|
||||
lifeline2.lifetime.visible = True
|
||||
assert not ll.lifetime.visible
|
||||
assert not glued
|
||||
|
||||
# connect head of message to lifeline's head
|
||||
self.connect(message, message.head, lifeline1)
|
||||
|
||||
glued = self.allow(message, message.tail, lifeline2, lifeline2.lifetime.port)
|
||||
# no connection possible as 2nd lifeline is in sequence diagram
|
||||
# mode
|
||||
self.assertFalse(glued)
|
||||
def test_visible_lifetime_glue(diagram):
|
||||
"""Test message to visible lifetime glue
|
||||
"""
|
||||
ll = diagram.create(LifelineItem)
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
def test_message_glue_sd(self):
|
||||
"""Test gluing message on sequence diagram."""
|
||||
ll.lifetime.visible = True
|
||||
|
||||
msg = self.create(MessageItem)
|
||||
ll1 = self.create(LifelineItem)
|
||||
ll2 = self.create(LifelineItem)
|
||||
glued = allow(msg, msg.head, ll, ll.lifetime.port)
|
||||
assert glued
|
||||
|
||||
# 1st lifeline - communication diagram
|
||||
# 2nd lifeline - sequence diagram
|
||||
ll2.lifetime.visible = True
|
||||
|
||||
# connect lifetime of message to lifeline's lifetime
|
||||
self.connect(msg, msg.head, ll1, ll1.lifetime.port)
|
||||
def test_lost_message_connection(diagram, element_factory):
|
||||
"""Test lost message connection
|
||||
"""
|
||||
ll = diagram.create(LifelineItem)
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
glued = self.allow(msg, msg.tail, ll2)
|
||||
# no connection possible as 2nd lifeline is in communication
|
||||
# diagram mode
|
||||
self.assertFalse(glued)
|
||||
connect(msg, msg.head, ll)
|
||||
|
||||
def test_messages_disconnect_cd(self):
|
||||
"""Test disconnecting messages on communication diagram
|
||||
"""
|
||||
ll1 = self.create(LifelineItem)
|
||||
ll2 = self.create(LifelineItem)
|
||||
msg = self.create(MessageItem)
|
||||
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
|
||||
occurrences = element_factory.lselect(
|
||||
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
|
||||
)
|
||||
|
||||
self.connect(msg, msg.head, ll1)
|
||||
self.connect(msg, msg.tail, ll2)
|
||||
# If one side is connected a "lost" message is created
|
||||
assert msg.subject is not None
|
||||
assert msg.subject.messageKind == "lost"
|
||||
|
||||
subject = msg.subject
|
||||
assert 1 == len(messages)
|
||||
assert 1 == len(occurrences)
|
||||
assert messages[0] is msg.subject
|
||||
assert occurrences[0] is msg.subject.sendEvent
|
||||
|
||||
assert subject.sendEvent and subject.receiveEvent
|
||||
|
||||
messages = list(self.kindof(UML.Message))
|
||||
occurrences = set(self.kindof(UML.MessageOccurrenceSpecification))
|
||||
def test_found_message_connection(diagram, element_factory):
|
||||
"""Test found message connection
|
||||
"""
|
||||
ll = diagram.create(LifelineItem)
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
# verify integrity of messages
|
||||
self.assertEqual(1, len(messages))
|
||||
assert 2 == len(occurrences)
|
||||
connect(msg, msg.tail, ll)
|
||||
|
||||
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
|
||||
occurrences = element_factory.lselect(
|
||||
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
|
||||
)
|
||||
|
||||
# If one side is connected a "found" message is created
|
||||
assert msg.subject is not None
|
||||
assert msg.subject.messageKind == "found"
|
||||
|
||||
assert 1 == len(messages)
|
||||
assert 1 == len(occurrences)
|
||||
assert messages[0] is msg.subject
|
||||
assert occurrences[0] is msg.subject.receiveEvent
|
||||
|
||||
|
||||
def test_complete_message_connection(diagram, element_factory):
|
||||
"""Test complete message connection
|
||||
"""
|
||||
ll1 = diagram.create(LifelineItem)
|
||||
ll2 = diagram.create(LifelineItem)
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
connect(msg, msg.head, ll1)
|
||||
connect(msg, msg.tail, ll2)
|
||||
|
||||
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
|
||||
occurrences = element_factory.lselect(
|
||||
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
|
||||
)
|
||||
|
||||
# two sides are connected - "complete" message is created
|
||||
assert msg.subject is not None
|
||||
assert msg.subject.messageKind == "complete"
|
||||
|
||||
assert 1 == len(messages)
|
||||
assert 2 == len(occurrences)
|
||||
assert messages[0] is msg.subject
|
||||
assert msg.subject.sendEvent in occurrences, f"{occurrences}"
|
||||
assert msg.subject.receiveEvent in occurrences, f"{occurrences}"
|
||||
|
||||
|
||||
def test_lifetime_connection(diagram):
|
||||
"""Test messages' lifetimes connection
|
||||
"""
|
||||
msg = diagram.create(MessageItem)
|
||||
ll1 = diagram.create(LifelineItem)
|
||||
ll2 = diagram.create(LifelineItem)
|
||||
|
||||
# make lifelines to be in sequence diagram mode
|
||||
ll1.lifetime.visible = True
|
||||
ll2.lifetime.visible = True
|
||||
assert ll1.lifetime.visible and ll2.lifetime.visible
|
||||
|
||||
# connect lifetimes with messages message to lifeline's head
|
||||
connect(msg, msg.head, ll1, ll1.lifetime.port)
|
||||
connect(msg, msg.tail, ll2, ll2.lifetime.port)
|
||||
|
||||
assert msg.subject is not None
|
||||
assert msg.subject.messageKind == "complete"
|
||||
|
||||
|
||||
def test_disconnection(diagram):
|
||||
"""Test message disconnection
|
||||
"""
|
||||
ll1 = diagram.create(LifelineItem)
|
||||
ll2 = diagram.create(LifelineItem)
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
connect(msg, msg.head, ll1)
|
||||
connect(msg, msg.tail, ll2)
|
||||
|
||||
# one side disconnection
|
||||
disconnect(msg, msg.head)
|
||||
assert msg.subject is not None, f"{msg.subject}"
|
||||
|
||||
# 2nd side disconnection
|
||||
disconnect(msg, msg.tail)
|
||||
assert msg.subject is None, f"{msg.subject}"
|
||||
|
||||
|
||||
def test_lifetime_connectivity_on_head(diagram, element_factory):
|
||||
"""Test lifeline's lifetime connectivity change on head connection
|
||||
"""
|
||||
ll = diagram.create(LifelineItem, subject=element_factory.create(UML.Lifeline))
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
# connect message to lifeline's head, lifeline's lifetime
|
||||
# visibility and connectivity should change
|
||||
connect(msg, msg.head, ll)
|
||||
assert not ll.lifetime.visible
|
||||
assert not ll.lifetime.connectable
|
||||
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
|
||||
|
||||
# ... and disconnection
|
||||
disconnect(msg, msg.head)
|
||||
assert ll.lifetime.connectable
|
||||
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
|
||||
|
||||
|
||||
def test_lifetime_connectivity_on_lifetime(diagram, element_factory):
|
||||
"""Test lifeline's lifetime connectivity change on lifetime connection
|
||||
"""
|
||||
ll = diagram.create(LifelineItem, subject=element_factory.create(UML.Lifeline))
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
ll.lifetime.visible = True
|
||||
|
||||
# connect message to lifeline's lifetime, lifeline's lifetime
|
||||
# visibility and connectivity should be unchanged
|
||||
connect(msg, msg.head, ll, ll.lifetime.port)
|
||||
assert ll.lifetime.connectable
|
||||
assert ll.lifetime.MIN_LENGTH_VISIBLE == ll.lifetime.min_length
|
||||
|
||||
# ... and disconnection
|
||||
disconnect(msg, msg.head)
|
||||
assert ll.lifetime.connectable
|
||||
assert ll.lifetime.visible
|
||||
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
|
||||
|
||||
|
||||
def test_message_glue_cd(diagram):
|
||||
"""Test gluing message on communication diagram."""
|
||||
|
||||
lifeline1 = diagram.create(LifelineItem)
|
||||
lifeline2 = diagram.create(LifelineItem)
|
||||
message = diagram.create(MessageItem)
|
||||
|
||||
# make second lifeline to be in sequence diagram mode
|
||||
lifeline2.lifetime.visible = True
|
||||
|
||||
# connect head of message to lifeline's head
|
||||
connect(message, message.head, lifeline1)
|
||||
|
||||
glued = allow(message, message.tail, lifeline2, lifeline2.lifetime.port)
|
||||
# no connection possible as 2nd lifeline is in sequence diagram
|
||||
# mode
|
||||
assert not glued
|
||||
|
||||
|
||||
def test_message_glue_sd(diagram):
|
||||
"""Test gluing message on sequence diagram."""
|
||||
|
||||
msg = diagram.create(MessageItem)
|
||||
ll1 = diagram.create(LifelineItem)
|
||||
ll2 = diagram.create(LifelineItem)
|
||||
|
||||
# 1st lifeline - communication diagram
|
||||
# 2nd lifeline - sequence diagram
|
||||
ll2.lifetime.visible = True
|
||||
|
||||
# connect lifetime of message to lifeline's lifetime
|
||||
connect(msg, msg.head, ll1, ll1.lifetime.port)
|
||||
|
||||
glued = allow(msg, msg.tail, ll2)
|
||||
# no connection possible as 2nd lifeline is in communication
|
||||
# diagram mode
|
||||
assert not glued
|
||||
|
||||
|
||||
def test_messages_disconnect_cd(diagram, element_factory):
|
||||
"""Test disconnecting messages on communication diagram
|
||||
"""
|
||||
ll1 = diagram.create(LifelineItem)
|
||||
ll2 = diagram.create(LifelineItem)
|
||||
msg = diagram.create(MessageItem)
|
||||
|
||||
connect(msg, msg.head, ll1)
|
||||
connect(msg, msg.tail, ll2)
|
||||
|
||||
subject = msg.subject
|
||||
|
||||
assert subject.sendEvent
|
||||
assert subject.receiveEvent
|
||||
|
||||
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
|
||||
occurrences = element_factory.lselect(
|
||||
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
|
||||
)
|
||||
|
||||
# verify integrity of messages
|
||||
assert 1 == len(messages)
|
||||
assert 2 == len(occurrences)
|
||||
|
||||
|
||||
def test_message_connect_to_execution_specification(diagram, element_factory):
|
||||
"""Test gluing message on sequence diagram."""
|
||||
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
message = diagram.create(MessageItem)
|
||||
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
|
||||
|
||||
connect(message, message.head, exec_spec, exec_spec.ports()[0])
|
||||
|
||||
assert message.subject
|
||||
assert message.subject.sendEvent.covered is lifeline.subject
|
||||
|
||||
|
||||
def test_message_disconnect_from_execution_specification(diagram, element_factory):
|
||||
"""Test gluing message on sequence diagram."""
|
||||
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
message = diagram.create(MessageItem)
|
||||
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
|
||||
connect(message, message.head, exec_spec, exec_spec.ports()[0])
|
||||
|
||||
disconnect(message, message.head)
|
||||
|
||||
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
|
||||
occurrences = element_factory.lselect(
|
||||
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
|
||||
)
|
||||
|
||||
assert not message.subject
|
||||
assert not len(messages)
|
||||
assert not len(occurrences)
|
||||
|
46
gaphor/diagram/interactions/tests/test_ordering.py
Normal file
46
gaphor/diagram/interactions/tests/test_ordering.py
Normal file
@ -0,0 +1,46 @@
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.interactions.executionspecification import (
|
||||
ExecutionSpecificationItem,
|
||||
)
|
||||
from gaphor.diagram.interactions.interactionsconnect import order_lifeline_covered_by
|
||||
from gaphor.diagram.interactions.lifeline import LifelineItem
|
||||
from gaphor.diagram.interactions.message import MessageItem
|
||||
from gaphor.diagram.tests.fixtures import connect
|
||||
|
||||
|
||||
def test_ordering(diagram, element_factory):
|
||||
lifeline = diagram.create(
|
||||
LifelineItem, subject=element_factory.create(UML.Lifeline)
|
||||
)
|
||||
lifeline.lifetime.visible = True
|
||||
lifeline.lifetime.bottom.pos.y = 1000
|
||||
lifetime_top = lifeline.lifetime.top.pos
|
||||
|
||||
exec_spec = diagram.create(ExecutionSpecificationItem)
|
||||
message1 = diagram.create(MessageItem)
|
||||
message2 = diagram.create(MessageItem)
|
||||
message3 = diagram.create(MessageItem)
|
||||
|
||||
message1.head.pos.y = lifetime_top.y + 300
|
||||
exec_spec.top.pos.y = lifetime_top.y + 400
|
||||
message2.head.pos.y = lifetime_top.y + 500
|
||||
exec_spec.bottom.pos.y = lifetime_top.y + 600
|
||||
message3.tail.pos.y = lifetime_top.y + 700
|
||||
|
||||
# Add in "random" order
|
||||
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
|
||||
connect(message3, message3.tail, lifeline, lifeline.lifetime.port)
|
||||
connect(message1, message1.head, lifeline, lifeline.lifetime.port)
|
||||
connect(message2, message2.head, exec_spec, exec_spec.ports()[1])
|
||||
|
||||
occurrences = [
|
||||
message1.subject.sendEvent,
|
||||
exec_spec.subject.start,
|
||||
message2.subject.sendEvent,
|
||||
exec_spec.subject.finish,
|
||||
message3.subject.receiveEvent,
|
||||
]
|
||||
|
||||
order_lifeline_covered_by(lifeline)
|
||||
|
||||
assert list(lifeline.subject.coveredBy) == occurrences
|
@ -1,7 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from typing import Optional
|
||||
|
||||
import gaphas
|
||||
from gaphas.aspect import ConnectionSink
|
||||
from gaphas.aspect import Connector as ConnectorAspect
|
||||
from gaphas.geometry import Rectangle, distance_rectangle_point
|
||||
|
||||
from gaphor.diagram.text import TextAlign, text_point_at_line
|
||||
@ -43,6 +47,32 @@ def from_package_str(item):
|
||||
)
|
||||
|
||||
|
||||
def _get_sink(item, handle, target):
|
||||
assert item.canvas
|
||||
|
||||
hpos = item.canvas.get_matrix_i2i(item, target).transform_point(*handle.pos)
|
||||
port = None
|
||||
dist = 10e6
|
||||
for p in target.ports():
|
||||
pos, d = p.glue(hpos)
|
||||
if not port or d < dist:
|
||||
port = p
|
||||
dist = d
|
||||
|
||||
return ConnectionSink(target, port)
|
||||
|
||||
|
||||
def postload_connect(item: gaphas.Item, handle: gaphas.Handle, target: gaphas.Item):
|
||||
"""
|
||||
Helper function: when loading a model, handles should be connected as
|
||||
part of the `postload` step. This function finds a suitable spot on the
|
||||
`target` item to connect the handle to.
|
||||
"""
|
||||
connector = ConnectorAspect(item, handle)
|
||||
sink = _get_sink(item, handle, target)
|
||||
connector.connect(sink)
|
||||
|
||||
|
||||
# Note: the official documentation is using the terms "Shape" and "Edge" for element and line.
|
||||
|
||||
|
||||
@ -247,25 +277,6 @@ class LinePresentation(Presentation[S], gaphas.Line):
|
||||
def postload(self):
|
||||
assert self.canvas
|
||||
|
||||
def get_sink(handle, item):
|
||||
assert self.canvas
|
||||
|
||||
hpos = self.canvas.get_matrix_i2i(self, item).transform_point(*handle.pos)
|
||||
port = None
|
||||
dist = 10e6
|
||||
for p in item.ports():
|
||||
pos, d = p.glue(hpos)
|
||||
if not port or d < dist:
|
||||
port = p
|
||||
dist = d
|
||||
|
||||
return gaphas.aspect.ConnectionSink(item, port)
|
||||
|
||||
def postload_connect(handle, item):
|
||||
connector = gaphas.aspect.Connector(self, handle)
|
||||
sink = get_sink(handle, item)
|
||||
connector.connect(sink)
|
||||
|
||||
if hasattr(self, "_load_orthogonal"):
|
||||
# Ensure there are enough handles
|
||||
if self._load_orthogonal and len(self._handles) < 3:
|
||||
@ -274,18 +285,12 @@ class LinePresentation(Presentation[S], gaphas.Line):
|
||||
self.orthogonal = self._load_orthogonal
|
||||
del self._load_orthogonal
|
||||
|
||||
# First update matrix and solve constraints (NE and SW handle are
|
||||
# lazy and are resolved by the constraint solver rather than set
|
||||
# directly.
|
||||
self.canvas.update_matrix(self)
|
||||
self.canvas.solver.solve()
|
||||
|
||||
if hasattr(self, "_load_head_connection"):
|
||||
postload_connect(self.head, self._load_head_connection)
|
||||
postload_connect(self, self.head, self._load_head_connection)
|
||||
del self._load_head_connection
|
||||
|
||||
if hasattr(self, "_load_tail_connection"):
|
||||
postload_connect(self.tail, self._load_tail_connection)
|
||||
postload_connect(self, self.tail, self._load_tail_connection)
|
||||
del self._load_tail_connection
|
||||
|
||||
super().postload()
|
||||
|
@ -4,7 +4,6 @@ from gaphor.diagram.profiles.extension import ExtensionItem
|
||||
def _load():
|
||||
from gaphor.diagram.profiles import (
|
||||
extensionconnect,
|
||||
metaclasspropertypage,
|
||||
stereotypepage,
|
||||
)
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
from typing import Optional
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.connectors import IConnect, RelationshipConnect
|
||||
from gaphor.diagram.connectors import Connector, RelationshipConnect
|
||||
from gaphor.diagram.presentation import Classified
|
||||
from gaphor.diagram.profiles.extension import ExtensionItem
|
||||
|
||||
|
||||
@IConnect.register(Classified, ExtensionItem)
|
||||
@Connector.register(Classified, ExtensionItem)
|
||||
class ExtensionConnect(RelationshipConnect):
|
||||
"""Connect class and stereotype items using an extension item."""
|
||||
|
||||
@ -31,8 +33,11 @@ class ExtensionConnect(RelationshipConnect):
|
||||
c1 = self.get_connected(line.head)
|
||||
c2 = self.get_connected(line.tail)
|
||||
if c1 and c2:
|
||||
head_type = c1.subject
|
||||
tail_type = c2.subject
|
||||
assert isinstance(c1.subject, UML.Class)
|
||||
assert isinstance(c2.subject, UML.Stereotype)
|
||||
|
||||
head_type: UML.Class = c1.subject
|
||||
tail_type: UML.Stereotype = c2.subject
|
||||
|
||||
# First check if we do not already contain the right subject:
|
||||
if line.subject:
|
||||
|
@ -1,82 +0,0 @@
|
||||
"""
|
||||
Metaclass item editors.
|
||||
"""
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.core import gettext
|
||||
from gaphor.diagram.propertypages import (
|
||||
NamedElementPropertyPage,
|
||||
PropertyPages,
|
||||
create_hbox_label,
|
||||
)
|
||||
|
||||
|
||||
def _issubclass(c, b):
|
||||
try:
|
||||
return issubclass(c, b)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
@PropertyPages.register(UML.Class)
|
||||
class MetaclassNamePropertyPage(NamedElementPropertyPage):
|
||||
"""
|
||||
Metaclass name editor. Provides editable combo box entry with
|
||||
predefined list of names of UML classes.
|
||||
"""
|
||||
|
||||
order = 10
|
||||
|
||||
NAME_LABEL = gettext("Name")
|
||||
|
||||
CLASSES = list(
|
||||
sorted(
|
||||
n
|
||||
for n in dir(UML)
|
||||
if _issubclass(getattr(UML, n), UML.Element) and n != "Stereotype"
|
||||
)
|
||||
)
|
||||
|
||||
def construct(self):
|
||||
if not UML.model.is_metaclass(self.subject):
|
||||
return super().construct()
|
||||
|
||||
page = Gtk.VBox()
|
||||
|
||||
subject = self.subject
|
||||
if not subject:
|
||||
return page
|
||||
|
||||
hbox = create_hbox_label(self, page, self.NAME_LABEL)
|
||||
model = Gtk.ListStore(str)
|
||||
for c in self.CLASSES:
|
||||
model.append([c])
|
||||
|
||||
cb = Gtk.ComboBox.new_with_model_and_entry(model)
|
||||
|
||||
completion = Gtk.EntryCompletion()
|
||||
completion.set_model(model)
|
||||
completion.set_minimum_key_length(1)
|
||||
completion.set_text_column(0)
|
||||
cb.get_child().set_completion(completion)
|
||||
|
||||
entry = cb.get_child()
|
||||
entry.set_text(subject and subject.name or "")
|
||||
hbox.pack_start(cb, True, True, 0)
|
||||
page.default = entry
|
||||
|
||||
# monitor subject.name attribute
|
||||
changed_id = entry.connect("changed", self._on_name_change)
|
||||
|
||||
def handler(event):
|
||||
if event.element is subject and event.new_value is not None:
|
||||
entry.handler_block(changed_id)
|
||||
entry.set_text(event.new_value)
|
||||
entry.handler_unblock(changed_id)
|
||||
|
||||
self.watcher.watch("name", handler).subscribe_all()
|
||||
entry.connect("destroy", self.watcher.unsubscribe_all)
|
||||
page.show_all()
|
||||
return page
|
@ -26,11 +26,13 @@ Style = TypedDict(
|
||||
"line-width": float,
|
||||
"vertical-spacing": float,
|
||||
"border-radius": float,
|
||||
"fill": str,
|
||||
"font": str,
|
||||
"font-style": FontStyle,
|
||||
"font-weight": Optional[FontWeight],
|
||||
"text-decoration": Optional[TextDecoration],
|
||||
"text-align": TextAlign,
|
||||
"stoke": str,
|
||||
"vertical-align": VerticalAlign,
|
||||
# CommentItem:
|
||||
"ear": int,
|
||||
@ -66,6 +68,13 @@ def draw_border(box, context, bounding_box):
|
||||
cr.rectangle(x, y, width, height)
|
||||
|
||||
cr.close_path()
|
||||
|
||||
fill = box.style("fill")
|
||||
if fill:
|
||||
color = cr.get_source()
|
||||
cr.set_source_rgb(1, 1, 1) # white
|
||||
cr.fill_preserve()
|
||||
cr.set_source(color)
|
||||
cr.stroke()
|
||||
|
||||
|
||||
@ -98,7 +107,8 @@ class Box:
|
||||
- min-height
|
||||
- min-width
|
||||
- padding: a tuple (top, right, bottom, left)
|
||||
|
||||
- vertical-align: alignment of child shapes
|
||||
- border-radius
|
||||
"""
|
||||
|
||||
def __init__(self, *children, style: Style = {}, draw=None):
|
||||
@ -110,6 +120,7 @@ class Box:
|
||||
"padding": (0, 0, 0, 0),
|
||||
"vertical-align": VerticalAlign.MIDDLE,
|
||||
"border-radius": 0,
|
||||
"fill": None,
|
||||
**style, # type: ignore[misc] # noqa: F821
|
||||
}
|
||||
self._draw_border = draw
|
||||
|
@ -7,7 +7,7 @@ gaphor.adapter package.
|
||||
"""
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.connectors import IConnect, RelationshipConnect
|
||||
from gaphor.diagram.connectors import Connector, RelationshipConnect
|
||||
from gaphor.diagram.states.pseudostates import (
|
||||
HistoryPseudostateItem,
|
||||
InitialPseudostateItem,
|
||||
@ -35,7 +35,7 @@ class VertexConnect(RelationshipConnect):
|
||||
relation.guard = self.line.model.create(UML.Constraint)
|
||||
|
||||
|
||||
@IConnect.register(VertexItem, TransitionItem)
|
||||
@Connector.register(VertexItem, TransitionItem)
|
||||
class TransitionConnect(VertexConnect):
|
||||
"""Connect two state vertices using transition item."""
|
||||
|
||||
@ -59,7 +59,7 @@ class TransitionConnect(VertexConnect):
|
||||
return None
|
||||
|
||||
|
||||
@IConnect.register(InitialPseudostateItem, TransitionItem)
|
||||
@Connector.register(InitialPseudostateItem, TransitionItem)
|
||||
class InitialPseudostateTransitionConnect(VertexConnect):
|
||||
"""Connect initial pseudostate using transition item.
|
||||
|
||||
@ -72,8 +72,6 @@ class InitialPseudostateTransitionConnect(VertexConnect):
|
||||
Glue to initial pseudostate with transition's head and when there are
|
||||
no transitions connected.
|
||||
"""
|
||||
assert self.canvas
|
||||
|
||||
line = self.line
|
||||
element = self.element
|
||||
|
||||
@ -90,7 +88,7 @@ class InitialPseudostateTransitionConnect(VertexConnect):
|
||||
return None
|
||||
|
||||
|
||||
@IConnect.register(HistoryPseudostateItem, TransitionItem)
|
||||
@Connector.register(HistoryPseudostateItem, TransitionItem)
|
||||
class HistoryPseudostateTransitionConnect(VertexConnect):
|
||||
"""Connect history pseudostate using transition item.
|
||||
|
||||
|
1
gaphor/diagram/tests/conftest.py
Normal file
1
gaphor/diagram/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from gaphor.diagram.tests.fixtures import diagram, element_factory, loader
|
94
gaphor/diagram/tests/fixtures.py
Normal file
94
gaphor/diagram/tests/fixtures.py
Normal file
@ -0,0 +1,94 @@
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
from gaphas.aspect import ConnectionSink
|
||||
from gaphas.aspect import Connector as ConnectorAspect
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.connectors import Connector
|
||||
from gaphor.misc.xmlwriter import XMLWriter
|
||||
from gaphor.services.eventmanager import EventManager
|
||||
from gaphor.storage import storage
|
||||
from gaphor.UML.elementfactory import ElementFactory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def element_factory():
|
||||
return ElementFactory(EventManager())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def diagram(element_factory):
|
||||
return element_factory.create(UML.Diagram)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def saver(element_factory):
|
||||
def save():
|
||||
"""
|
||||
Save diagram into string.
|
||||
"""
|
||||
|
||||
f = StringIO()
|
||||
storage.save(XMLWriter(f), element_factory)
|
||||
data = f.getvalue()
|
||||
f.close()
|
||||
|
||||
return data
|
||||
|
||||
return save
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def loader(element_factory):
|
||||
def load(data):
|
||||
"""
|
||||
Load data from specified string.
|
||||
"""
|
||||
element_factory.flush()
|
||||
assert not list(element_factory.select())
|
||||
|
||||
f = StringIO(data)
|
||||
storage.load(f, factory=element_factory)
|
||||
f.close()
|
||||
|
||||
return load
|
||||
|
||||
|
||||
def allow(line, handle, item, port=None):
|
||||
if port is None and len(item.ports()) > 0:
|
||||
port = item.ports()[0]
|
||||
|
||||
adapter = Connector(item, line)
|
||||
return adapter.allow(handle, port)
|
||||
|
||||
|
||||
def connect(line, handle, item, port=None):
|
||||
"""
|
||||
Connect line's handle to an item.
|
||||
|
||||
If port is not provided, then first port is used.
|
||||
"""
|
||||
canvas = line.canvas
|
||||
|
||||
if port is None and len(item.ports()) > 0:
|
||||
port = item.ports()[0]
|
||||
|
||||
sink = ConnectionSink(item, port)
|
||||
connector = ConnectorAspect(line, handle)
|
||||
|
||||
connector.connect(sink)
|
||||
|
||||
cinfo = canvas.get_connection(handle)
|
||||
assert cinfo.connected is item
|
||||
assert cinfo.port is port
|
||||
|
||||
|
||||
def disconnect(line, handle):
|
||||
"""
|
||||
Disconnect line's handle.
|
||||
"""
|
||||
canvas = line.canvas
|
||||
|
||||
canvas.disconnect_item(line, handle)
|
||||
assert not canvas.get_connection(handle)
|
@ -1,9 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.presentation import ElementPresentation, LinePresentation
|
||||
from gaphor.services.eventmanager import EventManager
|
||||
from gaphor.UML.elementfactory import ElementFactory
|
||||
|
||||
|
||||
class DummyVisualComponent:
|
||||
@ -24,16 +20,6 @@ class StubLine(LinePresentation):
|
||||
super().__init__(id, model, shape_middle=DummyVisualComponent())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def element_factory():
|
||||
return ElementFactory(EventManager())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def diagram(element_factory):
|
||||
return element_factory.create(UML.Diagram)
|
||||
|
||||
|
||||
def test_creation(element_factory):
|
||||
p = element_factory.create(UML.Presentation)
|
||||
|
||||
|
@ -3,13 +3,13 @@ Use cases related connection adapters.
|
||||
"""
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.diagram.connectors import IConnect, RelationshipConnect
|
||||
from gaphor.diagram.connectors import Connector, RelationshipConnect
|
||||
from gaphor.diagram.usecases.extend import ExtendItem
|
||||
from gaphor.diagram.usecases.include import IncludeItem
|
||||
from gaphor.diagram.usecases.usecase import UseCaseItem
|
||||
|
||||
|
||||
@IConnect.register(UseCaseItem, IncludeItem)
|
||||
@Connector.register(UseCaseItem, IncludeItem)
|
||||
class IncludeConnect(RelationshipConnect):
|
||||
"""Connect use cases with an include item relationship."""
|
||||
|
||||
@ -33,7 +33,7 @@ class IncludeConnect(RelationshipConnect):
|
||||
self.line.subject = relation
|
||||
|
||||
|
||||
@IConnect.register(UseCaseItem, ExtendItem)
|
||||
@Connector.register(UseCaseItem, ExtendItem)
|
||||
class ExtendConnect(RelationshipConnect):
|
||||
"""Connect use cases with an extend item relationship."""
|
||||
|
||||
|
@ -113,7 +113,7 @@ class CopyService(Service, ActionProvider):
|
||||
# update items' matrix immediately
|
||||
for item in new_items.values():
|
||||
item.matrix.translate(10, 10)
|
||||
canvas.update_matrix(item)
|
||||
canvas.update_matrices([item])
|
||||
|
||||
# solve internal constraints of items immediately as item.postload
|
||||
# reconnects items and all handles have to be in place
|
||||
|
@ -185,8 +185,9 @@ def load_elements_generator(elements, factory, gaphor_version):
|
||||
yield from _load_attributes_and_references(elements, update_status_queue)
|
||||
|
||||
for d in factory.select(lambda e: isinstance(e, UML.Diagram)):
|
||||
canvas = d.canvas
|
||||
# update_now() is implicitly called when lock is released
|
||||
d.canvas.block_updates = False
|
||||
canvas.block_updates = False
|
||||
|
||||
# do a postload:
|
||||
for id, elem in list(elements.items()):
|
||||
|
34
gaphor/storage/tests/conftest.py
Normal file
34
gaphor/storage/tests/conftest.py
Normal file
@ -0,0 +1,34 @@
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.services.eventmanager import EventManager
|
||||
from gaphor.storage import storage
|
||||
from gaphor.UML.elementfactory import ElementFactory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def element_factory():
|
||||
return ElementFactory(EventManager())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def diagram(element_factory):
|
||||
return element_factory.create(UML.Diagram)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def loader(element_factory):
|
||||
def load(data):
|
||||
"""
|
||||
Load data from specified string.
|
||||
"""
|
||||
element_factory.flush()
|
||||
assert not list(element_factory.select())
|
||||
|
||||
f = StringIO(data)
|
||||
storage.load(f, factory=element_factory)
|
||||
f.close()
|
||||
|
||||
return load
|
106
gaphor/storage/tests/test_group.py
Normal file
106
gaphor/storage/tests/test_group.py
Normal file
@ -0,0 +1,106 @@
|
||||
import pytest
|
||||
|
||||
from gaphor.diagram.classes import DependencyItem
|
||||
from gaphor.diagram.components import NodeItem
|
||||
|
||||
PARENT_X = 189
|
||||
PARENT_Y = 207
|
||||
CHILD_ONE_X = 32
|
||||
CHILD_ONE_Y = 54
|
||||
CHILD_TWO_X = 44
|
||||
CHILD_TWO_Y = 208
|
||||
|
||||
|
||||
def handle_pos(item, handle_index):
|
||||
return tuple(map(float, item.handles()[handle_index].pos))
|
||||
|
||||
|
||||
def test_load_grouped_connected_items(element_factory, loader):
|
||||
loader(NODE_EXAMPLE_XML)
|
||||
|
||||
diagram = element_factory.lselect()[0]
|
||||
canvas = diagram.canvas
|
||||
node_item, dep_item = canvas.get_root_items()
|
||||
child_one, child_two = canvas.get_children(node_item)
|
||||
|
||||
assert isinstance(node_item, NodeItem)
|
||||
assert isinstance(dep_item, DependencyItem)
|
||||
assert isinstance(child_one, NodeItem)
|
||||
assert isinstance(child_two, NodeItem)
|
||||
|
||||
assert canvas.get_parent(child_one) is node_item
|
||||
|
||||
assert tuple(canvas.get_matrix_i2c(child_one)) == (
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
PARENT_X + CHILD_ONE_X,
|
||||
PARENT_Y + CHILD_ONE_Y,
|
||||
)
|
||||
assert tuple(canvas.get_matrix_i2c(child_two)) == (
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
PARENT_X + CHILD_TWO_X,
|
||||
PARENT_Y + CHILD_TWO_Y,
|
||||
)
|
||||
|
||||
|
||||
NODE_EXAMPLE_XML = f"""\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<gaphor xmlns="http://gaphor.sourceforge.net/model" version="3.0" gaphor-version="1.2.0rc2">
|
||||
<Diagram id="3ea414eb-5eb5-11ea-9ccf-45e3771927d8">
|
||||
<canvas>
|
||||
<item id="41f681ab-5eb5-11ea-9ccf-45e3771927d8" type="NodeItem">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, {PARENT_X}, {PARENT_Y})</val>
|
||||
</matrix>
|
||||
<width>
|
||||
<val>388.5</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>286.5</val>
|
||||
</height>
|
||||
<item id="4541c555-5eb5-11ea-9ccf-45e3771927d8" type="NodeItem">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, {CHILD_ONE_X}, {CHILD_ONE_Y})</val>
|
||||
</matrix>
|
||||
<width>
|
||||
<val>100.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>50.0</val>
|
||||
</height>
|
||||
</item>
|
||||
<item id="4f927913-5eb5-11ea-9ccf-45e3771927d8" type="NodeItem">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, {CHILD_TWO_X}, {CHILD_TWO_Y})</val>
|
||||
</matrix>
|
||||
<width>
|
||||
<val>100.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>50.0</val>
|
||||
</height>
|
||||
</item>
|
||||
</item>
|
||||
<item id="5b4d81cb-5eb5-11ea-9ccf-45e3771927d8" type="DependencyItem">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 274.0, 312.0)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(0.0, 0.0), (6.0, 104.0)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="4541c555-5eb5-11ea-9ccf-45e3771927d8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="4f927913-5eb5-11ea-9ccf-45e3771927d8"/>
|
||||
</tail-connection>
|
||||
</item>
|
||||
</canvas>
|
||||
</Diagram>
|
||||
</gaphor>
|
||||
"""
|
@ -5,6 +5,8 @@ Unittest the storage and parser modules
|
||||
import re
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.application import distribution
|
||||
from gaphor.diagram.classes import AssociationItem, ClassItem, InterfaceItem
|
||||
@ -147,6 +149,7 @@ class StorageTestCase(TestCase):
|
||||
assert len(elements) == 1, elements
|
||||
assert elements[0].name == difficult_name, elements[0].name
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_load_uml_metamodel(self):
|
||||
"""
|
||||
Test if the meta model can be loaded.
|
||||
|
@ -10,13 +10,14 @@ import unittest
|
||||
from io import StringIO
|
||||
from typing import Type, TypeVar
|
||||
|
||||
from gaphas.aspect import ConnectionSink, Connector
|
||||
from gaphas.aspect import ConnectionSink
|
||||
from gaphas.aspect import Connector as ConnectorAspect
|
||||
|
||||
# For DiagramItemConnector aspect:
|
||||
import gaphor.diagram.diagramtools # noqa
|
||||
from gaphor import UML
|
||||
from gaphor.application import Session
|
||||
from gaphor.diagram.connectors import IConnect
|
||||
from gaphor.diagram.connectors import Connector
|
||||
from gaphor.diagram.grouping import Group
|
||||
|
||||
T = TypeVar("T")
|
||||
@ -66,7 +67,7 @@ class TestCase(unittest.TestCase):
|
||||
if port is None and len(item.ports()) > 0:
|
||||
port = item.ports()[0]
|
||||
|
||||
adapter = IConnect(item, line)
|
||||
adapter = Connector(item, line)
|
||||
return adapter.allow(handle, port)
|
||||
|
||||
def connect(self, line, handle, item, port=None):
|
||||
@ -82,7 +83,7 @@ class TestCase(unittest.TestCase):
|
||||
port = item.ports()[0]
|
||||
|
||||
sink = ConnectionSink(item, port)
|
||||
connector = Connector(line, handle)
|
||||
connector = ConnectorAspect(line, handle)
|
||||
|
||||
connector.connect(sink)
|
||||
|
||||
|
@ -167,12 +167,9 @@ class ElementEditor(UIComponent, ActionProvider):
|
||||
Return an ordered list of (order, name, adapter).
|
||||
"""
|
||||
adaptermap = {}
|
||||
try:
|
||||
if item.subject:
|
||||
for adapter in PropertyPages(item.subject):
|
||||
adaptermap[adapter.name] = (adapter.order, adapter.name, adapter)
|
||||
except AttributeError:
|
||||
pass
|
||||
if item.subject:
|
||||
for adapter in PropertyPages(item.subject):
|
||||
adaptermap[adapter.name] = (adapter.order, adapter.name, adapter)
|
||||
for adapter in PropertyPages(item):
|
||||
adaptermap[adapter.name] = (adapter.order, adapter.name, adapter)
|
||||
|
||||
|
@ -33,6 +33,7 @@ ICONS=diagram \
|
||||
send-signal-action \
|
||||
accept-event-action \
|
||||
lifeline \
|
||||
execution-specification \
|
||||
message \
|
||||
interaction \
|
||||
state \
|
||||
|
39
gaphor/ui/icons/gaphor-execution-specification-symbolic.svg
Normal file
39
gaphor/ui/icons/gaphor-execution-specification-symbolic.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<?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="7.9999957"
|
||||
height="15.998676"
|
||||
viewBox="0 0 2.1166655 4.2329832"
|
||||
version="1.1"
|
||||
id="svg4268">
|
||||
<defs
|
||||
id="defs4262" />
|
||||
<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="path1267-3"
|
||||
d="m 1.0371094,3.0644531 a 0.18522686,0.18522686 0 0 0 -0.16406252,0.1875 v 0.7929688 a 0.18554686,0.18554686 0 1 0 0.37109372,0 V 3.2519531 a 0.18522686,0.18522686 0 0 0 -0.2070312,-0.1875 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-feature-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;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;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.37041667;stroke-linecap:round;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" />
|
||||
<path
|
||||
id="path1267-6"
|
||||
d="M 1.0546875,0 A 0.18522686,0.18522686 0 0 0 0.87304688,0.1875 v 0.79492188 a 0.18554686,0.18554686 0 1 0 0.37109372,0 V 0.1875 A 0.18522686,0.18522686 0 0 0 1.0546875,0 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-feature-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;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;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.37041667;stroke-linecap:round;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" />
|
||||
<path
|
||||
id="rect1263-5"
|
||||
d="M 0.23828125,0.79492188 A 0.26460978,0.26460978 0 0 0 0,1.0585938 V 3.1757812 A 0.26460978,0.26460978 0 0 0 0.26367188,3.4394531 H 1.8515625 A 0.26460978,0.26460978 0 0 0 2.1171875,3.1757812 V 1.0585938 A 0.26460978,0.26460978 0 0 0 1.8515625,0.79492188 H 0.26367188 a 0.26460978,0.26460978 0 0 0 -0.0253906,0 z M 0.52929688,1.3242188 H 1.5878906 V 2.9101562 H 0.52929688 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-feature-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;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;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.52916664;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" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.2 KiB |
@ -33,11 +33,11 @@
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="40.173252"
|
||||
inkscape:cx="24.316871"
|
||||
inkscape:cy="1146.0668"
|
||||
inkscape:zoom="160.69301"
|
||||
inkscape:cx="229.52617"
|
||||
inkscape:cy="1157.2818"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="pointer"
|
||||
inkscape:current-layer="execution-specification"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:window-width="3840"
|
||||
@ -1219,6 +1219,28 @@
|
||||
rx="1.5874995"
|
||||
ry="1.5874976" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="execution-specification"
|
||||
inkscape:label="execution-specification">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.37041667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 61.647917,-6.6659362 v 0.7937501"
|
||||
id="path1267-3"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.37041667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 61.647917,-9.7294768 v 0.7937496"
|
||||
id="path1267-6"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.52916664;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1263-5"
|
||||
width="1.5874989"
|
||||
height="2.116667"
|
||||
x="60.854168"
|
||||
y="-8.8583422" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="lifeline"
|
||||
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
@ -3,12 +3,13 @@ Test handle tool functionality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from gaphas.aspect import ConnectionSink, Connector
|
||||
from gaphas.aspect import ConnectionSink
|
||||
from gaphas.aspect import Connector as ConnectorAspect
|
||||
from gi.repository import Gdk, Gtk
|
||||
|
||||
from gaphor import UML
|
||||
from gaphor.application import Session
|
||||
from gaphor.diagram.connectors import IConnect
|
||||
from gaphor.diagram.connectors import Connector
|
||||
from gaphor.diagram.diagramtools import ConnectHandleTool, DiagramItemConnector
|
||||
from gaphor.diagram.general.comment import CommentItem
|
||||
from gaphor.diagram.general.commentline import CommentLineItem
|
||||
@ -73,16 +74,16 @@ def commentline(diagram):
|
||||
|
||||
|
||||
def test_aspect_type(commentline):
|
||||
aspect = Connector(commentline, commentline.handles()[0])
|
||||
aspect = ConnectorAspect(commentline, commentline.handles()[0])
|
||||
assert isinstance(aspect, DiagramItemConnector)
|
||||
|
||||
|
||||
def test_query(comment):
|
||||
assert IConnect(comment, commentline)
|
||||
def test_query(comment, commentline):
|
||||
assert Connector(comment, commentline)
|
||||
|
||||
|
||||
def test_allow(commentline, comment):
|
||||
aspect = Connector(commentline, commentline.handles()[0])
|
||||
aspect = ConnectorAspect(commentline, commentline.handles()[0])
|
||||
assert aspect.item is commentline
|
||||
assert aspect.handle is commentline.handles()[0]
|
||||
|
||||
@ -92,7 +93,7 @@ def test_allow(commentline, comment):
|
||||
|
||||
def test_connect(diagram, comment, commentline):
|
||||
sink = ConnectionSink(comment, comment.ports()[0])
|
||||
aspect = Connector(commentline, commentline.handles()[0])
|
||||
aspect = ConnectorAspect(commentline, commentline.handles()[0])
|
||||
aspect.connect(sink)
|
||||
canvas = diagram.canvas
|
||||
cinfo = canvas.get_connection(commentline.handles()[0])
|
||||
@ -146,7 +147,7 @@ def test_iconnect(session, event_manager, element_factory):
|
||||
assert cinfo.constraint is not None
|
||||
assert cinfo.connected is actor, cinfo.connected
|
||||
|
||||
Connector(line, handle).disconnect()
|
||||
ConnectorAspect(line, handle).disconnect()
|
||||
|
||||
cinfo = diagram.canvas.get_connection(handle)
|
||||
|
||||
|
@ -9,4 +9,7 @@ python_files = test_*.py
|
||||
|
||||
# Console tests are failing the GitHub Actions CI (seg fault)
|
||||
norecursedirs = gaphor/plugins/console/tests
|
||||
junit_family=xunit1
|
||||
junit_family=xunit1
|
||||
|
||||
markers =
|
||||
slow: marks tests as slow (deselect with '-m "not slow"')
|
Loading…
Reference in New Issue
Block a user