diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..8d38d0f99
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,29 @@
+name: Java CI with Maven
+
+on:
+ pull_request:
+ branches: [ "develop" ]
+ push:
+ branches: [ "develop", "feature-branch" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Build with Maven
+ run: mvn clean install -DskipTests --file pom.xml --settings .maven-settings.xml
+
+ - name: Run Tests
+ run: mvn test --file pom.xml --settings .maven-settings.xml
+
diff --git a/.maven-settings.xml b/.maven-settings.xml
new file mode 100644
index 000000000..0de651b15
--- /dev/null
+++ b/.maven-settings.xml
@@ -0,0 +1,9 @@
+
+
+
+ github
+ ${env.GITHUB_ACTOR}
+ ${env.GITHUB_TOKEN}
+
+
+
diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml
index 7c276da85..8f23616c2 100644
--- a/jhotdraw-core/pom.xml
+++ b/jhotdraw-core/pom.xml
@@ -35,10 +35,43 @@
6.8.21
test
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 4.11.0
+ test
+
+
+ com.tngtech.jgiven
+ jgiven-junit
+ 0.18.2
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.24.2
+ test
+
${project.groupId}
jhotdraw-actions
${project.version}
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.2
+
+
+
\ No newline at end of file
diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/GroupAction.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/GroupAction.java
index 08c5a0ada..d50f230f0 100644
--- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/GroupAction.java
+++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/GroupAction.java
@@ -7,160 +7,73 @@
*/
package org.jhotdraw.draw.action;
-import org.jhotdraw.draw.figure.Figure;
+import java.awt.event.ActionEvent;
+import java.util.Collection;
+import java.util.LinkedList;
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+import javax.swing.undo.UndoableEdit;
+import org.jhotdraw.draw.Drawing;
+import org.jhotdraw.draw.DrawingEditor;
+import org.jhotdraw.draw.DrawingView;
import org.jhotdraw.draw.figure.CompositeFigure;
+import org.jhotdraw.draw.figure.Figure;
import org.jhotdraw.draw.figure.GroupFigure;
-import java.util.*;
-import javax.swing.undo.*;
-import org.jhotdraw.draw.*;
import org.jhotdraw.util.ResourceBundleUtil;
-/**
- * GroupAction.
- *
- * @author Werner Randelshofer
- * @version $Id$
- */
public class GroupAction extends AbstractSelectedAction {
private static final long serialVersionUID = 1L;
public static final String ID = "edit.groupSelection";
private CompositeFigure prototype;
- /**
- * If this variable is true, this action groups figures.
- * If this variable is false, this action ungroups figures.
- */
- private boolean isGroupingAction;
-
- /**
- * Creates a new instance.
- */
+
public GroupAction(DrawingEditor editor) {
- this(editor, new GroupFigure(), true);
+ this(editor, new GroupFigure());
}
public GroupAction(DrawingEditor editor, CompositeFigure prototype) {
- this(editor, prototype, true);
- }
-
- public GroupAction(DrawingEditor editor, CompositeFigure prototype, boolean isGroupingAction) {
super(editor);
this.prototype = prototype;
- this.isGroupingAction = isGroupingAction;
- ResourceBundleUtil labels
- = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
+ ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
labels.configureAction(this, ID);
updateEnabledState();
}
@Override
protected void updateEnabledState() {
- if (getView() != null) {
- setEnabled(isGroupingAction ? canGroup() : canUngroup());
- } else {
- setEnabled(false);
- }
+ setEnabled(canGroup(getView()));
}
protected boolean canGroup() {
- return getView() != null && getView().getSelectionCount() > 1;
+ return canGroup(getView());
}
- protected boolean canUngroup() {
- return getView() != null
- && getView().getSelectionCount() == 1
- && prototype != null
- && getView().getSelectedFigures().iterator().next().getClass().equals(
- prototype.getClass());
+ protected boolean canGroup(DrawingView v) {
+ return v != null && v.getSelectionCount() > 1;
}
@Override
- public void actionPerformed(java.awt.event.ActionEvent e) {
- if (isGroupingAction) {
- if (canGroup()) {
- final DrawingView view = getView();
- final LinkedList ungroupedFigures = new LinkedList<>(view.getSelectedFigures());
- final CompositeFigure group = (CompositeFigure) prototype.clone();
- UndoableEdit edit = new AbstractUndoableEdit() {
- private static final long serialVersionUID = 1L;
-
- @Override
- public String getPresentationName() {
- ResourceBundleUtil labels
- = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
- return labels.getString("edit.groupSelection.text");
- }
-
- @Override
- public void redo() throws CannotRedoException {
- super.redo();
- groupFigures(view, group, ungroupedFigures);
- }
-
- @Override
- public void undo() throws CannotUndoException {
- ungroupFigures(view, group);
- super.undo();
- }
-
- @Override
- public boolean addEdit(UndoableEdit anEdit) {
- return super.addEdit(anEdit);
- }
- };
- groupFigures(view, group, ungroupedFigures);
- fireUndoableEditHappened(edit);
- }
- } else {
- if (canUngroup()) {
- final DrawingView view = getView();
- final CompositeFigure group = (CompositeFigure) getView().getSelectedFigures().iterator().next();
- final LinkedList ungroupedFigures = new LinkedList<>();
- UndoableEdit edit = new AbstractUndoableEdit() {
- private static final long serialVersionUID = 1L;
-
- @Override
- public String getPresentationName() {
- ResourceBundleUtil labels
- = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
- return labels.getString("edit.ungroupSelection.text");
- }
-
- @Override
- public void redo() throws CannotRedoException {
- super.redo();
- ungroupFigures(view, group);
- }
-
- @Override
- public void undo() throws CannotUndoException {
- groupFigures(view, group, ungroupedFigures);
- super.undo();
- }
- };
- ungroupedFigures.addAll(ungroupFigures(view, group));
- fireUndoableEditHappened(edit);
- }
+ public void actionPerformed(ActionEvent e) {
+ if (canGroup()) {
+ final DrawingView view = getView();
+ final LinkedList ungroupedFigures = new LinkedList<>(view.getSelectedFigures());
+ assert prototype.clone() instanceof CompositeFigure : "prototype.clone() must be a CompositeFigure";
+ final CompositeFigure group = (CompositeFigure) prototype.clone();
+
+ UndoableEdit edit = new GroupUndoableEdit(view, group, ungroupedFigures);
+ groupFigures(view, group, ungroupedFigures);
+ fireUndoableEditHappened(edit);
}
}
- public Collection ungroupFigures(DrawingView view, CompositeFigure group) {
-// XXX - This code is redundant with UngroupAction
- LinkedList figures = new LinkedList<>(group.getChildren());
- view.clearSelection();
- group.basicRemoveAllChildren();
- view.getDrawing().basicAddAll(view.getDrawing().indexOf(group), figures);
- view.getDrawing().remove(group);
- view.addToSelection(figures);
- return figures;
- }
-
public void groupFigures(DrawingView view, CompositeFigure group, Collection figures) {
- Collection sorted = view.getDrawing().sort(figures);
- int index = view.getDrawing().indexOf(sorted.iterator().next());
- view.getDrawing().basicRemoveAll(figures);
+ Drawing drawing = view.getDrawing();
+ Collection sorted = drawing.sort(figures);
+ int index = drawing.indexOf(sorted.iterator().next());
+ drawing.basicRemoveAll(figures);
view.clearSelection();
- view.getDrawing().add(index, group);
+ drawing.add(index, group);
group.willChange();
for (Figure f : sorted) {
f.willChange();
@@ -169,4 +82,43 @@ public void groupFigures(DrawingView view, CompositeFigure group, Collection ungroupedFigures;
+
+ GroupUndoableEdit(DrawingView view, CompositeFigure group, LinkedList ungroupedFigures) {
+ this.view = view;
+ this.group = group;
+ this.ungroupedFigures = ungroupedFigures;
+ }
+
+ @Override
+ public String getPresentationName() {
+ ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
+ return labels.getString("edit.groupSelection.text");
+ }
+
+ @Override
+ public void redo() throws CannotRedoException {
+ super.redo();
+ groupFigures(view, group, ungroupedFigures);
+ }
+
+ @Override
+ public void undo() throws CannotUndoException {
+ Drawing drawing = view.getDrawing();
+ LinkedList figures = new LinkedList<>(group.getChildren());
+ view.clearSelection();
+ group.basicRemoveAllChildren();
+ drawing.basicAddAll(drawing.indexOf(group), figures);
+ drawing.remove(group);
+ view.addToSelection(figures);
+ super.undo();
+ }
+ }
}
diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/UngroupAction.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/UngroupAction.java
index dc8956cfd..0fe7c9b07 100644
--- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/UngroupAction.java
+++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/UngroupAction.java
@@ -7,40 +7,108 @@
*/
package org.jhotdraw.draw.action;
+import java.awt.event.ActionEvent;
+import java.util.Collection;
+import java.util.LinkedList;
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+import javax.swing.undo.UndoableEdit;
+import org.jhotdraw.draw.Drawing;
+import org.jhotdraw.draw.DrawingEditor;
+import org.jhotdraw.draw.DrawingView;
import org.jhotdraw.draw.figure.CompositeFigure;
+import org.jhotdraw.draw.figure.Figure;
import org.jhotdraw.draw.figure.GroupFigure;
-import org.jhotdraw.draw.*;
import org.jhotdraw.util.ResourceBundleUtil;
-/**
- * UngroupAction.
- *
- * @author Werner Randelshofer
- * @version $Id$
- */
-public class UngroupAction extends GroupAction {
+public class UngroupAction extends AbstractSelectedAction {
private static final long serialVersionUID = 1L;
public static final String ID = "edit.ungroupSelection";
- /**
- * Creates a new instance.
- */
private CompositeFigure prototype;
- /**
- * Creates a new instance.
- */
+ // Constructor chaining eliminates the duplicate setup code smell
public UngroupAction(DrawingEditor editor) {
- super(editor, new GroupFigure(), false);
- ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
- labels.configureAction(this, ID);
- updateEnabledState();
+ this(editor, new GroupFigure());
}
public UngroupAction(DrawingEditor editor, CompositeFigure prototype) {
- super(editor, prototype, false);
+ super(editor);
+ this.prototype = prototype;
ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
labels.configureAction(this, ID);
updateEnabledState();
}
+
+ @Override
+ protected void updateEnabledState() {
+ setEnabled(canUngroup(getView()));
+ }
+
+ protected boolean canUngroup() {
+ return canUngroup(getView());
+ }
+
+ protected boolean canUngroup(DrawingView v) {
+ return v != null
+ && v.getSelectionCount() == 1
+ && prototype != null
+ && v.getSelectedFigures().iterator().next().getClass().equals(
+ prototype.getClass());
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (canUngroup()) {
+ final DrawingView view = getView();
+ final CompositeFigure group = (CompositeFigure) view.getSelectedFigures().iterator().next();
+ final LinkedList ungroupedFigures = new LinkedList<>();
+
+ UndoableEdit edit = new UngroupUndoableEdit(view, group);
+ ungroupedFigures.addAll(ungroupFigures(view, group));
+ fireUndoableEditHappened(edit);
+ }
+ }
+
+ public Collection ungroupFigures(DrawingView view, CompositeFigure group) {
+ Drawing drawing = view.getDrawing();
+ LinkedList figures = new LinkedList<>(group.getChildren());
+ view.clearSelection();
+ group.basicRemoveAllChildren();
+ drawing.basicAddAll(drawing.indexOf(group), figures);
+ drawing.remove(group);
+ view.addToSelection(figures);
+ return figures;
+ }
+
+ // Extracted from anonymous inner class to eliminate Anonymous Inner Class smell
+ private class UngroupUndoableEdit extends AbstractUndoableEdit {
+
+ private static final long serialVersionUID = 1L;
+ private final DrawingView view;
+ private final CompositeFigure group;
+
+ UngroupUndoableEdit(DrawingView view, CompositeFigure group) {
+ this.view = view;
+ this.group = group;
+ }
+
+ @Override
+ public String getPresentationName() {
+ ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
+ return labels.getString("edit.ungroupSelection.text");
+ }
+
+ @Override
+ public void redo() throws CannotRedoException {
+ super.redo();
+ ungroupFigures(view, group);
+ }
+
+ @Override
+ public void undo() throws CannotUndoException {
+ super.undo();
+ }
+ }
}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GivenGroupingStage.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GivenGroupingStage.java
new file mode 100644
index 000000000..03886ca01
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GivenGroupingStage.java
@@ -0,0 +1,111 @@
+package org.jhotdraw.draw.action;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ScenarioState;
+import org.jhotdraw.draw.Drawing;
+import org.jhotdraw.draw.DrawingView;
+import org.jhotdraw.draw.figure.Figure;
+import org.jhotdraw.draw.figure.GroupFigure;
+import org.mockito.Mockito;
+
+import java.awt.geom.Rectangle2D;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class GivenGroupingStage extends Stage {
+
+ @ScenarioState
+ DrawingView view;
+
+ @ScenarioState
+ GroupAction groupAction;
+
+ @ScenarioState
+ UngroupAction ungroupAction;
+
+ @ScenarioState
+ List selectedFigures;
+
+ @ScenarioState
+ GroupFigure selectedGroup;
+
+ /** Returns a stub rectangle used wherever getDrawingArea() is needed. */
+ private Rectangle2D.Double rect() {
+ return new Rectangle2D.Double(0, 0, 10, 10);
+ }
+
+ /** Stubs both getDrawingArea() overloads on a mock figure. */
+ private void stubDrawingArea(Figure mock) {
+ Mockito.when(mock.getDrawingArea()).thenReturn(rect());
+ Mockito.when(mock.getDrawingArea(Mockito.anyDouble())).thenReturn(rect());
+ }
+
+ public GivenGroupingStage two_figures_are_selected() {
+ Drawing drawing = Mockito.mock(Drawing.class);
+ view = Mockito.mock(DrawingView.class);
+ Mockito.when(view.getDrawing()).thenReturn(drawing);
+
+ Figure f1 = Mockito.mock(Figure.class);
+ Figure f2 = Mockito.mock(Figure.class);
+ stubDrawingArea(f1);
+ stubDrawingArea(f2);
+ selectedFigures = Arrays.asList(f1, f2);
+
+ Mockito.when(drawing.sort(selectedFigures)).thenReturn(selectedFigures);
+ Mockito.when(drawing.indexOf(f1)).thenReturn(0);
+
+ Set selection = new HashSet<>(selectedFigures);
+ Mockito.when(view.getSelectionCount()).thenReturn(2);
+ Mockito.when(view.getSelectedFigures()).thenReturn(selection);
+
+ groupAction = new GroupAction(null, new GroupFigure());
+ return self();
+ }
+
+ public GivenGroupingStage one_GroupFigure_is_selected() {
+ Drawing drawing = Mockito.mock(Drawing.class);
+ view = Mockito.mock(DrawingView.class);
+ Mockito.when(view.getDrawing()).thenReturn(drawing);
+
+ selectedGroup = new GroupFigure();
+
+ Figure child1 = Mockito.mock(Figure.class);
+ Figure child2 = Mockito.mock(Figure.class);
+ stubDrawingArea(child1);
+ stubDrawingArea(child2);
+
+ selectedGroup.willChange();
+ selectedGroup.basicAdd(child1);
+ selectedGroup.basicAdd(child2);
+ selectedGroup.changed();
+
+ Mockito.when(drawing.indexOf(selectedGroup)).thenReturn(0);
+
+ Set selection = new HashSet<>(Collections.singletonList(selectedGroup));
+ Mockito.when(view.getSelectionCount()).thenReturn(1);
+ Mockito.when(view.getSelectedFigures()).thenReturn(selection);
+
+ ungroupAction = new UngroupAction(null, new GroupFigure());
+ return self();
+ }
+
+ public GivenGroupingStage one_plain_figure_is_selected() {
+ Drawing drawing = Mockito.mock(Drawing.class);
+ view = Mockito.mock(DrawingView.class);
+ Mockito.when(view.getDrawing()).thenReturn(drawing);
+
+ Figure f = Mockito.mock(Figure.class);
+ stubDrawingArea(f);
+ selectedFigures = Collections.singletonList(f);
+
+ Set selection = new HashSet<>(selectedFigures);
+ Mockito.when(view.getSelectionCount()).thenReturn(1);
+ Mockito.when(view.getSelectedFigures()).thenReturn(selection);
+
+ groupAction = new GroupAction(null, new GroupFigure());
+ return self();
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupActionTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupActionTest.java
new file mode 100644
index 000000000..4c5244fc4
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupActionTest.java
@@ -0,0 +1,273 @@
+/*
+ * JUnit 4 tests for GroupAction and UngroupAction domain logic.
+ * Swing dependencies (DrawingView, Drawing) are stubbed with Mockito
+ * so each test exercises a single code path through a single method.
+ */
+package org.jhotdraw.draw.action;
+
+import org.jhotdraw.draw.Drawing;
+import org.jhotdraw.draw.DrawingView;
+import org.jhotdraw.draw.figure.CompositeFigure;
+import org.jhotdraw.draw.figure.Figure;
+import org.jhotdraw.draw.figure.GroupFigure;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+import org.mockito.InOrder;
+
+import static org.mockito.Mockito.*;
+
+public class GroupActionTest {
+
+ // --- shared stubs ---
+ private DrawingView view;
+ private Drawing drawing;
+ private GroupAction groupAction;
+ private UngroupAction ungroupAction;
+
+ @Before
+ public void setUp() {
+ view = mock(DrawingView.class);
+ drawing = mock(Drawing.class);
+ when(view.getDrawing()).thenReturn(drawing);
+
+ // Construct actions without a real DrawingEditor by passing null;
+ // we only test the pure domain methods (groupFigures / ungroupFigures /
+ // canGroup / canUngroup) which do not touch the editor.
+ groupAction = new GroupAction(null, new GroupFigure());
+ ungroupAction = new UngroupAction(null, new GroupFigure());
+ }
+
+ // =========================================================
+ // GroupAction – canGroup
+ // =========================================================
+
+ @Test
+ public void canGroup_returnsFalse_whenViewIsNull() {
+ // view == null → cannot group
+ assertFalse(groupAction.canGroup(null));
+ }
+
+ @Test
+ public void canGroup_returnsFalse_whenSelectionCountIsOne() {
+ when(view.getSelectionCount()).thenReturn(1);
+ assertFalse(groupAction.canGroup(view));
+ }
+
+ @Test
+ public void canGroup_returnsTrue_whenSelectionCountIsTwo() {
+ when(view.getSelectionCount()).thenReturn(2);
+ assertTrue(groupAction.canGroup(view));
+ }
+
+ @Test
+ public void canGroup_returnsTrue_whenSelectionCountIsMany() {
+ // boundary: more than two figures selected
+ when(view.getSelectionCount()).thenReturn(10);
+ assertTrue(groupAction.canGroup(view));
+ }
+
+ // =========================================================
+ // GroupAction – groupFigures
+ // =========================================================
+
+ @Test
+ public void groupFigures_addsGroupToDrawingAtCorrectIndex() {
+ Figure f1 = mock(Figure.class);
+ Figure f2 = mock(Figure.class);
+ List figures = Arrays.asList(f1, f2);
+ CompositeFigure group = mock(CompositeFigure.class);
+
+ when(drawing.sort(figures)).thenReturn(figures);
+ when(drawing.indexOf(f1)).thenReturn(3);
+
+ groupAction.groupFigures(view, group, figures);
+
+ verify(drawing).basicRemoveAll(figures);
+ verify(drawing).add(3, group);
+ }
+
+ @Test
+ public void groupFigures_addsEachFigureToGroup() {
+ Figure f1 = mock(Figure.class);
+ Figure f2 = mock(Figure.class);
+ List figures = Arrays.asList(f1, f2);
+ CompositeFigure group = mock(CompositeFigure.class);
+
+ when(drawing.sort(figures)).thenReturn(figures);
+ when(drawing.indexOf(f1)).thenReturn(0);
+
+ groupAction.groupFigures(view, group, figures);
+
+ verify(group).basicAdd(f1);
+ verify(group).basicAdd(f2);
+ }
+
+ @Test
+ public void groupFigures_selectsGroupAfterGrouping() {
+ Figure f1 = mock(Figure.class);
+ List figures = Collections.singletonList(f1);
+ CompositeFigure group = mock(CompositeFigure.class);
+
+ when(drawing.sort(figures)).thenReturn(figures);
+ when(drawing.indexOf(f1)).thenReturn(0);
+
+ groupAction.groupFigures(view, group, figures);
+
+ verify(view).addToSelection(group);
+ }
+
+ @Test
+ public void groupFigures_callsWillChangeAndChangedOnGroup() {
+ Figure f1 = mock(Figure.class);
+ List figures = Collections.singletonList(f1);
+ CompositeFigure group = mock(CompositeFigure.class);
+
+ when(drawing.sort(figures)).thenReturn(figures);
+ when(drawing.indexOf(f1)).thenReturn(0);
+
+ groupAction.groupFigures(view, group, figures);
+
+ verify(group).willChange();
+ verify(group).changed();
+ }
+
+ // =========================================================
+ // UngroupAction – canUngroup
+ // =========================================================
+
+ @Test
+ public void canUngroup_returnsFalse_whenViewIsNull() {
+ assertFalse(ungroupAction.canUngroup(null));
+ }
+
+ @Test
+ public void canUngroup_returnsFalse_whenSelectionCountIsNotOne() {
+ when(view.getSelectionCount()).thenReturn(2);
+ assertFalse(ungroupAction.canUngroup(view));
+ }
+
+ @Test
+ public void canUngroup_returnsFalse_whenSelectionCountIsZero() {
+ // boundary: empty selection
+ when(view.getSelectionCount()).thenReturn(0);
+ assertFalse(ungroupAction.canUngroup(view));
+ }
+
+ @Test
+ public void canUngroup_returnsFalse_whenSelectedFigureIsWrongType() {
+ Figure plainFigure = mock(Figure.class);
+ Set sel = new HashSet<>(Collections.singletonList(plainFigure));
+ when(view.getSelectionCount()).thenReturn(1);
+ when(view.getSelectedFigures()).thenReturn(sel);
+ assertFalse(ungroupAction.canUngroup(view));
+ }
+
+ @Test
+ public void canUngroup_returnsTrue_whenExactlyOneGroupFigureSelected() {
+ GroupFigure gf = new GroupFigure();
+ Set sel = new HashSet<>(Collections.singletonList(gf));
+ when(view.getSelectionCount()).thenReturn(1);
+ when(view.getSelectedFigures()).thenReturn(sel);
+ assertTrue(ungroupAction.canUngroup(view));
+ }
+
+ // =========================================================
+ // UngroupAction – ungroupFigures
+ // =========================================================
+
+ @Test
+ public void ungroupFigures_returnsChildrenOfGroup() {
+ Figure child1 = mock(Figure.class);
+ Figure child2 = mock(Figure.class);
+ CompositeFigure group = mock(CompositeFigure.class);
+ when(group.getChildren()).thenReturn(Arrays.asList(child1, child2));
+ when(drawing.indexOf(group)).thenReturn(0);
+
+ Collection result = ungroupAction.ungroupFigures(view, group);
+
+ assertTrue(result.contains(child1));
+ assertTrue(result.contains(child2));
+ }
+
+ @Test
+ public void ungroupFigures_removesGroupFromDrawing() {
+ CompositeFigure group = mock(CompositeFigure.class);
+ when(group.getChildren()).thenReturn(Collections.emptyList());
+ when(drawing.indexOf(group)).thenReturn(2);
+
+ ungroupAction.ungroupFigures(view, group);
+
+ verify(drawing).remove(group);
+ }
+
+ @Test
+ public void ungroupFigures_addsChildrenBackToDrawingAtGroupIndex() {
+ Figure child = mock(Figure.class);
+ CompositeFigure group = mock(CompositeFigure.class);
+ List children = Collections.singletonList(child);
+ when(group.getChildren()).thenReturn(children);
+ when(drawing.indexOf(group)).thenReturn(5);
+
+ ungroupAction.ungroupFigures(view, group);
+
+ verify(drawing).basicAddAll(5, children);
+ }
+
+ @Test
+ public void ungroupFigures_clearsSelectionThenSelectsChildren() {
+ Figure child = mock(Figure.class);
+ CompositeFigure group = mock(CompositeFigure.class);
+ when(group.getChildren()).thenReturn(Collections.singletonList(child));
+ when(drawing.indexOf(group)).thenReturn(0);
+
+ ungroupAction.ungroupFigures(view, group);
+
+ // clearSelection must happen before addToSelection
+ InOrder inOrder = inOrder(view);
+ inOrder.verify(view).clearSelection();
+ inOrder.verify(view).addToSelection(anyCollection());
+ }
+
+ @Test
+ public void ungroupFigures_returnsEmptyCollection_whenGroupHasNoChildren() {
+ // boundary: empty group
+ CompositeFigure group = mock(CompositeFigure.class);
+ when(group.getChildren()).thenReturn(Collections.emptyList());
+ when(drawing.indexOf(group)).thenReturn(0);
+
+ Collection result = ungroupAction.ungroupFigures(view, group);
+
+ assertTrue(result.isEmpty());
+ }
+
+ // =========================================================
+ // Java assertion (invariant) smoke test
+ // =========================================================
+
+ @Test
+ public void groupFigures_invariant_groupIsInDrawingAfterGrouping() {
+ Figure f1 = mock(Figure.class);
+ List figures = Collections.singletonList(f1);
+ CompositeFigure group = mock(CompositeFigure.class);
+
+ when(drawing.sort(figures)).thenReturn(figures);
+ when(drawing.indexOf(f1)).thenReturn(0);
+ // After add(0, group) the drawing "contains" group
+ when(drawing.contains(group)).thenReturn(true);
+
+ groupAction.groupFigures(view, group, figures);
+
+ // Invariant: the group must exist in the drawing after grouping
+ assert drawing.contains(group) : "Invariant violated: group must be in drawing after groupFigures";
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupingBDDTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupingBDDTest.java
new file mode 100644
index 000000000..1103df562
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/GroupingBDDTest.java
@@ -0,0 +1,28 @@
+package org.jhotdraw.draw.action;
+
+import com.tngtech.jgiven.junit.ScenarioTest;
+import org.junit.Test;
+
+public class GroupingBDDTest extends ScenarioTest {
+
+ @Test
+ public void a_designer_can_group_multiple_figures() {
+ given().two_figures_are_selected();
+ when().the_group_action_is_triggered();
+ then().the_figures_are_combined_into_a_GroupFigure();
+ }
+
+ @Test
+ public void a_designer_can_ungroup_a_grouped_figure() {
+ given().one_GroupFigure_is_selected();
+ when().the_ungroup_action_is_triggered();
+ then().the_children_are_restored_to_the_drawing();
+ }
+
+ @Test
+ public void group_action_is_disabled_for_a_single_figure() {
+ given().one_plain_figure_is_selected();
+ when().checking_if_grouping_is_allowed();
+ then().canGroup_returns_false();
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/ThenGroupingStage.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/ThenGroupingStage.java
new file mode 100644
index 000000000..7c6c0f080
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/ThenGroupingStage.java
@@ -0,0 +1,53 @@
+package org.jhotdraw.draw.action;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ScenarioState;
+import org.jhotdraw.draw.DrawingView;
+import org.jhotdraw.draw.figure.CompositeFigure;
+import org.jhotdraw.draw.figure.Figure;
+import org.jhotdraw.draw.figure.GroupFigure;
+import org.mockito.Mockito;
+
+import java.util.Collection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ThenGroupingStage extends Stage {
+
+ @ScenarioState
+ DrawingView view;
+
+ @ScenarioState
+ CompositeFigure resultGroup;
+
+ @ScenarioState
+ Collection resultChildren;
+
+ @ScenarioState
+ boolean canGroupResult;
+
+ public ThenGroupingStage the_figures_are_combined_into_a_GroupFigure() {
+ assertThat(resultGroup)
+ .as("result must be a GroupFigure")
+ .isInstanceOf(GroupFigure.class);
+ // Verify the group was selected in the view
+ Mockito.verify(view).addToSelection(resultGroup);
+ return self();
+ }
+
+ public ThenGroupingStage the_children_are_restored_to_the_drawing() {
+ assertThat(resultChildren)
+ .as("ungroupFigures must return the children that were in the group")
+ .hasSize(2);
+ // Verify children were re-selected in the view
+ Mockito.verify(view).addToSelection(resultChildren);
+ return self();
+ }
+
+ public ThenGroupingStage canGroup_returns_false() {
+ assertThat(canGroupResult)
+ .as("canGroup should be false when only one figure is selected")
+ .isFalse();
+ return self();
+ }
+}
diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/WhenGroupingStage.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/WhenGroupingStage.java
new file mode 100644
index 000000000..962ebd95b
--- /dev/null
+++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/WhenGroupingStage.java
@@ -0,0 +1,56 @@
+package org.jhotdraw.draw.action;
+
+import com.tngtech.jgiven.Stage;
+import com.tngtech.jgiven.annotation.ScenarioState;
+import org.jhotdraw.draw.DrawingView;
+import org.jhotdraw.draw.figure.CompositeFigure;
+import org.jhotdraw.draw.figure.Figure;
+import org.jhotdraw.draw.figure.GroupFigure;
+
+import java.util.Collection;
+import java.util.List;
+
+public class WhenGroupingStage extends Stage {
+
+ @ScenarioState
+ DrawingView view;
+
+ @ScenarioState
+ GroupAction groupAction;
+
+ @ScenarioState
+ UngroupAction ungroupAction;
+
+ @ScenarioState
+ List selectedFigures;
+
+ @ScenarioState
+ GroupFigure selectedGroup;
+
+ @ScenarioState
+ CompositeFigure resultGroup;
+
+ @ScenarioState
+ Collection resultChildren;
+
+ @ScenarioState
+ boolean canGroupResult;
+
+ public WhenGroupingStage the_group_action_is_triggered() {
+ resultGroup = new GroupFigure();
+ // drawing stubs were set up in Given; groupFigures calls view.getDrawing() internally
+ groupAction.groupFigures(view, resultGroup, selectedFigures);
+ return self();
+ }
+
+ public WhenGroupingStage the_ungroup_action_is_triggered() {
+ // selectedGroup was pre-populated with children in Given
+ resultChildren = ungroupAction.ungroupFigures(view, selectedGroup);
+ return self();
+ }
+
+ public WhenGroupingStage checking_if_grouping_is_allowed() {
+ canGroupResult = groupAction.canGroup(view);
+ return self();
+ }
+}
diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/CombineAction.java b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/CombineAction.java
index 62b3bd1ec..f28256f5e 100644
--- a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/CombineAction.java
+++ b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/CombineAction.java
@@ -50,7 +50,6 @@ protected boolean canGroup() {
return canCombine;
}
- @Override
@SuppressWarnings("unchecked")
public Collection ungroupFigures(DrawingView view, CompositeFigure group) {
LinkedList figures = new LinkedList(group.getChildren());
diff --git a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/SplitAction.java b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/SplitAction.java
index a48a85f32..59a19cfbc 100644
--- a/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/SplitAction.java
+++ b/jhotdraw-samples/jhotdraw-samples-misc/src/main/java/org/jhotdraw/samples/odg/action/SplitAction.java
@@ -1,10 +1,3 @@
-/*
- * @(#)SplitPathsAction.java
- *
- * Copyright (c) 2007 The authors and contributors of JHotDraw.
- * You may not use, copy or modify this file, except in compliance with the
- * accompanying license terms.
- */
package org.jhotdraw.samples.odg.action;
import org.jhotdraw.draw.figure.Figure;
@@ -15,12 +8,6 @@
import org.jhotdraw.samples.odg.figures.ODGPathFigure;
import org.jhotdraw.util.*;
-/**
- * SplitPathsAction.
- *
- * @author Werner Randelshofer
- * @version $Id$
- */
public class SplitAction extends UngroupAction {
private static final long serialVersionUID = 1L;
@@ -28,9 +15,6 @@ public class SplitAction extends UngroupAction {
private ResourceBundleUtil labels
= ResourceBundleUtil.getBundle("org.jhotdraw.samples.odg.Labels");
- /**
- * Creates a new instance.
- */
public SplitAction(DrawingEditor editor) {
super(editor, new ODGPathFigure());
labels.configureAction(this, ID);
@@ -65,26 +49,4 @@ public Collection ungroupFigures(DrawingView view, CompositeFigure group
view.addToSelection(paths);
return figures;
}
-
- @SuppressWarnings("unchecked")
- @Override
- public void groupFigures(DrawingView view, CompositeFigure group, Collection figures) {
- Collection sorted = view.getDrawing().sort(figures);
- view.getDrawing().basicRemoveAll(figures);
- view.clearSelection();
- view.getDrawing().add(group);
- group.willChange();
- ((ODGPathFigure) group).removeAllChildren();
- for (Map.Entry, Object> entry : figures.iterator().next().getAttributes().entrySet()) {
- group.set((AttributeKey