diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 000000000..d53a3f0d6 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,33 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI with Maven + +on: + push: + branches: [ "Kornelia" ] + pull_request: + branches: [ "Kornelia" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + +#test \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..308dbda25 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,260 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "type": "java", + "name": "Current File", + "request": "launch", + "mainClass": "${file}" + }, + { + "type": "java", + "name": "CIELABColorSpace", + "request": "launch", + "mainClass": "org.jhotdraw.color.CIELABColorSpace", + "projectName": "jhotdraw-gui" + }, + { + "type": "java", + "name": "CIELCHabColorSpace", + "request": "launch", + "mainClass": "org.jhotdraw.color.CIELCHabColorSpace", + "projectName": "jhotdraw-gui" + }, + { + "type": "java", + "name": "EditCanvasPanel", + "request": "launch", + "mainClass": "org.jhotdraw.gui.action.EditCanvasPanel", + "projectName": "jhotdraw-gui" + }, + { + "type": "java", + "name": "CIEXYChromaticityDiagram", + "request": "launch", + "mainClass": "org.jhotdraw.samples.color.CIEXYChromaticityDiagram", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "JMixer", + "request": "launch", + "mainClass": "org.jhotdraw.samples.color.JMixer", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "WheelsAndSlidersMain", + "request": "launch", + "mainClass": "org.jhotdraw.samples.color.WheelsAndSlidersMain", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "FontChooserMain", + "request": "launch", + "mainClass": "org.jhotdraw.samples.font.FontChooserMain", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "ActivityMonitorSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.ActivityMonitorSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "AnimationSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.AnimationSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "BezierDemo", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.BezierDemo", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "ConnectingFiguresSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.ConnectingFiguresSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "CreationToolSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.CreationToolSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "DefaultDOMStorableSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.DefaultDOMStorableSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "DnDMultiEditorSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.DnDMultiEditorSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "EditorSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.EditorSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "FileIconsSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.FileIconsSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "LabeledLineConnectionFigureSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.LabeledLineConnectionFigureSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "LayouterSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.LayouterSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "MovableChildFiguresSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.MovableChildFiguresSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "MovableChildFiguresSampleWithAbstractDrawingView", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.MovableChildFiguresSampleWithAbstractDrawingView", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "MovableChildFiguresSampleWithDelegatorDrawingView", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.MovableChildFiguresSampleWithDelegatorDrawingView", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "MultiEditorSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.MultiEditorSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "SVGDrawingPanelSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.SVGDrawingPanelSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "SelectionToolSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.SelectionToolSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "SmartConnectionFigureSample", + "request": "launch", + "mainClass": "org.jhotdraw.samples.mini.SmartConnectionFigureSample", + "projectName": "jhotdraw-samples-mini" + }, + { + "type": "java", + "name": "Main", + "request": "launch", + "mainClass": "org.jhotdraw.samples.draw.Main", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "Main(1)", + "request": "launch", + "mainClass": "org.jhotdraw.samples.net.Main", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "NetApplet", + "request": "launch", + "mainClass": "org.jhotdraw.samples.net.NetApplet", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "Main(2)", + "request": "launch", + "mainClass": "org.jhotdraw.samples.odg.Main", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "Main(3)", + "request": "launch", + "mainClass": "org.jhotdraw.samples.pert.Main", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "PertApplet", + "request": "launch", + "mainClass": "org.jhotdraw.samples.pert.PertApplet", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "Main(4)", + "request": "launch", + "mainClass": "org.jhotdraw.samples.svg.Main", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "SVGApplet", + "request": "launch", + "mainClass": "org.jhotdraw.samples.svg.SVGApplet", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "Main(5)", + "request": "launch", + "mainClass": "org.jhotdraw.samples.teddy.Main", + "projectName": "jhotdraw-samples-misc" + }, + { + "type": "java", + "name": "Bezier", + "request": "launch", + "mainClass": "org.jhotdraw.geom.Bezier", + "projectName": "jhotdraw-utils" + } + ] +} \ No newline at end of file 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..6f0d4bac7 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 @@ -36,15 +36,17 @@ public class UndoAction extends AbstractViewAction { 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(); - } + private transient PropertyChangeListener redoActionPropertyListener = 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(); } }; diff --git a/jhotdraw-utils/pom.xml b/jhotdraw-utils/pom.xml index b1b2faf01..1f33e837c 100644 --- a/jhotdraw-utils/pom.xml +++ b/jhotdraw-utils/pom.xml @@ -15,5 +15,48 @@ 6.8.21 test + + + junit + junit + 4.13.2 + test + + + + com.tngtech.jgiven + jgiven-junit + 1.3.1 + test + + + + + org.assertj + assertj-core + 3.25.3 + test + + + + + + + com.tngtech.jgiven + jgiven-maven-plugin + 1.3.1 + + + + report + + + + + html + + + + \ 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..2ba07234a --- /dev/null +++ b/jhotdraw-utils/src/test/java/org/jhotdraw/undo/UndoRedoManagerTest.java @@ -0,0 +1,98 @@ +package org.jhotdraw.undo; + +import org.junit.*; +import javax.swing.undo.*; +import static org.junit.Assert.*; + + +public class UndoRedoManagerTest { + + private UndoRedoManager manager; + + + private static class SimpleEdit extends AbstractUndoableEdit { + private static final long serialVersionUID = 1L; + + } + + @Before + public void setUp() { + manager = new UndoRedoManager(); + } + + @After + public void tearDown() { + manager.discardAllEdits(); + } + + + // Test 1 — BEST CASE + // Adding a significant edit should make undo available. + + @Test + public void testAddEdit_EnablesUndo() { + // Initially nothing to undo + assertFalse("Nothing to undo on a fresh manager", manager.canUndo()); + + manager.addEdit(new SimpleEdit()); + + // After adding an edit, undo must be possible + assertTrue("Undo should be available after adding an edit", manager.canUndo()); + // And the undo action button should be enabled + assertTrue("UndoAction should be enabled", manager.getUndoAction().isEnabled()); + // Redo has nothing to replay yet + assertFalse("Redo should NOT be available before any undo", manager.canRedo()); + } + + + // Test 2 — BEST CASE + // After undo, redo becomes available; after redo, undo is back. + + @Test + public void testUndoThenRedo_TogglesAvailability() { + manager.addEdit(new SimpleEdit()); + + // --- undo --- + manager.undo(); + assertFalse("Undo should NOT be available after undoing the only edit", + manager.canUndo()); + assertTrue("Redo should be available after undo", manager.canRedo()); + assertTrue("RedoAction should be enabled", manager.getRedoAction().isEnabled()); + + // --- redo --- + manager.redo(); + assertTrue("Undo should be available again after redo", manager.canUndo()); + assertFalse("Redo should NOT be available after redo", manager.canRedo()); + } + + // Test 3 — BOUNDARY CASE + // Calling undo on an empty manager must throw CannotUndoException. + + @Test(expected = CannotUndoException.class) + public void testUndo_OnEmptyManager_ThrowsException() { + // No edits added — undo must always throw here + manager.undo(); + } + + // + // Test 4 — BOUNDARY CASE + // discardAllEdits resets everything: no undo, no redo, and hasSignificantEdits goes back to false. + + @Test + public void testDiscardAllEdits_ResetsState() { + manager.addEdit(new SimpleEdit()); + assertTrue("Sanity check: should have significant edits", manager.hasSignificantEdits()); + + manager.discardAllEdits(); + + // All three invariants must hold after discard + assertFalse("canUndo must be false after discard", manager.canUndo()); + assertFalse("canRedo must be false after discard", manager.canRedo()); + assertFalse("hasSignificantEdits must be false after discard", + manager.hasSignificantEdits()); + assertFalse("UndoAction must be disabled after discard", + manager.getUndoAction().isEnabled()); + assertFalse("RedoAction must be disabled after discard", + manager.getRedoAction().isEnabled()); + } +} \ No newline at end of file diff --git a/jhotdraw-utils/src/test/resources/org/jhotdraw/undo/Labels.properties b/jhotdraw-utils/src/test/resources/org/jhotdraw/undo/Labels.properties new file mode 100644 index 000000000..ef3c9d4ce --- /dev/null +++ b/jhotdraw-utils/src/test/resources/org/jhotdraw/undo/Labels.properties @@ -0,0 +1,4 @@ +edit.undo.text=Undo +edit.redo.text=Redo +edit.undo=Undo +edit.redo=Redo diff --git a/jhotdraw-utils/src/test/resources/org/jhotdraw/undo/UndoRedoManagerBDDTest.java b/jhotdraw-utils/src/test/resources/org/jhotdraw/undo/UndoRedoManagerBDDTest.java new file mode 100644 index 000000000..592bbf6a3 --- /dev/null +++ b/jhotdraw-utils/src/test/resources/org/jhotdraw/undo/UndoRedoManagerBDDTest.java @@ -0,0 +1,187 @@ +package org.jhotdraw.undo; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.As; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import com.tngtech.jgiven.junit.ScenarioTest; +import org.junit.Test; + +import javax.swing.undo.AbstractUndoableEdit; + +import static org.assertj.core.api.Assertions.assertThat; + + +@As("Undo/Redo Functionality") +public class UndoRedoManagerBDDTest + extends ScenarioTest { + + + static class DrawingEdit extends AbstractUndoableEdit { + private static final long serialVersionUID = 1L; + } + + // Scenario 1 — Acceptance Criterion 1 + // The user can undo the latest action. + @Test + @As("User can undo the latest drawing action") + public void user_can_undo_the_latest_action() { + given().a_fresh_undo_manager() + .and().the_user_has_performed_a_drawing_action(); + + when().the_user_triggers_undo(); + + then().undo_is_no_longer_available() + .and().redo_becomes_available(); + } + + + // Scenario 2 — Acceptance Criterion 2 + // The user can redo an undone action. + + @Test + @As("User can redo a previously undone action") + public void user_can_redo_an_undone_action() { + given().a_fresh_undo_manager() + .and().the_user_has_performed_a_drawing_action(); + + when().the_user_triggers_undo() + .and().the_user_triggers_redo(); + + then().undo_is_available_again() + .and().redo_is_no_longer_available(); + } + + + // Scenario 3 — Acceptance Criterion 3 + // Multiple undo/redo operations are supported. + + @Test + @As("User can undo and redo multiple drawing actions") + public void user_can_undo_multiple_actions() { + given().a_fresh_undo_manager() + .and().the_user_has_performed_$_drawing_actions(3); + + when().the_user_triggers_undo() + .and().the_user_triggers_undo(); + + then().undo_is_still_available_for_remaining_edits() + .and().redo_becomes_available(); + } + + + // Scenario 4 — Acceptance Criterion 4 + // State is correctly reset when edits are discarded (e.g. new file). + + @Test + @As("Manager resets correctly when all edits are discarded") + public void manager_resets_state_after_discard() { + given().a_fresh_undo_manager() + .and().the_user_has_performed_a_drawing_action(); + + when().all_edits_are_discarded(); + + then().neither_undo_nor_redo_is_available(); + } + + + // STAGES + + + public static class GivenStage extends Stage { + + @ProvidedScenarioState + UndoRedoManager manager; + + public GivenStage a_fresh_undo_manager() { + manager = new UndoRedoManager(); + return self(); + } + + public GivenStage the_user_has_performed_a_drawing_action() { + manager.addEdit(new DrawingEdit()); + return self(); + } + + public GivenStage the_user_has_performed_$_drawing_actions(int count) { + for (int i = 0; i < count; i++) { + manager.addEdit(new DrawingEdit()); + } + return self(); + } + } + + public static class WhenStage extends Stage { + + @ExpectedScenarioState + UndoRedoManager manager; + + public WhenStage the_user_triggers_undo() { + manager.undo(); + return self(); + } + + public WhenStage the_user_triggers_redo() { + manager.redo(); + return self(); + } + + public WhenStage all_edits_are_discarded() { + manager.discardAllEdits(); + return self(); + } + } + + public static class ThenStage extends Stage { + + @ExpectedScenarioState + UndoRedoManager manager; + + public ThenStage undo_is_no_longer_available() { + assertThat(manager.canUndo()) + .as("Undo should not be available after undoing the only edit") + .isFalse(); + return self(); + } + + public ThenStage redo_becomes_available() { + assertThat(manager.canRedo()) + .as("Redo should be available after an undo") + .isTrue(); + return self(); + } + + public ThenStage undo_is_available_again() { + assertThat(manager.canUndo()) + .as("Undo should be available again after redo") + .isTrue(); + return self(); + } + + public ThenStage redo_is_no_longer_available() { + assertThat(manager.canRedo()) + .as("Redo should not be available after redoing") + .isFalse(); + return self(); + } + + public ThenStage undo_is_still_available_for_remaining_edits() { + assertThat(manager.canUndo()) + .as("Undo should still be available (1 edit remains)") + .isTrue(); + return self(); + } + + public ThenStage neither_undo_nor_redo_is_available() { + assertThat(manager.canUndo()) + .as("Undo must be disabled after discard") + .isFalse(); + assertThat(manager.canRedo()) + .as("Redo must be disabled after discard") + .isFalse(); + return self(); + } + } +} \ No newline at end of file