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