Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #408 where undo on diagram items breaks the model #410

Merged
merged 2 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion gaphor/core/modeling/diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def create_as(self, type, id, parent=None, subject=None):
if subject:
item.subject = subject
self.canvas.add(item, parent)
self.model.handle(DiagramItemCreated(self.model, item))
self.model.handle(DiagramItemCreated(self, item))
return item

def lookup(self, id):
Expand Down
17 changes: 6 additions & 11 deletions gaphor/core/modeling/elementfactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,17 +195,12 @@ def handle(self, event: object) -> None:
Handle events coming from elements.
"""
if isinstance(event, UnlinkEvent):
self._unlink_element(event.element)
element = event.element
assert isinstance(element.id, str)
try:
del self._elements[element.id]
except KeyError:
return
event = ElementDeleted(self, event.element)
if self.event_manager and not self._block_events:
self.event_manager.handle(event)

def _unlink_element(self, element: Element) -> None:
"""
NOTE: Invoked from Element.unlink() to perform an element unlink.
"""
try:
assert isinstance(element.id, str)
del self._elements[element.id]
except KeyError:
pass
24 changes: 16 additions & 8 deletions gaphor/core/modeling/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,6 @@ def __init__(self, service, element):
self.element = element


class DiagramItemCreated(ElementCreated):
"""A diagram item has been created."""

def __init__(self, service, element):
"""Constructor. The element parameter is the element being created."""
super().__init__(service, element)


class ElementDeleted(ServiceEvent):
"""An element has been deleted."""

Expand All @@ -176,6 +168,22 @@ def __init__(self, service, element):
self.element = element


class DiagramItemCreated:
"""A diagram item has been created."""

def __init__(self, diagram, element):
self.diagram = diagram
self.element = element


class DiagramItemDeleted:
"""A diagram item has been deleted."""

def __init__(self, diagram, element):
self.diagram = diagram
self.element = element


class ModelReady(ServiceEvent):
"""A generic element factory event."""

Expand Down
10 changes: 9 additions & 1 deletion gaphor/core/modeling/presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import TYPE_CHECKING, Callable, Generic, List, Optional, TypeVar

from gaphor.core.modeling import Element
from gaphor.core.modeling.event import DiagramItemDeleted
from gaphor.core.modeling.properties import association, relation_one

if TYPE_CHECKING:
Expand All @@ -20,6 +21,11 @@
class Presentation(Element, Generic[S]):
"""
This presentation is used to link the behaviors of `gaphor.core.modeling` and `gaphas.Item`.

Note that Presentations are not managed by the Element Factory. Instead, Presentation
objects are owned by Diagram.
As a result they do not emit ElementCreated and ElementDeleted events. Presentations have their own
create and delete events: DiagramItemCreated and DiagramItemDeleted.
"""

def __init__(self, id=None, model=None):
Expand Down Expand Up @@ -79,8 +85,10 @@ def unlink(self):
Remove the item from the canvas and set subject to None.
"""
if self.canvas:
diagram = self.diagram
self.canvas.remove(self)
super().unlink()
super().unlink()
self.handle(DiagramItemDeleted(diagram, self))


Element.presentation = association(
Expand Down
8 changes: 0 additions & 8 deletions gaphor/services/undomanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ def commit_transaction(self, event=None):
assert self._current_transaction

if self._current_transaction.can_execute():
# Here:
self.clear_redo_stack()
self._undo_stack.append(self._current_transaction)
while len(self._undo_stack) > self._stack_depth:
Expand Down Expand Up @@ -291,9 +290,6 @@ def _unregister_undo_handlers(self):
@event_handler(ElementCreated)
def undo_create_event(self, event):
factory = event.service
# A factory is not always present, e.g. for DiagramItems
if not factory:
return
element = event.element

def _undo_create_event():
Expand All @@ -308,11 +304,7 @@ def _undo_create_event():
@event_handler(ElementDeleted)
def undo_delete_event(self, event):
factory = event.service
# A factory is not always present, e.g. for DiagramItems
if not factory:
return
element = event.element
assert factory, f"No factory defined for {element} ({factory})"

def _undo_delete_event():
factory._elements[element.id] = element
Expand Down
48 changes: 41 additions & 7 deletions tests/test_undo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from gaphor import UML
from gaphor.application import Application
from gaphor.core import transactional
from gaphor.core import Transaction
from gaphor.UML.classes import AssociationItem, ClassItem


Expand All @@ -19,6 +19,11 @@ def session(application):
return application.new_session()


@pytest.fixture
def event_manager(session):
return session.get_service("event_manager")


@pytest.fixture
def element_factory(session):
return session.get_service("element_factory")
Expand Down Expand Up @@ -50,7 +55,7 @@ def connect(line, handle, item, port=None):
assert cinfo.port is port


def test_class_association_undo_redo(element_factory, undo_manager):
def test_class_association_undo_redo(event_manager, element_factory, undo_manager):
diagram = element_factory.create(UML.Diagram)

assert 0 == len(diagram.canvas.solver.constraints)
Expand All @@ -70,14 +75,11 @@ def test_class_association_undo_redo(element_factory, undo_manager):
assert 7 == len(element_factory.lselect())
assert 14 == len(diagram.canvas.solver.constraints)

@transactional
def delete_class():
ci2.unlink()

undo_manager.clear_undo_stack()
assert not undo_manager.can_undo()

delete_class()
with Transaction(event_manager):
ci2.unlink()

assert undo_manager.can_undo()

Expand All @@ -104,3 +106,35 @@ def get_connected(handle):
assert ci2 == get_connected(a.tail)

undo_manager.redo_transaction()


def test_diagram_item_should_not_end_up_in_element_factory(
event_manager, element_factory, undo_manager
):
diagram = element_factory.create(UML.Diagram)

with Transaction(event_manager):
cls = diagram.create(ClassItem, subject=element_factory.create(UML.Class))

undo_manager.undo_transaction()
undo_manager.redo_transaction()

assert cls not in element_factory.lselect(), element_factory.lselect()


def test_deleted_diagram_item_should_not_end_up_in_element_factory(
event_manager, element_factory, undo_manager
):
diagram = element_factory.create(UML.Diagram)
cls = diagram.create(ClassItem, subject=element_factory.create(UML.Class))

with Transaction(event_manager):
cls.unlink()

undo_manager.undo_transaction()

assert cls not in element_factory.lselect(), element_factory.lselect()

undo_manager.redo_transaction()

assert cls not in element_factory.lselect(), element_factory.lselect()