Skip to content

Commit

Permalink
Merge pull request #410 from gaphor/fix-undo-issue
Browse files Browse the repository at this point in the history
Fix #408 where undo on diagram items breaks the model
  • Loading branch information
danyeaw committed Aug 20, 2020
2 parents 1fc70f4 + 137b8f6 commit 7b92917
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 36 deletions.
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()

0 comments on commit 7b92917

Please sign in to comment.