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