diff --git a/.github/workflows/maven-ci.yml b/.github/workflows/maven-ci.yml
new file mode 100644
index 000000000..7270aa6e9
--- /dev/null
+++ b/.github/workflows/maven-ci.yml
@@ -0,0 +1,55 @@
+name: CI (Maven)
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: read
+ checks: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '21'
+ cache: maven
+ server-id: github
+ server-username: GITHUB_ACTOR
+ server-password: GITHUB_TOKEN
+
+ - name: Build & test (use wrapper if present)
+ run: |
+ if [ -f mvnw ]; then
+ ./mvnw -B -ntp -DskipITs=true verify
+ else
+ mvn -B -ntp -DskipITs=true verify
+ fi
+
+ - name: Upload surefire test report (on failure)
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: surefire-reports
+ path: |
+ **/target/surefire-reports/*.txt
+ **/target/surefire-reports/*.xml
+ **/target/surefire-reports/*.dumpstream
+
+ - name: Publish JUnit results
+ if: always()
+ uses: dorny/test-reporter@v1
+ with:
+ name: Unit tests
+ path: "**/target/surefire-reports/*.xml"
+ reporter: java-junit
diff --git a/.maven-settings.xml b/.maven-settings.xml
new file mode 100644
index 000000000..319b26bae
--- /dev/null
+++ b/.maven-settings.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ github
+ ${env.GITHUB_ACTOR}
+ ${env.GITHUB_TOKEN}
+
+
+
diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml
index 7c276da85..7b8bdde47 100644
--- a/jhotdraw-core/pom.xml
+++ b/jhotdraw-core/pom.xml
@@ -40,5 +40,11 @@
jhotdraw-actions
${project.version}
-
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
\ No newline at end of file
diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/BezierTool.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/BezierTool.java
index 83f8419c6..98de07d4c 100644
--- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/BezierTool.java
+++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/BezierTool.java
@@ -18,6 +18,7 @@
import org.jhotdraw.geom.Bezier;
import org.jhotdraw.geom.BezierPath;
import org.jhotdraw.geom.Geom;
+import org.jhotdraw.undo.CompositeEdit;
import org.jhotdraw.util.*;
/**
@@ -225,7 +226,10 @@ protected void fireUndoEvent(Figure createdFigure, DrawingView creationView) {
final Figure addedFigure = createdFigure;
final Drawing addedDrawing = creationView.getDrawing();
final DrawingView addedView = creationView;
- getDrawing().fireUndoableEditHappened(new AbstractUndoableEdit() {
+
+ CompositeEdit compositeEdit = new CompositeEdit(presentationName);
+
+ UndoableEdit undoableEdit = new AbstractUndoableEdit() {
private static final long serialVersionUID = 1L;
@Override
@@ -246,7 +250,10 @@ public void redo() throws CannotRedoException {
addedDrawing.add(addedFigure);
addedView.addToSelection(addedFigure);
}
- });
+ };
+ compositeEdit.addEdit(undoableEdit);
+ compositeEdit.end();
+ getDrawing().fireUndoableEditHappened(compositeEdit);
}
@Override
diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/ConnectionTool.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/ConnectionTool.java
index d23ba4675..8d05ca6b4 100644
--- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/ConnectionTool.java
+++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/ConnectionTool.java
@@ -16,6 +16,7 @@
import javax.swing.undo.*;
import org.jhotdraw.draw.*;
import org.jhotdraw.draw.connector.Connector;
+import org.jhotdraw.undo.CompositeEdit;
import org.jhotdraw.util.*;
/**
@@ -283,7 +284,9 @@ public void mouseReleased(MouseEvent e) {
createdFigure.changed();
final Figure addedFigure = createdFigure;
final Drawing addedDrawing = getDrawing();
- getDrawing().fireUndoableEditHappened(new AbstractUndoableEdit() {
+
+ CompositeEdit compositeEdit = new CompositeEdit(presentationName);
+ UndoableEdit undoableEdit = new AbstractUndoableEdit() {
private static final long serialVersionUID = 1L;
@Override
@@ -302,7 +305,11 @@ public void redo() throws CannotRedoException {
super.redo();
addedDrawing.add(addedFigure);
}
- });
+ };
+ compositeEdit.addEdit(undoableEdit);
+ compositeEdit.end();
+ getDrawing().fireUndoableEditHappened(compositeEdit);
+
targetFigure = null;
Point2D.Double anchor = startConnector.getAnchor();
Rectangle r = new Rectangle(getView().drawingToView(anchor));
diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/CreationTool.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/CreationTool.java
index c7e07706f..934f5d04e 100644
--- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/CreationTool.java
+++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/tool/CreationTool.java
@@ -15,6 +15,7 @@
import java.util.*;
import javax.swing.undo.*;
import org.jhotdraw.draw.*;
+import org.jhotdraw.undo.CompositeEdit;
import org.jhotdraw.util.*;
/**
@@ -60,7 +61,7 @@ public class CreationTool extends AbstractTool {
* Attributes to be applied to the created ConnectionFigure. These attributes override the
* default attributes of the DrawingEditor.
*/
- protected Map, Object> prototypeAttributes;
+ protected transient Map, Object> prototypeAttributes;
/**
* A localized name for this tool. The presentationName is displayed by the UndoableEdit.
*/
@@ -239,7 +240,9 @@ public void mouseReleased(MouseEvent evt) {
}
final Figure addedFigure = createdFigure;
final Drawing addedDrawing = getDrawing();
- getDrawing().fireUndoableEditHappened(new AbstractUndoableEdit() {
+ CompositeEdit compositeEdit = new CompositeEdit(presentationName);
+
+ UndoableEdit edit = new AbstractUndoableEdit() {
private static final long serialVersionUID = 1L;
@Override
@@ -258,7 +261,12 @@ public void redo() throws CannotRedoException {
super.redo();
addedDrawing.add(addedFigure);
}
- });
+ };
+
+ compositeEdit.addEdit(edit);
+ compositeEdit.end();
+ getDrawing().fireUndoableEditHappened(compositeEdit);
+
Rectangle r = new Rectangle(anchor.x, anchor.y, 0, 0);
r.add(evt.getX(), evt.getY());
maybeFireBoundsInvalidated(r);
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/BezierToolTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/BezierToolTest.java
new file mode 100644
index 000000000..98394ee3b
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/BezierToolTest.java
@@ -0,0 +1,117 @@
+package org.jhotdraw.draw.tool;
+
+import org.jhotdraw.undo.UndoRedoManager;
+import org.junit.*;
+import javax.swing.undo.*;
+import java.util.Locale;
+import static org.junit.Assert.*;
+
+public class BezierToolTest {
+
+ private UndoRedoManager undoManager;
+
+ @BeforeClass
+ public static void setUpClass() {
+ Locale.setDefault(Locale.ENGLISH);
+ }
+
+ @Before
+ public void setUp() {
+ undoManager = new UndoRedoManager();
+ }
+
+ @Test
+ public void testEditAddedToHistory() {
+ MockEdit edit = new MockEdit("Create Bezier Figure");
+ undoManager.addEdit(edit);
+
+ assertTrue(undoManager.canUndo());
+ assertEquals("Create Bezier Figure", edit.getPresentationName());
+ }
+
+ @Test
+ public void testMultipleCreationsPreserveHistory() {
+ MockEdit edit1 = new MockEdit("Create Bezier 1");
+ undoManager.addEdit(edit1);
+
+ MockEdit edit2 = new MockEdit("Create Bezier 2");
+ undoManager.addEdit(edit2);
+
+ assertTrue(undoManager.canUndo());
+
+ undoManager.undo();
+ assertTrue(edit2.wasUndone());
+
+ assertTrue(undoManager.canUndo());
+ undoManager.undo();
+ assertTrue(edit1.wasUndone());
+ }
+
+ @Test
+ public void testChronologicalUndoOrder() {
+ MockEdit[] edits = new MockEdit[3];
+ for (int i = 0; i < 3; i++) {
+ edits[i] = new MockEdit("Bezier Edit " + (i + 1));
+ undoManager.addEdit(edits[i]);
+ }
+
+ for (int i = 2; i >= 0; i--) {
+ assertTrue(undoManager.canUndo());
+ undoManager.undo();
+ assertTrue(edits[i].wasUndone());
+ }
+
+ assertFalse(undoManager.canUndo());
+ }
+
+ @Test
+ public void testRedoAfterUndo() {
+ MockEdit edit = new MockEdit("Bezier Undo Test");
+ undoManager.addEdit(edit);
+
+ undoManager.undo();
+ assertTrue(edit.wasUndone());
+ assertTrue(undoManager.canRedo());
+
+ undoManager.redo();
+ assertTrue(edit.wasRedone());
+ }
+
+ private static class MockEdit extends AbstractUndoableEdit {
+ private static final long serialVersionUID = 1L;
+ private final String name;
+ private boolean undone = false;
+ private boolean redone = false;
+
+ public MockEdit(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getPresentationName() {
+ return name;
+ }
+
+ @Override
+ public void undo() throws CannotUndoException {
+ super.undo();
+ undone = true;
+ redone = false;
+ }
+
+ @Override
+ public void redo() throws CannotRedoException {
+ super.redo();
+ redone = true;
+ undone = false;
+ }
+
+ public boolean wasUndone() {
+ return undone;
+ }
+
+ public boolean wasRedone() {
+ return redone;
+ }
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ConnectionToolTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ConnectionToolTest.java
new file mode 100644
index 000000000..c0dbb799b
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/ConnectionToolTest.java
@@ -0,0 +1,117 @@
+package org.jhotdraw.draw.tool;
+
+import org.jhotdraw.undo.UndoRedoManager;
+import org.junit.*;
+import javax.swing.undo.*;
+import java.util.Locale;
+import static org.junit.Assert.*;
+
+public class ConnectionToolTest {
+
+ private UndoRedoManager undoManager;
+
+ @BeforeClass
+ public static void setUpClass() {
+ Locale.setDefault(Locale.ENGLISH);
+ }
+
+ @Before
+ public void setUp() {
+ undoManager = new UndoRedoManager();
+ }
+
+ @Test
+ public void testEditAddedToHistory() {
+ MockEdit edit = new MockEdit("Create Connection");
+ undoManager.addEdit(edit);
+
+ assertTrue(undoManager.canUndo());
+ assertEquals("Create Connection", edit.getPresentationName());
+ }
+
+ @Test
+ public void testMultipleCreationsPreserveHistory() {
+ MockEdit edit1 = new MockEdit("Create Connection 1");
+ undoManager.addEdit(edit1);
+
+ MockEdit edit2 = new MockEdit("Create Connection 2");
+ undoManager.addEdit(edit2);
+
+ assertTrue(undoManager.canUndo());
+
+ undoManager.undo();
+ assertTrue(edit2.wasUndone());
+
+ assertTrue(undoManager.canUndo());
+ undoManager.undo();
+ assertTrue(edit1.wasUndone());
+ }
+
+ @Test
+ public void testChronologicalUndoOrder() {
+ MockEdit[] edits = new MockEdit[3];
+ for (int i = 0; i < 3; i++) {
+ edits[i] = new MockEdit("Connection " + (i + 1));
+ undoManager.addEdit(edits[i]);
+ }
+
+ for (int i = 2; i >= 0; i--) {
+ assertTrue(undoManager.canUndo());
+ undoManager.undo();
+ assertTrue(edits[i].wasUndone());
+ }
+
+ assertFalse(undoManager.canUndo());
+ }
+
+ @Test
+ public void testRedoAfterUndo() {
+ MockEdit edit = new MockEdit("Test Connection");
+ undoManager.addEdit(edit);
+
+ undoManager.undo();
+ assertTrue(edit.wasUndone());
+ assertTrue(undoManager.canRedo());
+
+ undoManager.redo();
+ assertTrue(edit.wasRedone());
+ }
+
+ private static class MockEdit extends AbstractUndoableEdit {
+ private static final long serialVersionUID = 1L;
+ private final String name;
+ private boolean undone = false;
+ private boolean redone = false;
+
+ public MockEdit(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getPresentationName() {
+ return name;
+ }
+
+ @Override
+ public void undo() throws CannotUndoException {
+ super.undo();
+ undone = true;
+ redone = false;
+ }
+
+ @Override
+ public void redo() throws CannotRedoException {
+ super.redo();
+ redone = true;
+ undone = false;
+ }
+
+ public boolean wasUndone() {
+ return undone;
+ }
+
+ public boolean wasRedone() {
+ return redone;
+ }
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/CreationToolTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/CreationToolTest.java
new file mode 100644
index 000000000..c5af799c8
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/tool/CreationToolTest.java
@@ -0,0 +1,119 @@
+package org.jhotdraw.draw.tool;
+
+import org.jhotdraw.undo.UndoRedoManager;
+import org.junit.*;
+import javax.swing.undo.*;
+import java.util.Locale;
+import static org.junit.Assert.*;
+
+public class CreationToolTest {
+
+ private UndoRedoManager undoManager;
+
+ @BeforeClass
+ public static void setUpClass() {
+ Locale.setDefault(Locale.ENGLISH);
+ }
+
+ @Before
+ public void setUp() {
+ undoManager = new UndoRedoManager();
+ }
+
+
+ @Test
+ public void testEditAddedToHistory() {
+ MockEdit edit = new MockEdit("Create Figure");
+ undoManager.addEdit(edit);
+
+ assertTrue("Should be able to undo after creation", undoManager.canUndo());
+ assertEquals("Undo name should match", "Create Figure", edit.getPresentationName());
+ }
+
+ @Test
+ public void testMultipleCreationsPreserveHistory() {
+ MockEdit edit1 = new MockEdit("Create Rectangle");
+ undoManager.addEdit(edit1);
+
+ MockEdit edit2 = new MockEdit("Create Ellipse");
+ undoManager.addEdit(edit2);
+
+ assertTrue("Should be able to undo", undoManager.canUndo());
+
+ undoManager.undo();
+ assertTrue("Edit 2 should be undone", edit2.wasUndone());
+
+ assertTrue("Should still be able to undo first edit", undoManager.canUndo());
+ undoManager.undo();
+ assertTrue("Edit 1 should be undone", edit1.wasUndone());
+ }
+
+ @Test
+ public void testChronologicalUndoOrder() {
+ MockEdit[] edits = new MockEdit[3];
+ for (int i = 0; i < 3; i++) {
+ edits[i] = new MockEdit("Edit " + (i + 1));
+ undoManager.addEdit(edits[i]);
+ }
+
+ for (int i = 2; i >= 0; i--) {
+ assertTrue("Should be able to undo edit " + (i + 1), undoManager.canUndo());
+ undoManager.undo();
+ assertTrue("Edit " + (i + 1) + " should be undone", edits[i].wasUndone());
+ }
+
+ assertFalse("Should not be able to undo anymore", undoManager.canUndo());
+ }
+
+
+ @Test
+ public void testRedoAfterUndo() {
+ MockEdit edit = new MockEdit("Test Edit");
+ undoManager.addEdit(edit);
+
+ undoManager.undo();
+ assertTrue("Edit should be undone", edit.wasUndone());
+ assertTrue("Should be able to redo", undoManager.canRedo());
+
+ undoManager.redo();
+ assertTrue("Edit should be redone", edit.wasRedone());
+ }
+
+ private static class MockEdit extends AbstractUndoableEdit {
+ private static final long serialVersionUID = 1L;
+ private final String name;
+ private boolean undone = false;
+ private boolean redone = false;
+
+ public MockEdit(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getPresentationName() {
+ return name;
+ }
+
+ @Override
+ public void undo() throws CannotUndoException {
+ super.undo();
+ undone = true;
+ redone = false;
+ }
+
+ @Override
+ public void redo() throws CannotRedoException {
+ super.redo();
+ redone = true;
+ undone = false;
+ }
+
+ public boolean wasUndone() {
+ return undone;
+ }
+
+ public boolean wasRedone() {
+ return redone;
+ }
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-utils/pom.xml b/jhotdraw-utils/pom.xml
index b1b2faf01..3eecbe1c3 100644
--- a/jhotdraw-utils/pom.xml
+++ b/jhotdraw-utils/pom.xml
@@ -15,5 +15,62 @@
6.8.21
test
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
+ com.tngtech.jgiven
+ jgiven-junit
+ 1.3.1
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.24.2
+ test
+
+
+ net.bytebuddy
+ byte-buddy
+ 1.14.10
+ test
+
+
+ net.bytebuddy
+ byte-buddy-agent
+ 1.14.10
+ test
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ --add-opens java.base/java.lang=ALL-UNNAMED
+ --add-opens java.base/java.util=ALL-UNNAMED
+
+
+
+
+
\ No newline at end of file
diff --git a/jhotdraw-utils/src/main/java/org/jhotdraw/undo/RedoAction.java b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/RedoAction.java
new file mode 100644
index 000000000..6b4eeed10
--- /dev/null
+++ b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/RedoAction.java
@@ -0,0 +1,38 @@
+package org.jhotdraw.undo;
+
+import javax.swing.*;
+import javax.swing.undo.CannotRedoException;
+import java.awt.event.ActionEvent;
+import java.util.logging.Logger;
+
+import static org.jhotdraw.undo.UndoRedoManager.getLabels;
+/**
+ * Redo Action for use in a menu bar.
+ */
+
+public class RedoAction extends AbstractAction {
+
+ private static final long serialVersionUID = 1L;
+
+ private final UndoRedoManager undoManager;
+
+ private transient Logger logger = Logger.getLogger(RedoAction.class.getName());
+
+ public RedoAction(UndoRedoManager undoManager) {
+ this.undoManager = undoManager;
+ getLabels().configureAction(this, "edit.redo");
+ setEnabled(false);
+ }
+
+ /**
+ * Invoked when an action occurs.
+ */
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ try {
+ undoManager.redo();
+ } catch (CannotRedoException e) {
+ logger.warning("Can't redo: " + e);
+ }
+ }
+}
diff --git a/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoAction.java b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoAction.java
new file mode 100644
index 000000000..5ff066cc1
--- /dev/null
+++ b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoAction.java
@@ -0,0 +1,40 @@
+package org.jhotdraw.undo;
+
+import javax.swing.*;
+import javax.swing.undo.CannotUndoException;
+import java.awt.event.ActionEvent;
+import java.util.logging.Logger;
+
+import static org.jhotdraw.undo.UndoRedoManager.getLabels;
+
+/**
+ * Undo Action for use in a menu bar.
+ */
+public class UndoAction extends AbstractAction {
+
+ private static final long serialVersionUID = 1L;
+
+ private final UndoRedoManager undoRedoManager;
+
+ private transient Logger logger = Logger.getLogger(UndoAction.class.getName());
+
+
+ public UndoAction(UndoRedoManager undoRedoManager) {
+ this.undoRedoManager = undoRedoManager;
+ getLabels().configureAction(this, "edit.undo");
+ setEnabled(false);
+ }
+
+ /**
+ * Invoked when an action occurs.
+ */
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ try {
+ undoRedoManager.undo();
+ } catch (CannotUndoException e) {
+ logger.warning("Can't undo: " + e);
+ }
+ }
+ }
+
diff --git a/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoRedoManager.java b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoRedoManager.java
index 40ac0b4ce..bd06b6c36 100644
--- a/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoRedoManager.java
+++ b/jhotdraw-utils/src/main/java/org/jhotdraw/undo/UndoRedoManager.java
@@ -9,7 +9,8 @@
import java.awt.event.*;
import java.beans.*;
-import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.undo.*;
import org.jhotdraw.util.*;
@@ -21,8 +22,9 @@
* @author Werner Randelshofer
* @version $Id$
*/
-public class UndoRedoManager extends UndoManager { //javax.swing.undo.UndoManager {
+public class UndoRedoManager extends UndoManager {
+ private transient Logger logger = Logger.getLogger(UndoRedoManager.class.getName());
private static final long serialVersionUID = 1L;
protected PropertyChangeSupport propertySupport = new PropertyChangeSupport(this);
private static final boolean DEBUG = false;
@@ -48,6 +50,7 @@ public class UndoRedoManager extends UndoManager { //javax.swing.undo.UndoManage
* Sending this UndoableEdit event to the UndoRedoManager
* disables the Undo and Redo functions of the manager.
*/
+
public static final UndoableEdit DISCARD_ALL_EDITS = new AbstractUndoableEdit() {
private static final long serialVersionUID = 1L;
@@ -62,58 +65,6 @@ public boolean canRedo() {
}
};
- /**
- * Undo Action for use in a menu bar.
- */
- private class UndoAction
- extends AbstractAction {
-
- private static final long serialVersionUID = 1L;
-
- public UndoAction() {
- labels.configureAction(this, "edit.undo");
- setEnabled(false);
- }
-
- /**
- * Invoked when an action occurs.
- */
- @Override
- public void actionPerformed(ActionEvent evt) {
- try {
- undo();
- } catch (CannotUndoException e) {
- System.err.println("Cannot undo: " + e);
- e.printStackTrace();
- }
- }
- }
-
- /**
- * Redo Action for use in a menu bar.
- */
- private class RedoAction
- extends AbstractAction {
-
- private static final long serialVersionUID = 1L;
-
- public RedoAction() {
- labels.configureAction(this, "edit.redo");
- setEnabled(false);
- }
-
- /**
- * Invoked when an action occurs.
- */
- @Override
- public void actionPerformed(ActionEvent evt) {
- try {
- redo();
- } catch (CannotRedoException e) {
- System.out.println("Cannot redo: " + e);
- }
- }
- }
/**
* The undo action instance.
*/
@@ -135,21 +86,18 @@ public static ResourceBundleUtil getLabels() {
*/
public UndoRedoManager() {
getLabels();
- undoAction = new UndoAction();
- redoAction = new RedoAction();
- }
-
- public void setLocale(Locale l) {
- labels = ResourceBundleUtil.getBundle("org.jhotdraw.undo.Labels", l);
+ undoAction = new UndoAction(this);
+ redoAction = new RedoAction(this);
}
/**
* Discards all edits.
*/
@Override
- public void discardAllEdits() {
+ public synchronized void discardAllEdits() {
super.discardAllEdits();
- updateActions();
+ updateUndoAction();
+ updateRedoAction();
setHasSignificantEdits(false);
}
@@ -186,16 +134,17 @@ public boolean hasSignificantEdits() {
* @see CompoundEdit#addEdit
*/
@Override
- public boolean addEdit(UndoableEdit anEdit) {
+ public synchronized boolean addEdit(UndoableEdit anEdit) {
if (DEBUG) {
- System.out.println("UndoRedoManager@" + hashCode() + ".add " + anEdit);
+ logger.log(Level.FINE, () -> "UndoRedoManager@" + hashCode() + ".add " + anEdit);
}
if (undoOrRedoInProgress) {
anEdit.die();
return true;
}
boolean success = super.addEdit(anEdit);
- updateActions();
+ updateUndoAction();
+ updateRedoAction();
if (success && anEdit.isSignificant() && editToBeUndone() == anEdit) {
setHasSignificantEdits(true);
}
@@ -218,33 +167,36 @@ public Action getRedoAction() {
/**
* Updates the properties of the UndoAction
- * and of the RedoAction.
*/
- private void updateActions() {
- String label;
- if (DEBUG) {
- System.out.println("UndoRedoManager@" + hashCode() + ".updateActions "
- + editToBeUndone()
- + " canUndo=" + canUndo() + " canRedo=" + canRedo());
- }
+ private void updateUndoAction() {
if (canUndo()) {
undoAction.setEnabled(true);
- label = getUndoPresentationName();
+ setActionValues(undoAction, getUndoPresentationName());
} else {
undoAction.setEnabled(false);
- label = labels.getString("edit.undo.text");
+ setActionValues(undoAction, labels.getString("edit.undo.text"));
}
- undoAction.putValue(Action.NAME, label);
- undoAction.putValue(Action.SHORT_DESCRIPTION, label);
+ }
+
+ /**
+ * Updates the properties of the RedoAction
+ */
+ private void updateRedoAction() {
if (canRedo()) {
redoAction.setEnabled(true);
- label = getRedoPresentationName();
+ setActionValues(redoAction, getRedoPresentationName());
} else {
redoAction.setEnabled(false);
- label = labels.getString("edit.redo.text");
+ setActionValues(redoAction, labels.getString("edit.redo.text"));
}
- redoAction.putValue(Action.NAME, label);
- redoAction.putValue(Action.SHORT_DESCRIPTION, label);
+ }
+
+ /**
+ * Helper method to remove duplication for Action.NAME and SHORT_DESCRIPTION
+ */
+ private void setActionValues(AbstractAction action, String label) {
+ action.putValue(Action.NAME, label);
+ action.putValue(Action.SHORT_DESCRIPTION, label);
}
/**
@@ -260,7 +212,8 @@ public void undo()
super.undo();
} finally {
undoOrRedoInProgress = false;
- updateActions();
+ updateUndoAction();
+ updateRedoAction();
}
}
@@ -277,7 +230,8 @@ public void redo()
super.redo();
} finally {
undoOrRedoInProgress = false;
- updateActions();
+ updateUndoAction();
+ updateRedoAction();
}
}
@@ -294,7 +248,8 @@ public void undoOrRedo()
super.undoOrRedo();
} finally {
undoOrRedoInProgress = false;
- updateActions();
+ updateUndoAction();
+ updateRedoAction();
}
}
diff --git a/jhotdraw-utils/src/main/resources/org/jhotdraw/undo/Labels.properties b/jhotdraw-utils/src/main/resources/org/jhotdraw/undo/Labels.properties
new file mode 100644
index 000000000..702140d13
--- /dev/null
+++ b/jhotdraw-utils/src/main/resources/org/jhotdraw/undo/Labels.properties
@@ -0,0 +1,2 @@
+edit.undo.text=Undo
+edit.redo.text=Redo
\ No newline at end of file
diff --git a/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/GivenUndoRedo.java b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/GivenUndoRedo.java
new file mode 100644
index 000000000..fda1e4f3a
--- /dev/null
+++ b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/GivenUndoRedo.java
@@ -0,0 +1,19 @@
+package org.jhotdraw.undo.BDD;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ProvidedScenarioState;
+import org.jhotdraw.undo.UndoRedoManager;
+import javax.swing.undo.AbstractUndoableEdit;
+
+public class GivenUndoRedo extends Stage {
+ @ProvidedScenarioState
+ private UndoRedoManager undoRedoManager;
+
+ public GivenUndoRedo i_have_edited_a_figure() {
+ undoRedoManager = new UndoRedoManager();
+ undoRedoManager.addEdit(new AbstractUndoableEdit() {
+ @Override public String getPresentationName() { return "Edit Figure"; }
+ });
+ return self();
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/ThenUndoRedo.java b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/ThenUndoRedo.java
new file mode 100644
index 000000000..4d707aab1
--- /dev/null
+++ b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/ThenUndoRedo.java
@@ -0,0 +1,27 @@
+package org.jhotdraw.undo.BDD;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ExpectedScenarioState;
+import org.jhotdraw.undo.UndoRedoManager;
+import static org.junit.Assert.*;
+
+public class ThenUndoRedo extends Stage {
+
+ @ExpectedScenarioState
+ UndoRedoManager undoRedoManager;
+
+ public ThenUndoRedo the_undo_and_redo_history_is_preserved_in_chronological_order() {
+ assertEquals("Undo Create Figure", undoRedoManager.getUndoPresentationName());
+ undoRedoManager.undo();
+
+ assertEquals("Undo Edit Figure", undoRedoManager.getUndoPresentationName());
+ undoRedoManager.undo();
+
+ assertEquals("Redo Edit Figure", undoRedoManager.getRedoPresentationName());
+ undoRedoManager.redo();
+ assertEquals("Redo Create Figure", undoRedoManager.getRedoPresentationName());
+ undoRedoManager.redo();
+
+ return self();
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/UndoRedoScenarioTest.java b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/UndoRedoScenarioTest.java
new file mode 100644
index 000000000..465ddd0af
--- /dev/null
+++ b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/UndoRedoScenarioTest.java
@@ -0,0 +1,16 @@
+package org.jhotdraw.undo.BDD;
+
+import com.tngtech.jgiven.junit.ScenarioTest;
+import org.junit.Test;
+
+public class UndoRedoScenarioTest extends ScenarioTest {
+
+ @Test
+ public void history_must_be_preserved_in_chronological_order() {
+ given().i_have_edited_a_figure();
+
+ when().i_create_a_new_figure();
+
+ then().the_undo_and_redo_history_is_preserved_in_chronological_order();
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/WhenUndoRedo.java b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/WhenUndoRedo.java
new file mode 100644
index 000000000..c533f7117
--- /dev/null
+++ b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/BDD/WhenUndoRedo.java
@@ -0,0 +1,19 @@
+package org.jhotdraw.undo.BDD;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ExpectedScenarioState;
+import org.jhotdraw.undo.UndoRedoManager;
+import javax.swing.undo.AbstractUndoableEdit;
+
+public class WhenUndoRedo extends Stage {
+ @ExpectedScenarioState
+ private UndoRedoManager undoRedoManager;
+
+ public WhenUndoRedo i_create_a_new_figure() {
+ // Perform the next chronological action
+ undoRedoManager.addEdit(new AbstractUndoableEdit() {
+ @Override public String getPresentationName() { return "Create Figure"; }
+ });
+ return self();
+ }
+}
\ No newline at end of file
diff --git a/jhotdraw-utils/src/test/java/org/jhotdraw/undo/UndoRedoManagerTest.java b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/UndoRedoManagerTest.java
new file mode 100644
index 000000000..083bf4b1c
--- /dev/null
+++ b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/UndoRedoManagerTest.java
@@ -0,0 +1,132 @@
+package org.jhotdraw.undo;
+
+import org.junit.*;
+import javax.swing.undo.*;
+import static org.junit.Assert.*;
+
+/**
+ * Minimal tests for UndoRedoManager functionality
+ * These tests verify the core undo/redo mechanism works correctly
+ */
+public class UndoRedoManagerTest {
+
+ @Test
+ public void testUndoManagerBasicFunctionality() {
+ UndoManager manager = new UndoManager();
+
+ MockEdit edit = new MockEdit("Test Edit");
+ manager.addEdit(edit);
+
+ assertTrue("Should be able to undo", manager.canUndo());
+ assertFalse("Should not be able to redo", manager.canRedo());
+
+ manager.undo();
+ assertTrue("Edit should be undone", edit.wasUndone());
+ assertFalse("Should not be able to undo anymore", manager.canUndo());
+ assertTrue("Should be able to redo", manager.canRedo());
+ }
+
+ @Test
+ public void testMultipleEditsChronologicalOrder() {
+ UndoManager manager = new UndoManager();
+
+ MockEdit edit1 = new MockEdit("Edit 1");
+ MockEdit edit2 = new MockEdit("Edit 2");
+ MockEdit edit3 = new MockEdit("Edit 3");
+
+ manager.addEdit(edit1);
+ manager.addEdit(edit2);
+ manager.addEdit(edit3);
+
+
+ manager.undo();
+ assertTrue("Edit 3 should be undone", edit3.wasUndone());
+ assertFalse("Edit 2 should not be undone", edit2.wasUndone());
+
+ manager.undo();
+ assertTrue("Edit 2 should be undone", edit2.wasUndone());
+ assertFalse("Edit 1 should not be undone", edit1.wasUndone());
+
+ manager.undo();
+ assertTrue("Edit 1 should be undone", edit1.wasUndone());
+ }
+
+ @Test
+ public void testRedoFunctionality() {
+ UndoManager manager = new UndoManager();
+
+ MockEdit edit = new MockEdit("Test Edit");
+ manager.addEdit(edit);
+ manager.undo();
+
+ assertTrue("Should be able to redo", manager.canRedo());
+ manager.redo();
+ assertTrue("Edit should be redone", edit.wasRedone());
+ assertFalse("Should not be able to redo anymore", manager.canRedo());
+ }
+
+ @Test
+ public void testDiscardAllEdits() {
+ UndoManager manager = new UndoManager();
+
+ manager.addEdit(new MockEdit("Edit 1"));
+ manager.addEdit(new MockEdit("Edit 2"));
+
+ assertTrue("Should be able to undo", manager.canUndo());
+
+ manager.discardAllEdits();
+
+ assertFalse("Should not be able to undo after discard", manager.canUndo());
+ assertFalse("Should not be able to redo after discard", manager.canRedo());
+ }
+
+ @Test(expected = CannotUndoException.class)
+ public void testUndoWhenEmpty() {
+ UndoManager manager = new UndoManager();
+ manager.undo();
+ }
+
+ @Test(expected = CannotRedoException.class)
+ public void testRedoWhenEmpty() {
+ UndoManager manager = new UndoManager();
+ manager.redo();
+ }
+
+ private static class MockEdit extends AbstractUndoableEdit {
+ private static final long serialVersionUID = 1L;
+ private final String name;
+ private boolean undone = false;
+ private boolean redone = false;
+
+ public MockEdit(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getPresentationName() {
+ return name;
+ }
+
+ @Override
+ public void undo() throws CannotUndoException {
+ super.undo();
+ undone = true;
+ redone = false;
+ }
+
+ @Override
+ public void redo() throws CannotRedoException {
+ super.redo();
+ redone = true;
+ undone = false;
+ }
+
+ public boolean wasUndone() {
+ return undone;
+ }
+
+ public boolean wasRedone() {
+ return redone;
+ }
+ }
+}
\ No newline at end of file