From 49c6b95a45ccdb992a5b9edd5ba66c764cb7052f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertram=20H=C3=B8jlund=20Petersen?= Date: Thu, 9 Jan 2025 12:56:19 +0100 Subject: [PATCH 1/5] Refactor Undo Redo actions --- .../jhotdraw/action/AbstractViewAction.java | 19 ++-- .../action/edit/AbstractUndoRedoAction.java | 101 ++++++++++++++++++ .../org/jhotdraw/action/edit/RedoAction.java | 80 +------------- .../org/jhotdraw/action/edit/UndoAction.java | 80 +------------- 4 files changed, 114 insertions(+), 166 deletions(-) create mode 100644 jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/AbstractUndoRedoAction.java diff --git a/jhotdraw-actions/src/main/java/org/jhotdraw/action/AbstractViewAction.java b/jhotdraw-actions/src/main/java/org/jhotdraw/action/AbstractViewAction.java index b072f9be8..84382a7ff 100644 --- a/jhotdraw-actions/src/main/java/org/jhotdraw/action/AbstractViewAction.java +++ b/jhotdraw-actions/src/main/java/org/jhotdraw/action/AbstractViewAction.java @@ -8,6 +8,7 @@ package org.jhotdraw.action; import java.beans.*; +import java.util.Objects; import javax.swing.*; import org.jhotdraw.api.app.Application; import org.jhotdraw.api.app.View; @@ -44,24 +45,18 @@ public abstract class AbstractViewAction extends AbstractAction { * the enabled state of the view and the application. */ private boolean combinedEnabled = true; - private PropertyChangeListener applicationListener = new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - if ((evt.getPropertyName() == null && Application.ACTIVE_VIEW_PROPERTY == null) || (evt.getPropertyName() != null && evt.getPropertyName().equals(Application.ACTIVE_VIEW_PROPERTY))) { // Strings get interned - updateView((View) evt.getOldValue(), (View) evt.getNewValue()); - } + private final PropertyChangeListener applicationListener = evt -> { + if (Objects.equals(evt.getPropertyName(), Application.ACTIVE_VIEW_PROPERTY)) { + updateView((View) evt.getOldValue(), (View) evt.getNewValue()); } }; - private PropertyChangeListener viewListener = new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { + private final PropertyChangeListener viewListener = evt -> { String name = evt.getPropertyName(); - if ("enabled".equals(name)) { + if (Objects.equals(name, "enabled")) { updateEnabled(); - } else if ((name == null && propertyName == null) || (name != null && name.equals(propertyName))) { + } else if (Objects.equals(name, propertyName)) { updateView(); } - } }; /** diff --git a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/AbstractUndoRedoAction.java b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/AbstractUndoRedoAction.java new file mode 100644 index 000000000..34af5cebc --- /dev/null +++ b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/AbstractUndoRedoAction.java @@ -0,0 +1,101 @@ +package org.jhotdraw.action.edit; + +import org.jhotdraw.action.AbstractViewAction; +import org.jhotdraw.api.app.Application; +import org.jhotdraw.api.app.View; +import org.jhotdraw.util.ResourceBundleUtil; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; + +/** + * An abstract base class for undo and redo actions that centralizes common logic. + *

+ * This class uses an {@code actionID} to differentiate between various actions, + * configures resource bundles, manages property change listeners, and delegates + * action invocation to the corresponding real action from the view. + *

+ */ +public abstract class AbstractUndoRedoAction extends AbstractViewAction { + protected final String actionID; + protected final ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.action.Labels"); + private final PropertyChangeListener actionPropertyListener = evt -> { + String name = evt.getPropertyName(); + if (Objects.equals(name, AbstractAction.NAME)) { + putValue(AbstractAction.NAME, evt.getNewValue()); + } else if ("enabled".equals(name)) { + updateEnabledState(); + } + }; + + protected AbstractUndoRedoAction(Application app, View view, String id) { + super(app, view); + this.actionID = id; + labels.configureAction(this, id); + } + + private Action getRealAction() { + return (getActiveView() == null) ? null : getActiveView().getActionMap().get(actionID); + } + + protected void updateEnabledState() { + boolean isEnabled = false; + Action realAction = getRealAction(); + if (realAction != null && realAction != this) { + isEnabled = realAction.isEnabled(); + } + setEnabled(isEnabled); + } + + @Override + public void actionPerformed(ActionEvent e) { + Action realAction = getRealAction(); + if (realAction != null && realAction != this) { + realAction.actionPerformed(e); + } + } + + /** + * Installs listeners on the view object. + */ + @Override + protected void installViewListeners(View p) { + super.installViewListeners(p); + Action actionInView = p.getActionMap().get(actionID); + if (actionInView != null && actionInView != this) { + actionInView.addPropertyChangeListener(actionPropertyListener); + } + } + + + /** + * Installs listeners on the view object. + */ + @Override + protected void uninstallViewListeners(View p) { + super.uninstallViewListeners(p); + Action actionInView = p.getActionMap().get(actionID); + if (actionInView != null && actionInView != this) { + actionInView.removePropertyChangeListener(actionPropertyListener); + } + } + + + @Override + protected void updateView(View oldValue, View newValue) { + super.updateView(oldValue, newValue); + if (newValue == null || newValue.getActionMap().get(actionID) == null) { + return; + } + Action actionInView = newValue.getActionMap().get(actionID); + if (actionInView != this) { + putValue(AbstractAction.NAME, actionInView.getValue(AbstractAction.NAME)); + updateEnabledState(); + } + } + + +} + diff --git a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/RedoAction.java b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/RedoAction.java index 794ab08c8..aa1b8d1fa 100644 --- a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/RedoAction.java +++ b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/RedoAction.java @@ -7,13 +7,9 @@ */ package org.jhotdraw.action.edit; -import java.awt.event.*; -import java.beans.*; -import javax.swing.*; -import org.jhotdraw.action.AbstractViewAction; import org.jhotdraw.api.app.Application; import org.jhotdraw.api.app.View; -import org.jhotdraw.util.*; + /** * Redoes the last user action on the active view. @@ -32,85 +28,15 @@ * @author Werner Randelshofer * @version $Id$ */ -public class RedoAction extends AbstractViewAction { - +public class RedoAction extends AbstractUndoRedoAction { private static final long serialVersionUID = 1L; public static final String ID = "edit.redo"; - private ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.action.Labels"); - private PropertyChangeListener redoActionPropertyListener = new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - String name = evt.getPropertyName(); - if ((name == null && AbstractAction.NAME == null) || (name != null && name.equals(AbstractAction.NAME))) { - putValue(AbstractAction.NAME, evt.getNewValue()); - } else if ("enabled".equals(name)) { - updateEnabledState(); - } - } - }; /** * Creates a new instance. */ public RedoAction(Application app, View view) { - super(app, view); - labels.configureAction(this, ID); + super(app, view, ID); } - protected void updateEnabledState() { - boolean isEnabled = false; - Action realRedoAction = getRealRedoAction(); - if (realRedoAction != null && realRedoAction != this) { - isEnabled = realRedoAction.isEnabled(); - } - setEnabled(isEnabled); - } - - @Override - protected void updateView(View oldValue, View newValue) { - super.updateView(oldValue, newValue); - if (newValue != null - && newValue.getActionMap().get(ID) != null - && newValue.getActionMap().get(ID) != this) { - putValue(AbstractAction.NAME, newValue.getActionMap().get(ID). - getValue(AbstractAction.NAME)); - updateEnabledState(); - } - } - - /** - * Installs listeners on the view object. - */ - @Override - protected void installViewListeners(View p) { - super.installViewListeners(p); - Action redoActionInView = p.getActionMap().get(ID); - if (redoActionInView != null && redoActionInView != this) { - redoActionInView.addPropertyChangeListener(redoActionPropertyListener); - } - } - - /** - * Installs listeners on the view object. - */ - @Override - protected void uninstallViewListeners(View p) { - super.uninstallViewListeners(p); - Action redoActionInView = p.getActionMap().get(ID); - if (redoActionInView != null && redoActionInView != this) { - redoActionInView.removePropertyChangeListener(redoActionPropertyListener); - } - } - - @Override - public void actionPerformed(ActionEvent e) { - Action realAction = getRealRedoAction(); - if (realAction != null && realAction != this) { - realAction.actionPerformed(e); - } - } - - private Action getRealRedoAction() { - return (getActiveView() == null) ? null : getActiveView().getActionMap().get(ID); - } } diff --git a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/UndoAction.java b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/UndoAction.java index 74c4f2cea..206b0d3f6 100644 --- a/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/UndoAction.java +++ b/jhotdraw-actions/src/main/java/org/jhotdraw/action/edit/UndoAction.java @@ -7,13 +7,9 @@ */ package org.jhotdraw.action.edit; -import java.awt.event.*; -import java.beans.*; -import javax.swing.*; -import org.jhotdraw.action.AbstractViewAction; import org.jhotdraw.api.app.Application; import org.jhotdraw.api.app.View; -import org.jhotdraw.util.*; + /** * Undoes the last user action. @@ -31,85 +27,15 @@ * @author Werner Randelshofer * @version $Id$ */ -public class UndoAction extends AbstractViewAction { +public class UndoAction extends AbstractUndoRedoAction { private static final long serialVersionUID = 1L; public static final String ID = "edit.undo"; - private ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.action.Labels"); - private PropertyChangeListener redoActionPropertyListener = new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - String name = evt.getPropertyName(); - if ((name == null && AbstractAction.NAME == null) || (name != null && name.equals(AbstractAction.NAME))) { - putValue(AbstractAction.NAME, evt.getNewValue()); - } else if ("enabled".equals(name)) { - updateEnabledState(); - } - } - }; /** * Creates a new instance. */ public UndoAction(Application app, View view) { - super(app, view); - labels.configureAction(this, ID); - } - - protected void updateEnabledState() { - boolean isEnabled = false; - Action realAction = getRealUndoAction(); - if (realAction != null && realAction != this) { - isEnabled = realAction.isEnabled(); - } - setEnabled(isEnabled); - } - - @Override - protected void updateView(View oldValue, View newValue) { - super.updateView(oldValue, newValue); - if (newValue != null - && newValue.getActionMap().get(ID) != null - && newValue.getActionMap().get(ID) != this) { - putValue(AbstractAction.NAME, newValue.getActionMap().get(ID). - getValue(AbstractAction.NAME)); - updateEnabledState(); - } - } - - /** - * Installs listeners on the view object. - */ - @Override - protected void installViewListeners(View p) { - super.installViewListeners(p); - Action undoActionInView = p.getActionMap().get(ID); - if (undoActionInView != null && undoActionInView != this) { - undoActionInView.addPropertyChangeListener(redoActionPropertyListener); - } - } - - /** - * Installs listeners on the view object. - */ - @Override - protected void uninstallViewListeners(View p) { - super.uninstallViewListeners(p); - Action undoActionInView = p.getActionMap().get(ID); - if (undoActionInView != null && undoActionInView != this) { - undoActionInView.removePropertyChangeListener(redoActionPropertyListener); - } - } - - @Override - public void actionPerformed(ActionEvent e) { - Action realUndoAction = getRealUndoAction(); - if (realUndoAction != null && realUndoAction != this) { - realUndoAction.actionPerformed(e); - } - } - - private Action getRealUndoAction() { - return (getActiveView() == null) ? null : getActiveView().getActionMap().get(ID); + super(app, view, ID); } } From 32e49a119c6b8986d88541bd82ee7cdc58495695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertram=20H=C3=B8jlund=20Petersen?= Date: Thu, 9 Jan 2025 12:58:45 +0100 Subject: [PATCH 2/5] Refactory DefaultDrawingView --- .../org/jhotdraw/draw/DefaultDrawingView.java | 38 +++++- .../draw/DefaultDrawingViewEventHandler.java | 113 ++++++++++++++++++ .../draw/IDrawingViewEventHandler.java | 14 +++ pom.xml | 10 +- 4 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 jhotdraw-core/src/main/java/org/jhotdraw/draw/DefaultDrawingViewEventHandler.java create mode 100644 jhotdraw-core/src/main/java/org/jhotdraw/draw/IDrawingViewEventHandler.java diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/DefaultDrawingView.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/DefaultDrawingView.java index de7e77022..25af20ddd 100644 --- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/DefaultDrawingView.java +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/DefaultDrawingView.java @@ -55,7 +55,7 @@ public class DefaultDrawingView * used to select the figures. */ private Set
selectedFigures = new LinkedHashSet<>(); - private LinkedList selectionHandles = new LinkedList<>(); + LinkedList selectionHandles = new LinkedList<>(); private boolean isConstrainerVisible = false; private Constrainer visibleConstrainer = new GridConstrainer(8, 8); private Constrainer invisibleConstrainer = new GridConstrainer(); @@ -182,8 +182,34 @@ public boolean isSelectionEmpty() { return selectedFigures.isEmpty(); } + public void setSecondaryHandleOwner(Handle handle) { + this.secondaryHandleOwner = handle; + } + + public void handleSecondaryHandlesRequest(HandleEvent e) { + secondaryHandleOwner = e.getHandle(); + secondaryHandles.clear(); + secondaryHandles.addAll(secondaryHandleOwner.createSecondaryHandles()); + for (Handle h : secondaryHandles) { + h.setView(DefaultDrawingView.this); + h.addHandleListener(eventHandler); + } + repaint(); + } + protected void setFocusGained() { + if (editor != null) { + editor.setActiveView(DefaultDrawingView.this); + } + } + private class EventHandler implements FigureListener, CompositeFigureListener, HandleListener, FocusListener { + private Drawing drawing; + + protected EventHandler(Drawing drawing1) { + this.drawing = drawing1; + } + @Override public void figureAdded(CompositeFigureEvent evt) { if (drawing.getChildCount() == 1 && getEmptyDrawingMessage() != null) { @@ -291,7 +317,7 @@ public void figureRemoved(FigureEvent e) { public void figureRequestRemove(FigureEvent e) { } } - private EventHandler eventHandler; + private IDrawingViewEventHandler eventHandler; /** * Creates new instance. @@ -307,8 +333,8 @@ public DefaultDrawingView() { setOpaque(true); } - protected EventHandler createEventHandler() { - return new EventHandler(); + protected IDrawingViewEventHandler createEventHandler() { + return new DefaultDrawingViewEventHandler(this); } /** @@ -913,7 +939,7 @@ private java.util.List getSecondaryHandles() { /** * Invalidates the handles. */ - private void invalidateHandles() { + void invalidateHandles() { if (handlesAreValid) { handlesAreValid = false; Rectangle invalidatedArea = null; @@ -1138,7 +1164,7 @@ public void setBounds(int x, int y, int width, int height) { * Updates the view translation taking into account the current dimension of the view * JComponent, the size of the drawing, and the scale factor. */ - private void validateViewTranslation() { + void validateViewTranslation() { if (getDrawing() == null) { translation.x = translation.y = 0; return; diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/DefaultDrawingViewEventHandler.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/DefaultDrawingViewEventHandler.java new file mode 100644 index 000000000..2ac51b3b0 --- /dev/null +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/DefaultDrawingViewEventHandler.java @@ -0,0 +1,113 @@ +package org.jhotdraw.draw; + +import org.jhotdraw.draw.event.*; + +import java.awt.event.FocusEvent; + +import static org.jhotdraw.draw.AttributeKeys.CANVAS_HEIGHT; +import static org.jhotdraw.draw.AttributeKeys.CANVAS_WIDTH; + +public class DefaultDrawingViewEventHandler implements IDrawingViewEventHandler { + private DefaultDrawingView view; + + protected DefaultDrawingViewEventHandler(DefaultDrawingView drawingView) { + this.view = drawingView; + } + + @Override + public void figureAdded(CompositeFigureEvent evt) { + if (view.getDrawing().getChildCount() == 1 && view.getEmptyDrawingMessage() != null) { + view.repaint(); + } else { + view.repaintDrawingArea(evt.getInvalidatedArea()); + } + view.invalidateDimension(); + } + + @Override + public void figureRemoved(CompositeFigureEvent evt) { + if (view.getDrawing().getChildCount() == 0 && view.getEmptyDrawingMessage() != null) { + view.repaint(); + } else { + view.repaintDrawingArea(evt.getInvalidatedArea()); + } + view.removeFromSelection(evt.getChildFigure()); + view.invalidateDimension(); + } + + @Override + public void areaInvalidated(FigureEvent evt) { + view.repaintDrawingArea(evt.getInvalidatedArea()); + view.invalidateDimension(); + } + + @Override + public void areaInvalidated(HandleEvent evt) { + view.repaint(evt.getInvalidatedArea()); + view.invalidateDimension(); + } + + @Override + public void handleRequestSecondaryHandles(HandleEvent e) { + view.handleSecondaryHandlesRequest(e); + } + + @Override + public void focusGained(FocusEvent e) { + view.setFocusGained(); + } + + @Override + public void focusLost(FocusEvent e) { + // repaintHandles(); + } + + @Override + public void handleRequestRemove(HandleEvent e) { + view.selectionHandles.remove(e.getHandle()); + e.getHandle().dispose(); + view.invalidateHandles(); + view.repaint(e.getInvalidatedArea()); + } + + @Override + public void attributeChanged(FigureEvent e) { + if (e.getSource() == view.getDrawing()) { + AttributeKey a = e.getAttribute(); + if (a.equals(CANVAS_HEIGHT) || a.equals(CANVAS_WIDTH)) { + view.validateViewTranslation(); + view.repaint(); // must repaint everything + } + if (e.getInvalidatedArea() != null) { + view.repaintDrawingArea(e.getInvalidatedArea()); + } else { + view.repaintDrawingArea(view.viewToDrawing(view.getCanvasViewBounds())); + } + } else { + if (e.getInvalidatedArea() != null) { + view.repaintDrawingArea(e.getInvalidatedArea()); + } + } + } + + @Override + public void figureHandlesChanged(FigureEvent e) { + } + + @Override + public void figureChanged(FigureEvent e) { + view.repaintDrawingArea(e.getInvalidatedArea()); + } + + @Override + public void figureAdded(FigureEvent e) { + } + + @Override + public void figureRemoved(FigureEvent e) { + } + + @Override + public void figureRequestRemove(FigureEvent e) { + } +} diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/IDrawingViewEventHandler.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/IDrawingViewEventHandler.java new file mode 100644 index 000000000..d815f20dd --- /dev/null +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/IDrawingViewEventHandler.java @@ -0,0 +1,14 @@ +package org.jhotdraw.draw; + +import org.jhotdraw.draw.event.CompositeFigureListener; +import org.jhotdraw.draw.event.FigureListener; +import org.jhotdraw.draw.event.HandleEvent; +import org.jhotdraw.draw.event.HandleListener; + +import java.awt.event.FocusListener; + +public interface IDrawingViewEventHandler extends FigureListener, CompositeFigureListener, HandleListener, FocusListener { + void handleRequestSecondaryHandles(HandleEvent e); + + void handleRequestRemove(HandleEvent e); +} diff --git a/pom.xml b/pom.xml index 5f6c7eef5..39eeb55a7 100644 --- a/pom.xml +++ b/pom.xml @@ -62,11 +62,11 @@ - jhotdraw-core - jhotdraw-samples - jhotdraw-xml - jhotdraw-api - jhotdraw-utils + jhotdraw-core + jhotdraw-samples + jhotdraw-xml + jhotdraw-api + jhotdraw-utils jhotdraw-gui jhotdraw-app jhotdraw-datatransfer From 8808f522b9e20d7fb5fbe7fd50465b0f51b5c891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertram=20H=C3=B8jlund=20Petersen?= Date: Thu, 9 Jan 2025 22:07:46 +0100 Subject: [PATCH 3/5] Added unit tests --- jhotdraw-actions/pom.xml | 12 +++ .../edit/AbstractUndoRedoActionTest.java | 77 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionTest.java diff --git a/jhotdraw-actions/pom.xml b/jhotdraw-actions/pom.xml index 5cc33e1c6..fe2055bc2 100644 --- a/jhotdraw-actions/pom.xml +++ b/jhotdraw-actions/pom.xml @@ -24,5 +24,17 @@ jhotdraw-datatransfer ${project.version} + + org.mockito + mockito-core + 5.7.0 + test + + + org.junit.jupiter + junit-jupiter + RELEASE + test + \ No newline at end of file diff --git a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionTest.java b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionTest.java new file mode 100644 index 000000000..e0bba9b5b --- /dev/null +++ b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionTest.java @@ -0,0 +1,77 @@ +package org.jhotdraw.action.edit; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.jhotdraw.action.edit.AbstractUndoRedoAction; +import org.junit.jupiter.api.BeforeEach; +import org.jhotdraw.api.app.Application; +import org.jhotdraw.api.app.View; +import org.junit.jupiter.api.Test; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeListener; + +class AbstractUndoRedoActionTest { + private Application mockApp; + private View mockView; + private AbstractUndoRedoAction action; + private ActionEvent mockEvent; + private Action mockRealAction; + + @BeforeEach + void setUp() { + mockApp = mock(Application.class); + mockView = mock(View.class); + mockEvent = mock(ActionEvent.class); + mockRealAction = mock(Action.class); + when(mockView.getActionMap()).thenReturn(new ActionMap()); + action = new AbstractUndoRedoAction(mockApp, mockView, "undo") {}; + } + + @Test + void actionPerformed_delegatesToRealAction() { + mockView.getActionMap().put("undo", mockRealAction); + action.updateView(null, mockView); + action.actionPerformed(mockEvent); + verify(mockRealAction).actionPerformed(mockEvent); + } + + @Test + void actionPerformed_noRealAction_doesNothing() { + action.updateView(null, mockView); + action.actionPerformed(mockEvent); + verify(mockRealAction, never()).actionPerformed(mockEvent); + } + + @Test + void updateEnabledState_realActionEnabled_setsEnabledTrue() { + when(mockRealAction.isEnabled()).thenReturn(true); + when(mockApp.isEnabled()).thenReturn(true); + when(mockView.isEnabled()).thenReturn(true); + mockView.getActionMap().put("undo", mockRealAction); + action.updateView(null, mockView); + action.updateEnabledState(); + assertTrue(action.isEnabled()); + } + @Test + void updateEnabledState_noRealAction_setsEnabledFalse() { + action.updateView(null, mockView); + action.updateEnabledState(); + assertFalse(action.isEnabled()); + } + + @Test + void installViewListeners_addsPropertyChangeListener() { + mockView.getActionMap().put("undo", mockRealAction); + action.installViewListeners(mockView); + verify(mockRealAction).addPropertyChangeListener(any(PropertyChangeListener.class)); + } + @Test + void uninstallViewListeners_removesPropertyChangeListener() { + mockView.getActionMap().put("undo", mockRealAction); + action.uninstallViewListeners(mockView); + verify(mockRealAction).removePropertyChangeListener(any(PropertyChangeListener.class)); + } +} \ No newline at end of file From 5921062fd2365acfae6c49c5c9fdc90e2255388b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertram=20H=C3=B8jlund=20Petersen?= Date: Fri, 10 Jan 2025 09:09:47 +0100 Subject: [PATCH 4/5] Add BDD tests with JGiven --- ...on.edit.AbstractUndoRedoActionBDDTest.json | 161 ++++++++++++++++++ jhotdraw-actions/pom.xml | 18 ++ .../edit/AbstractUndoRedoActionBDDTest.java | 21 +++ .../action/edit/GivenInitialState.java | 48 ++++++ .../action/edit/ThenVerifyOutcome.java | 30 ++++ .../edit/WhenUndoRedoActionIsPerformed.java | 19 +++ .../jhotdraw-samples-misc/pom.xml | 12 +- 7 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 jhotdraw-actions/jgiven-reports/org.jhotdraw.action.edit.AbstractUndoRedoActionBDDTest.json create mode 100644 jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionBDDTest.java create mode 100644 jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/GivenInitialState.java create mode 100644 jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenVerifyOutcome.java create mode 100644 jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/WhenUndoRedoActionIsPerformed.java diff --git a/jhotdraw-actions/jgiven-reports/org.jhotdraw.action.edit.AbstractUndoRedoActionBDDTest.json b/jhotdraw-actions/jgiven-reports/org.jhotdraw.action.edit.AbstractUndoRedoActionBDDTest.json new file mode 100644 index 000000000..406648cd6 --- /dev/null +++ b/jhotdraw-actions/jgiven-reports/org.jhotdraw.action.edit.AbstractUndoRedoActionBDDTest.json @@ -0,0 +1,161 @@ +{ + "className": "org.jhotdraw.action.edit.AbstractUndoRedoActionBDDTest", + "name": "Abstract Undo Redo Action BDD", + "scenarios": [ + { + "className": "org.jhotdraw.action.edit.AbstractUndoRedoActionBDDTest", + "testMethodName": "undo_action_should_delegate_to_real_action", + "description": "undo action should delegate to real action", + "tagIds": [], + "explicitParameters": [], + "derivedParameters": [], + "scenarioCases": [ + { + "caseNr": 1, + "steps": [ + { + "name": "an application is running", + "words": [ + { + "value": "Given", + "isIntroWord": true + }, + { + "value": "an application is running" + } + ], + "status": "PASSED", + "durationInNanos": 914291900, + "depth": 0, + "parentFailed": false + }, + { + "name": "a real action is set", + "words": [ + { + "value": "and", + "isIntroWord": true + }, + { + "value": "a real action is set" + } + ], + "status": "PASSED", + "durationInNanos": 4300600, + "depth": 0, + "parentFailed": false + }, + { + "name": "the undo action is performed", + "words": [ + { + "value": "When", + "isIntroWord": true + }, + { + "value": "the undo action is performed" + } + ], + "status": "PASSED", + "durationInNanos": 323900, + "depth": 0, + "parentFailed": false + }, + { + "name": "the real action should be invoked", + "words": [ + { + "value": "Then", + "isIntroWord": true + }, + { + "value": "the real action should be invoked" + } + ], + "status": "PASSED", + "durationInNanos": 9822700, + "depth": 0, + "parentFailed": false + } + ], + "explicitArguments": [], + "derivedArguments": [], + "status": "SUCCESS", + "durationInNanos": 952868100 + } + ], + "casesAsTable": false, + "durationInNanos": 952868100 + }, + { + "className": "org.jhotdraw.action.edit.AbstractUndoRedoActionBDDTest", + "testMethodName": "undo_action_should_do_nothing_if_no_real_action", + "description": "undo action should do nothing if no real action", + "tagIds": [], + "explicitParameters": [], + "derivedParameters": [], + "scenarioCases": [ + { + "caseNr": 1, + "steps": [ + { + "name": "an application is running", + "words": [ + { + "value": "Given", + "isIntroWord": true + }, + { + "value": "an application is running" + } + ], + "status": "PASSED", + "durationInNanos": 1127300, + "depth": 0, + "parentFailed": false + }, + { + "name": "the undo action is performed", + "words": [ + { + "value": "When", + "isIntroWord": true + }, + { + "value": "the undo action is performed" + } + ], + "status": "PASSED", + "durationInNanos": 391000, + "depth": 0, + "parentFailed": false + }, + { + "name": "the real action should not be invoked", + "words": [ + { + "value": "Then", + "isIntroWord": true + }, + { + "value": "the real action should not be invoked" + } + ], + "status": "PASSED", + "durationInNanos": 197300, + "depth": 0, + "parentFailed": false + } + ], + "explicitArguments": [], + "derivedArguments": [], + "status": "SUCCESS", + "durationInNanos": 2158700 + } + ], + "casesAsTable": false, + "durationInNanos": 2158700 + } + ], + "tagMap": {} +} \ No newline at end of file diff --git a/jhotdraw-actions/pom.xml b/jhotdraw-actions/pom.xml index fe2055bc2..824d8bc90 100644 --- a/jhotdraw-actions/pom.xml +++ b/jhotdraw-actions/pom.xml @@ -36,5 +36,23 @@ RELEASE test + + org.assertj + assertj-core + 3.24.2 + test + + + com.tngtech.jgiven + jgiven-junit + 1.3.0 + test + + + junit + junit + 4.13.1 + test + \ No newline at end of file diff --git a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionBDDTest.java b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionBDDTest.java new file mode 100644 index 000000000..a203eee44 --- /dev/null +++ b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionBDDTest.java @@ -0,0 +1,21 @@ +package org.jhotdraw.action.edit; + +import com.tngtech.jgiven.junit.ScenarioTest; +import org.junit.Test; + +public class AbstractUndoRedoActionBDDTest extends ScenarioTest { + + @Test + public void undo_action_should_delegate_to_real_action() { + given().an_application_is_running().and().a_real_action_is_set(); + when().the_undo_action_is_performed(); + then().the_real_action_should_be_invoked(); + } + + @Test + public void undo_action_should_do_nothing_if_no_real_action() { + given().an_application_is_running(); + when().the_undo_action_is_performed(); + then().the_real_action_should_not_be_invoked(); + } +} diff --git a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/GivenInitialState.java b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/GivenInitialState.java new file mode 100644 index 000000000..bc207ee32 --- /dev/null +++ b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/GivenInitialState.java @@ -0,0 +1,48 @@ +package org.jhotdraw.action.edit; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import org.jhotdraw.api.app.Application; +import org.jhotdraw.api.app.View; +import org.mockito.Mockito; +import org.mockito.Mockito.*; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +import static org.mockito.Mockito.mock; + +public class GivenInitialState extends Stage { + @ProvidedScenarioState + Application mockApp; + + @ProvidedScenarioState + View mockView; + + @ProvidedScenarioState + AbstractUndoRedoAction action; + + @ProvidedScenarioState + ActionEvent mockEvent; + + @ProvidedScenarioState + Action mockRealAction; + + + public GivenInitialState an_application_is_running() { + mockApp = mock(Application.class); + mockView = mock(View.class); + mockEvent = mock(ActionEvent.class); + mockRealAction = mock(Action.class); + Mockito.when(mockView.getActionMap()).thenReturn(new ActionMap()); + action = new AbstractUndoRedoAction(mockApp, mockView, "undo") { + }; + return self(); + } + + public GivenInitialState a_real_action_is_set() { + mockView.getActionMap().put("undo", mockRealAction); + action.updateView(null, mockView); + return self(); + } +} diff --git a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenVerifyOutcome.java b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenVerifyOutcome.java new file mode 100644 index 000000000..0060c0a45 --- /dev/null +++ b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenVerifyOutcome.java @@ -0,0 +1,30 @@ +package org.jhotdraw.action.edit; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import org.jhotdraw.api.app.Application; +import org.jhotdraw.api.app.View; +import org.mockito.Mockito; + +import static org.mockito.Mockito.*; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +public class ThenVerifyOutcome extends Stage { + @ProvidedScenarioState + Action mockRealAction; + + public ThenVerifyOutcome the_real_action_should_be_invoked() { + verify(mockRealAction).actionPerformed(any(ActionEvent.class)); + return self(); + } + + public ThenVerifyOutcome the_real_action_should_not_be_invoked() { + verify(mockRealAction, never()).actionPerformed(any(ActionEvent.class)); + return self(); + } + } + + + diff --git a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/WhenUndoRedoActionIsPerformed.java b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/WhenUndoRedoActionIsPerformed.java new file mode 100644 index 000000000..d27efbbad --- /dev/null +++ b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/WhenUndoRedoActionIsPerformed.java @@ -0,0 +1,19 @@ +package org.jhotdraw.action.edit; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; + +import java.awt.event.ActionEvent; + +public class WhenUndoRedoActionIsPerformed extends Stage { + @ProvidedScenarioState + AbstractUndoRedoAction action; + + @ProvidedScenarioState + ActionEvent mockEvent; + + public WhenUndoRedoActionIsPerformed the_undo_action_is_performed() { + action.actionPerformed(mockEvent); + return self(); + } + } diff --git a/jhotdraw-samples/jhotdraw-samples-misc/pom.xml b/jhotdraw-samples/jhotdraw-samples-misc/pom.xml index ca8104ee5..8b3ddcec3 100644 --- a/jhotdraw-samples/jhotdraw-samples-misc/pom.xml +++ b/jhotdraw-samples/jhotdraw-samples-misc/pom.xml @@ -19,11 +19,7 @@ Quaqua 7.3.4 - - net.sourceforge.htmlunit - htmlunit - 2.37.0 - + org.aspectj aspectjweaver @@ -40,6 +36,12 @@ 4.13.2 test + + org.assertj + assertj-core + 3.24.2 + test + From 02c4f1674cb5f119b005182c7c1ba39860989ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertram=20H=C3=B8jlund=20Petersen?= Date: Fri, 10 Jan 2025 12:09:45 +0100 Subject: [PATCH 5/5] Rename --- .../action/edit/AbstractUndoRedoActionBDDTest.java | 2 +- ...VerifyOutcome.java => ThenUndoActionIsPerformed.java} | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) rename jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/{ThenVerifyOutcome.java => ThenUndoActionIsPerformed.java} (62%) diff --git a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionBDDTest.java b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionBDDTest.java index a203eee44..1215d4151 100644 --- a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionBDDTest.java +++ b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/AbstractUndoRedoActionBDDTest.java @@ -3,7 +3,7 @@ import com.tngtech.jgiven.junit.ScenarioTest; import org.junit.Test; -public class AbstractUndoRedoActionBDDTest extends ScenarioTest { +public class AbstractUndoRedoActionBDDTest extends ScenarioTest { @Test public void undo_action_should_delegate_to_real_action() { diff --git a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenVerifyOutcome.java b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenUndoActionIsPerformed.java similarity index 62% rename from jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenVerifyOutcome.java rename to jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenUndoActionIsPerformed.java index 0060c0a45..57e3595a1 100644 --- a/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenVerifyOutcome.java +++ b/jhotdraw-actions/src/test/java/org/jhotdraw/action/edit/ThenUndoActionIsPerformed.java @@ -2,25 +2,22 @@ import com.tngtech.jgiven.Stage; import com.tngtech.jgiven.annotation.ProvidedScenarioState; -import org.jhotdraw.api.app.Application; -import org.jhotdraw.api.app.View; -import org.mockito.Mockito; import static org.mockito.Mockito.*; import javax.swing.*; import java.awt.event.ActionEvent; -public class ThenVerifyOutcome extends Stage { +public class ThenUndoActionIsPerformed extends Stage { @ProvidedScenarioState Action mockRealAction; - public ThenVerifyOutcome the_real_action_should_be_invoked() { + public ThenUndoActionIsPerformed the_real_action_should_be_invoked() { verify(mockRealAction).actionPerformed(any(ActionEvent.class)); return self(); } - public ThenVerifyOutcome the_real_action_should_not_be_invoked() { + public ThenUndoActionIsPerformed the_real_action_should_not_be_invoked() { verify(mockRealAction, never()).actionPerformed(any(ActionEvent.class)); return self(); }