From 164da000d1aa6c00d41a39d75005181aae748b50 Mon Sep 17 00:00:00 2001 From: phongsakon Date: Sun, 31 May 2026 05:14:22 +0200 Subject: [PATCH 1/5] Refactor AlignAction to remove duplicated alignment loop The six AlignAction subclasses each repeated the same loop over the selected figures and only differed in how they computed the translation. Extracted the varying geometry into an Alignment enum and pulled the shared loop up into the base class as a template method. Subclasses now just carry their Alignment value. Behaviour is unchanged, existing callers in ButtonFactory and AlignToolBar still compile. --- .../org/jhotdraw/draw/action/AlignAction.java | 201 ++++++++---------- .../org/jhotdraw/draw/action/Alignment.java | 109 ++++++++++ 2 files changed, 196 insertions(+), 114 deletions(-) create mode 100644 jhotdraw-core/src/main/java/org/jhotdraw/draw/action/Alignment.java diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/AlignAction.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/AlignAction.java index 9b682f099..b9d15ac75 100644 --- a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/AlignAction.java +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/AlignAction.java @@ -16,9 +16,29 @@ import org.jhotdraw.util.ResourceBundleUtil; /** - * Aligns the selected figures. + * Aligns the selected figures to a shared edge or centre line. * - * XXX - Fire edit events + *

This is the controller for the align feature. When the user presses an + * align button, Swing calls {@link #actionPerformed}.

+ * + *

What I changed while refactoring. Originally each of the six directions was + * a subclass that overrode the alignment method with the same loop, and only the + * line that computed the movement differed. I lifted that shared loop up into one + * method here, {@link #alignFigures}. This is the Form Template Method + * refactoring, the base class holds the skeleton of the algorithm and the part + * that varies is delegated out, in this case to the {@link Alignment} enum. The + * user sees no difference, which is the defining rule of a refactoring, so the + * existing callers in ButtonFactory and AlignToolBar still work unchanged.

+ * + *

Java notes to self.

+ * * * @author Werner Randelshofer * @version $Id$ @@ -26,17 +46,28 @@ public abstract class AlignAction extends AbstractSelectedAction { private static final long serialVersionUID = 1L; + // Which direction this action aligns to. "final" means it is set once in the + // constructor and can never change afterwards, which is what we want here. + private final Alignment alignment; + // The label texts and icons, looked up by key from a resource file. This is + // how JHotDraw supports several languages. protected ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); /** - * Creates a new instance. + * Creates an align action for the given direction. The subclasses below call + * this with their own Alignment value. */ - public AlignAction(DrawingEditor editor) { + public AlignAction(DrawingEditor editor, Alignment alignment) { super(editor); + this.alignment = alignment; updateEnabledState(); } + /** + * Decides when the button is clickable. Aligning needs at least two figures, + * because a single figure has nothing to line up against. + */ @Override public void updateEnabledState() { if (getView() != null) { @@ -47,6 +78,11 @@ public void updateEnabledState() { } } + /** + * Runs when the user clicks the button. The two fireUndoableEditHappened + * calls wrap the work in a single CompositeEdit, so one press of undo reverts + * the whole alignment rather than moving figures back one at a time. + */ @Override public void actionPerformed(java.awt.event.ActionEvent e) { CompositeEdit edit = new CompositeEdit(labels.getString("edit.align.text")); @@ -55,10 +91,39 @@ public void actionPerformed(java.awt.event.ActionEvent e) { fireUndoableEditHappened(edit); } - protected abstract void alignFigures(Collection
selectedFigures, Rectangle2D.Double selectionBounds); + /** + * The template method, the loop every direction shares. + * + *

It walks the selected figures, asks the {@link Alignment} strategy how + * far each one must move, and applies that move. The direction specific + * decision is the single call to {@code alignment.delta(...)}, so no subclass + * has to repeat the loop.

+ * + *

Step by step for each figure. {@code isTransformable} skips locked + * figures. {@code willChange} tells the figure a change is starting so it can + * prepare to repaint. An AffineTransform is Java's way to describe a move, and + * here we use only its translate part. {@code transform} applies it, + * {@code changed} signals the change is done, and the TransformEdit records + * the move so it can be undone.

+ */ + protected void alignFigures(Collection
selectedFigures, Rectangle2D.Double selectionBounds) { + for (Figure f : getView().getSelectedFigures()) { + if (f.isTransformable()) { + f.willChange(); + Rectangle2D.Double b = f.getBounds(); + Point2D.Double d = alignment.delta(b, selectionBounds); + AffineTransform tx = new AffineTransform(); + tx.translate(d.x, d.y); + f.transform(tx); + f.changed(); + fireUndoableEditHappened(new TransformEdit(f, tx)); + } + } + } /** - * Returns the bounds of the selected figures. + * Returns the smallest rectangle that contains all the selected figures. + * Every direction lines up against the edges of this combined box. */ protected Rectangle2D.Double getSelectionBounds() { Rectangle2D.Double bounds = null; @@ -66,41 +131,29 @@ protected Rectangle2D.Double getSelectionBounds() { if (bounds == null) { bounds = f.getBounds(); } else { + // add() grows the rectangle so it also covers this figure. bounds.add(f.getBounds()); } } return bounds; } + // The six concrete actions. Each one only has to remember which Alignment it + // is and set up its label. All the real work lives in the base class above. + public static class North extends AlignAction { private static final long serialVersionUID = 1L; public North(DrawingEditor editor) { - super(editor); + super(editor, Alignment.NORTH); labels.configureAction(this, "edit.alignNorth"); } public North(DrawingEditor editor, ResourceBundleUtil labels) { - super(editor); + super(editor, Alignment.NORTH); labels.configureAction(this, "edit.alignNorth"); } - - @Override - protected void alignFigures(Collection
selectedFigures, Rectangle2D.Double selectionBounds) { - double y = selectionBounds.y; - for (Figure f : getView().getSelectedFigures()) { - if (f.isTransformable()) { - f.willChange(); - Rectangle2D.Double b = f.getBounds(); - AffineTransform tx = new AffineTransform(); - tx.translate(0, y - b.y); - f.transform(tx); - f.changed(); - fireUndoableEditHappened(new TransformEdit(f, tx)); - } - } - } } public static class East extends AlignAction { @@ -108,30 +161,14 @@ public static class East extends AlignAction { private static final long serialVersionUID = 1L; public East(DrawingEditor editor) { - super(editor); + super(editor, Alignment.EAST); labels.configureAction(this, "edit.alignEast"); } public East(DrawingEditor editor, ResourceBundleUtil labels) { - super(editor); + super(editor, Alignment.EAST); labels.configureAction(this, "edit.alignEast"); } - - @Override - protected void alignFigures(Collection
selectedFigures, Rectangle2D.Double selectionBounds) { - double x = selectionBounds.x + selectionBounds.width; - for (Figure f : getView().getSelectedFigures()) { - if (f.isTransformable()) { - f.willChange(); - Rectangle2D.Double b = f.getBounds(); - AffineTransform tx = new AffineTransform(); - tx.translate(x - b.x - b.width, 0); - f.transform(tx); - f.changed(); - fireUndoableEditHappened(new TransformEdit(f, tx)); - } - } - } } public static class West extends AlignAction { @@ -139,30 +176,14 @@ public static class West extends AlignAction { private static final long serialVersionUID = 1L; public West(DrawingEditor editor) { - super(editor); + super(editor, Alignment.WEST); labels.configureAction(this, "edit.alignWest"); } public West(DrawingEditor editor, ResourceBundleUtil labels) { - super(editor); + super(editor, Alignment.WEST); labels.configureAction(this, "edit.alignWest"); } - - @Override - protected void alignFigures(Collection
selectedFigures, Rectangle2D.Double selectionBounds) { - double x = selectionBounds.x; - for (Figure f : getView().getSelectedFigures()) { - if (f.isTransformable()) { - f.willChange(); - Rectangle2D.Double b = f.getBounds(); - AffineTransform tx = new AffineTransform(); - tx.translate(x - b.x, 0); - f.transform(tx); - f.changed(); - fireUndoableEditHappened(new TransformEdit(f, tx)); - } - } - } } public static class South extends AlignAction { @@ -170,30 +191,14 @@ public static class South extends AlignAction { private static final long serialVersionUID = 1L; public South(DrawingEditor editor) { - super(editor); + super(editor, Alignment.SOUTH); labels.configureAction(this, "edit.alignSouth"); } public South(DrawingEditor editor, ResourceBundleUtil labels) { - super(editor); + super(editor, Alignment.SOUTH); labels.configureAction(this, "edit.alignSouth"); } - - @Override - protected void alignFigures(Collection
selectedFigures, Rectangle2D.Double selectionBounds) { - double y = selectionBounds.y + selectionBounds.height; - for (Figure f : getView().getSelectedFigures()) { - if (f.isTransformable()) { - f.willChange(); - Rectangle2D.Double b = f.getBounds(); - AffineTransform tx = new AffineTransform(); - tx.translate(0, y - b.y - b.height); - f.transform(tx); - f.changed(); - fireUndoableEditHappened(new TransformEdit(f, tx)); - } - } - } } public static class Vertical extends AlignAction { @@ -201,30 +206,14 @@ public static class Vertical extends AlignAction { private static final long serialVersionUID = 1L; public Vertical(DrawingEditor editor) { - super(editor); + super(editor, Alignment.VERTICAL); labels.configureAction(this, "edit.alignVertical"); } public Vertical(DrawingEditor editor, ResourceBundleUtil labels) { - super(editor); + super(editor, Alignment.VERTICAL); labels.configureAction(this, "edit.alignVertical"); } - - @Override - protected void alignFigures(Collection
selectedFigures, Rectangle2D.Double selectionBounds) { - double y = selectionBounds.y + selectionBounds.height / 2; - for (Figure f : getView().getSelectedFigures()) { - if (f.isTransformable()) { - f.willChange(); - Rectangle2D.Double b = f.getBounds(); - AffineTransform tx = new AffineTransform(); - tx.translate(0, y - b.y - b.height / 2); - f.transform(tx); - f.changed(); - fireUndoableEditHappened(new TransformEdit(f, tx)); - } - } - } } public static class Horizontal extends AlignAction { @@ -232,29 +221,13 @@ public static class Horizontal extends AlignAction { private static final long serialVersionUID = 1L; public Horizontal(DrawingEditor editor) { - super(editor); + super(editor, Alignment.HORIZONTAL); labels.configureAction(this, "edit.alignHorizontal"); } public Horizontal(DrawingEditor editor, ResourceBundleUtil labels) { - super(editor); + super(editor, Alignment.HORIZONTAL); labels.configureAction(this, "edit.alignHorizontal"); } - - @Override - protected void alignFigures(Collection
selectedFigures, Rectangle2D.Double selectionBounds) { - double x = selectionBounds.x + selectionBounds.width / 2; - for (Figure f : getView().getSelectedFigures()) { - if (f.isTransformable()) { - f.willChange(); - Rectangle2D.Double b = f.getBounds(); - AffineTransform tx = new AffineTransform(); - tx.translate(x - b.x - b.width / 2, 0); - f.transform(tx); - f.changed(); - fireUndoableEditHappened(new TransformEdit(f, tx)); - } - } - } } } diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/Alignment.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/Alignment.java new file mode 100644 index 000000000..890bd5f03 --- /dev/null +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/Alignment.java @@ -0,0 +1,109 @@ +/* + * Alignment.java + * + * New file added for the align and distribute feature. + * JHotDraw is distributed under the GNU LGPL v2.1. + */ +package org.jhotdraw.draw.action; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +/** + * The six ways the editor can line up a group of selected figures. + * + *

I pulled this out of AlignAction while refactoring. Before, the six + * directions each lived in their own subclass, and every subclass repeated the + * same loop over the figures. The only thing that really differed between them + * was one line of arithmetic, the amount each figure has to move. That + * repetition is the Duplicated Code smell from Kerievsky chapter 4. Moving the + * arithmetic here is the Extract Class step that removes it.

+ * + *

Java note to self. An enum is normally just a fixed list of named values, + * but in Java each value is allowed to carry its own behaviour by overriding a + * method. That is what the constants below do, each one gives its own version of + * delta(). So this single enum acts like a small family of six strategies + * without needing six separate classes.

+ * + *

Everything in here is pure geometry. It only reads rectangles and returns a + * movement vector. There is no Swing and no drawing model, which is exactly what + * makes it easy to unit test on its own.

+ * + *

Coordinate reminder. A Rectangle2D.Double stores x and y as its top left + * corner, plus width and height. Larger y means further down the screen. So the + * top edge of a figure is figure.y and its bottom edge is figure.y + height.

+ */ +public enum Alignment { + + /** Line the figures up on the top edge of the selection. */ + NORTH { + @Override + public Point2D.Double delta(Rectangle2D.Double figure, Rectangle2D.Double selection) { + // Move vertically only. dx is 0. dy lifts the figure's top (figure.y) + // up to the selection's top (selection.y). + return new Point2D.Double(0, selection.y - figure.y); + } + }, + /** Line the figures up on the bottom edge of the selection. */ + SOUTH { + @Override + public Point2D.Double delta(Rectangle2D.Double figure, Rectangle2D.Double selection) { + double targetBottom = selection.y + selection.height; + // The figure's own bottom is figure.y + figure.height. dy is the gap + // between that and the target bottom. + return new Point2D.Double(0, targetBottom - figure.y - figure.height); + } + }, + /** Line the figures up on the left edge of the selection. */ + WEST { + @Override + public Point2D.Double delta(Rectangle2D.Double figure, Rectangle2D.Double selection) { + // Move horizontally only, left edge to left edge. + return new Point2D.Double(selection.x - figure.x, 0); + } + }, + /** Line the figures up on the right edge of the selection. */ + EAST { + @Override + public Point2D.Double delta(Rectangle2D.Double figure, Rectangle2D.Double selection) { + double targetRight = selection.x + selection.width; + return new Point2D.Double(targetRight - figure.x - figure.width, 0); + } + }, + /** Centre the figures on the horizontal middle line of the selection. */ + VERTICAL { + @Override + public Point2D.Double delta(Rectangle2D.Double figure, Rectangle2D.Double selection) { + double targetCentreY = selection.y + selection.height / 2; + // Compare the figure's own centre, not its edge, to the target. + return new Point2D.Double(0, targetCentreY - figure.y - figure.height / 2); + } + }, + /** Centre the figures on the vertical middle line of the selection. */ + HORIZONTAL { + @Override + public Point2D.Double delta(Rectangle2D.Double figure, Rectangle2D.Double selection) { + double targetCentreX = selection.x + selection.width / 2; + return new Point2D.Double(targetCentreX - figure.x - figure.width / 2, 0); + } + }; + + /** + * Works out how far a figure has to move to line up with the selection. + * + *

The result is a vector, given as a Point2D.Double where x is the + * sideways move and y is the up or down move. A zero vector means the figure + * is already in place, so the caller can skip it. Returning a value instead + * of moving the figure directly keeps this method free of side effects, + * which is what lets a test call it and simply check the number.

+ * + *

This is declared abstract, which means the enum itself does not provide + * a body. Each constant above is forced to supply its own. That is the trick + * that lets one method name behave six different ways.

+ * + * @param figure the bounding box of the figure being moved + * @param selection the bounding box of the whole selection + * @return the (dx, dy) movement, never null + */ + public abstract Point2D.Double delta(Rectangle2D.Double figure, Rectangle2D.Double selection); +} From 1b8f0756bb03c8c79f571eb4399e709d20d99cd1 Mon Sep 17 00:00:00 2001 From: phongsakon Date: Sun, 31 May 2026 08:39:05 +0200 Subject: [PATCH 2/5] Add distribute figures feature Second half of the change request. Distribute spreads the selected figures so the gaps between them are equal, horizontally or vertically. Follows the same shape as AlignAction, a thin action delegating the geometry to a Distribution enum, so it reuses the template method the refactoring put in place. Wired the buttons into ButtonFactory and added the labels next to the alignment ones. --- .../draw/action/DistributeAction.java | 142 ++++++++++++++++++ .../jhotdraw/draw/action/Distribution.java | 127 ++++++++++++++++ .../org/jhotdraw/draw/Labels.properties | 10 ++ .../jhotdraw/gui/action/ButtonFactory.java | 21 +++ 4 files changed, 300 insertions(+) create mode 100644 jhotdraw-core/src/main/java/org/jhotdraw/draw/action/DistributeAction.java create mode 100644 jhotdraw-core/src/main/java/org/jhotdraw/draw/action/Distribution.java diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/DistributeAction.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/DistributeAction.java new file mode 100644 index 000000000..7d5f9eaca --- /dev/null +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/DistributeAction.java @@ -0,0 +1,142 @@ +/* + * DistributeAction.java + * + * New file added for the align and distribute feature. + * JHotDraw is distributed under the GNU LGPL v2.1. + */ +package org.jhotdraw.draw.action; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.List; +import org.jhotdraw.draw.DrawingEditor; +import org.jhotdraw.draw.figure.Figure; +import org.jhotdraw.draw.event.TransformEdit; +import org.jhotdraw.undo.CompositeEdit; +import org.jhotdraw.util.ResourceBundleUtil; + +/** + * Distributes the selected figures so the gaps between them are equal. + * + *

This is the new feature. The change request asked to align and distribute, + * alignment already existed and was tidied up first, so this class adds the + * distribute half on top of the clean structure.

+ * + *

I wrote it to mirror {@link AlignAction} on purpose. It is an + * {@link AbstractSelectedAction}, it wraps its work in a CompositeEdit so the + * whole thing is one undo, and it hands the actual arithmetic to the + * {@link Distribution} enum, exactly the way AlignAction hands its arithmetic to + * Alignment. Reusing that shape is the payoff of the earlier refactoring. The new + * feature drops into a tidy pattern instead of adding another copy of an old + * loop.

+ * + *

This also shows two of the SOLID principles in practice. The action handles + * the editor and undo, the enum handles the geometry, so each has a single + * responsibility. And adding distribution needed no change to AlignAction, which + * is the open closed idea, the code is open to a new operation while the old one + * stays closed to edits.

+ */ +public abstract class DistributeAction extends AbstractSelectedAction { + + private static final long serialVersionUID = 1L; + private final Distribution distribution; + protected ResourceBundleUtil labels + = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); + + public DistributeAction(DrawingEditor editor, Distribution distribution) { + super(editor); + this.distribution = distribution; + updateEnabledState(); + } + + /** + * Distribution needs at least three figures, two to pin the ends and one in + * the middle to move, so the button stays disabled below that. + */ + @Override + public void updateEnabledState() { + if (getView() != null) { + setEnabled(getView().isEnabled() + && getView().getSelectionCount() > 2); + } else { + setEnabled(false); + } + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + CompositeEdit edit = new CompositeEdit(labels.getString("edit.distribute.text")); + fireUndoableEditHappened(edit); + distributeFigures(new ArrayList<>(getView().getSelectedFigures())); + fireUndoableEditHappened(edit); + } + + /** + * The template method shared by both directions. + * + *

It asks the {@link Distribution} strategy for the new leading coordinate + * of every figure, then shifts each transformable figure by the difference + * between where it is and where it should be. Only the one axis moves, the + * other shift stays zero, so the figures keep their position on the other + * axis. A shift of zero means the figure is already in place and is skipped.

+ */ + protected void distributeFigures(List
figures) { + List boxes = new ArrayList<>(); + for (Figure f : figures) { + boxes.add(f.getBounds()); + } + double[] targets = distribution.newPositions(boxes); + for (int i = 0; i < figures.size(); i++) { + Figure f = figures.get(i); + if (!f.isTransformable()) { + continue; + } + double current = distribution.position(boxes.get(i)); + double shift = targets[i] - current; + if (shift == 0) { + continue; + } + f.willChange(); + AffineTransform tx = new AffineTransform(); + if (distribution == Distribution.HORIZONTAL) { + tx.translate(shift, 0); + } else { + tx.translate(0, shift); + } + f.transform(tx); + f.changed(); + fireUndoableEditHappened(new TransformEdit(f, tx)); + } + } + + public static class Horizontal extends DistributeAction { + + private static final long serialVersionUID = 1L; + + public Horizontal(DrawingEditor editor) { + super(editor, Distribution.HORIZONTAL); + labels.configureAction(this, "edit.distributeHorizontal"); + } + + public Horizontal(DrawingEditor editor, ResourceBundleUtil labels) { + super(editor, Distribution.HORIZONTAL); + labels.configureAction(this, "edit.distributeHorizontal"); + } + } + + public static class Vertical extends DistributeAction { + + private static final long serialVersionUID = 1L; + + public Vertical(DrawingEditor editor) { + super(editor, Distribution.VERTICAL); + labels.configureAction(this, "edit.distributeVertical"); + } + + public Vertical(DrawingEditor editor, ResourceBundleUtil labels) { + super(editor, Distribution.VERTICAL); + labels.configureAction(this, "edit.distributeVertical"); + } + } +} diff --git a/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/Distribution.java b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/Distribution.java new file mode 100644 index 000000000..c86b8461e --- /dev/null +++ b/jhotdraw-core/src/main/java/org/jhotdraw/draw/action/Distribution.java @@ -0,0 +1,127 @@ +/* + * Distribution.java + * + * New file added for the align and distribute feature. + * JHotDraw is distributed under the GNU LGPL v2.1. + */ +package org.jhotdraw.draw.action; + +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * The two axes along which the editor can space figures out evenly. + * + *

This is the new half of the feature. It sits next to {@link Alignment} on + * purpose. Alignment moves every figure onto one shared line, while distribution + * spreads the figures so the gaps between neighbours are equal. Both are pure + * geometry over bounding boxes, so neither one touches Swing or the drawing + * model and both stay easy to unit test.

+ * + *

Java note to self. Just like Alignment, this is an enum where each constant + * supplies its own behaviour. The two constants below differ only in which axis + * they read, x and width for horizontal, y and height for vertical. Pulling + * those two readings into {@code position} and {@code size} lets the real + * algorithm in {@code newPositions} be written once for both directions.

+ * + *

The maths runs over the whole selection at once, not one figure at a time, + * because where a figure ends up depends on the order and size of the others. + * The two outermost figures stay where they are and the ones in between are + * spread so the spacing is uniform.

+ */ +public enum Distribution { + + /** Spread figures left to right so the horizontal gaps are equal. */ + HORIZONTAL { + @Override + public double position(Rectangle2D.Double r) { + return r.x; + } + + @Override + public double size(Rectangle2D.Double r) { + return r.width; + } + }, + /** Spread figures top to bottom so the vertical gaps are equal. */ + VERTICAL { + @Override + public double position(Rectangle2D.Double r) { + return r.y; + } + + @Override + public double size(Rectangle2D.Double r) { + return r.height; + } + }; + + /** The leading coordinate of the box on this axis, x or y. */ + public abstract double position(Rectangle2D.Double r); + + /** The extent of the box on this axis, width or height. */ + public abstract double size(Rectangle2D.Double r); + + /** + * Works out a new leading coordinate for every box so the gaps between + * neighbours come out equal. + * + *

The result is a plain array of coordinates in the same order as the + * input list, so the caller can match each new value back to its figure by + * index. Returning numbers rather than moving figures keeps this testable.

+ * + *

Distribution only makes sense with three or more figures, two to pin the + * ends and at least one in the middle to move. With two or fewer the method + * returns the coordinates unchanged. The action enforces the same minimum + * before it even offers the command, so this is just a safety net.

+ * + * @param boxes the bounding boxes of the selected figures, in any order + * @return the new leading coordinate per box, lined up with the input by index + */ + public double[] newPositions(List boxes) { + int n = boxes.size(); + double[] result = new double[n]; + for (int i = 0; i < n; i++) { + result[i] = position(boxes.get(i)); + } + if (n < 3) { + return result; + } + + // Sort a copy of the indices by leading coordinate, so we can walk the + // figures from one end of the selection to the other. We sort indices + // rather than the boxes themselves so the answer still maps back by index. + List order = new ArrayList<>(); + for (int i = 0; i < n; i++) { + order.add(i); + } + order.sort(Comparator.comparingDouble(i -> position(boxes.get(i)))); + + int firstIdx = order.get(0); + int lastIdx = order.get(n - 1); + double start = position(boxes.get(firstIdx)); + double end = position(boxes.get(lastIdx)); + + // Add up the space the figures themselves take, so the space left over + // can be shared out as n-1 equal gaps. + double totalSize = 0; + for (Rectangle2D.Double b : boxes) { + totalSize += size(b); + } + double span = (end + size(boxes.get(lastIdx))) - start; + double gap = (span - totalSize) / (n - 1); + + // Walk the figures in order, placing each one right after the previous + // one plus a single uniform gap. The first and last keep their place. + double cursor = start; + for (int k = 0; k < n; k++) { + int idx = order.get(k); + result[idx] = cursor; + cursor += size(boxes.get(idx)) + gap; + } + result[lastIdx] = end; + return result; + } +} diff --git a/jhotdraw-core/src/main/resources/org/jhotdraw/draw/Labels.properties b/jhotdraw-core/src/main/resources/org/jhotdraw/draw/Labels.properties index 21b22b608..7a74e65df 100644 --- a/jhotdraw-core/src/main/resources/org/jhotdraw/draw/Labels.properties +++ b/jhotdraw-core/src/main/resources/org/jhotdraw/draw/Labels.properties @@ -33,6 +33,16 @@ edit.alignWest.mnemonic= edit.alignWest.text= edit.alignWest.largeIcon=${imageDir}/alignWest.png edit.alignWest.toolTipText=Align Left +# Labels for the new distribute feature. The action reads edit.distribute.text +# for the undo name shown in the menu, and the two distributeHorizontal and +# distributeVertical keys for the buttons. I left the icons out so the buttons +# fall back to their text, which keeps the change small and avoids adding new +# image files to the project. +edit.distribute.text=Distribute +edit.distributeHorizontal.text=Distribute Horizontally +edit.distributeHorizontal.toolTipText=Even horizontal gaps +edit.distributeVertical.text=Distribute Vertically +edit.distributeVertical.toolTipText=Even vertical gaps attribute.opacity.text=Opacity attribute.opacity.toolTipText=Opacity attribute.backgroundColor.toolTipText=Background Color diff --git a/jhotdraw-gui/src/main/java/org/jhotdraw/gui/action/ButtonFactory.java b/jhotdraw-gui/src/main/java/org/jhotdraw/gui/action/ButtonFactory.java index 1470a011b..f634d3939 100644 --- a/jhotdraw-gui/src/main/java/org/jhotdraw/gui/action/ButtonFactory.java +++ b/jhotdraw-gui/src/main/java/org/jhotdraw/gui/action/ButtonFactory.java @@ -68,6 +68,7 @@ import org.jhotdraw.draw.DrawingView; import org.jhotdraw.draw.action.AbstractSelectedAction; import org.jhotdraw.draw.action.AlignAction; +import org.jhotdraw.draw.action.DistributeAction; import org.jhotdraw.draw.action.ApplyAttributesAction; import org.jhotdraw.draw.action.AttributeAction; import org.jhotdraw.draw.action.AttributeToggler; @@ -1683,6 +1684,26 @@ public static void addAlignmentButtonsTo(JToolBar bar, final DrawingEditor edito dsp.add(d); } + /** + * Adds the two distribute buttons to the toolbar. + * + * Concept location showed that the alignment buttons are built here, so the + * new distribute buttons go in the same place. This is the small secondary + * change the feature needs, the one spot in the existing code that has to be + * touched so the new action is reachable from the user interface. + */ + public static void addDistributeButtonsTo(JToolBar bar, final DrawingEditor editor) { + addDistributeButtonsTo(bar, editor, new LinkedList<>()); + } + + public static void addDistributeButtonsTo(JToolBar bar, final DrawingEditor editor, java.util.List dsp) { + AbstractSelectedAction d; + bar.add(d = new DistributeAction.Horizontal(editor)).setFocusable(false); + dsp.add(d); + bar.add(d = new DistributeAction.Vertical(editor)).setFocusable(false); + dsp.add(d); + } + /** * Creates a button which toggles between two GridConstrainer for a * DrawingView. From eb2e9880dfad893a5ca8d81c7dc7cea4d08a16a1 Mon Sep 17 00:00:00 2001 From: phongsakon Date: Sun, 31 May 2026 11:52:47 +0200 Subject: [PATCH 3/5] Add JUnit 4 unit tests for alignment and distribution geometry Added JUnit 4 next to the existing TestNG so the lab's framework is in place without disturbing the current tests. The tests cover the pure geometry the refactoring exposed: 9 cases for Alignment, one per direction plus the already-aligned and oversized-figure boundaries, and 6 for Distribution, the equal-gap best case plus the two and one figure boundaries where it must do nothing. All 15 green. --- jhotdraw-core/pom.xml | 12 ++ .../jhotdraw/draw/action/AlignmentTest.java | 120 ++++++++++++++++++ .../draw/action/DistributionTest.java | 109 ++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/draw/action/AlignmentTest.java create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/draw/action/DistributionTest.java diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml index 7c276da85..9f6ad3a04 100644 --- a/jhotdraw-core/pom.xml +++ b/jhotdraw-core/pom.xml @@ -35,6 +35,18 @@ 6.8.21 test + + + junit + junit + 4.13.2 + test + ${project.groupId} jhotdraw-actions diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/AlignmentTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/AlignmentTest.java new file mode 100644 index 000000000..60a7cb104 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/AlignmentTest.java @@ -0,0 +1,120 @@ +/* + * AlignmentTest.java + * + * New file added for the align and distribute feature. + * JHotDraw is distributed under the GNU LGPL v2.1. + */ +package org.jhotdraw.draw.action; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Unit tests for the {@link Alignment} geometry. + * + *

These test the pure part of the align feature, the movement each direction + * works out. This is exactly what the refactoring made possible. Before, the + * same arithmetic was buried inside Swing action subclasses and could only be + * checked by clicking the real buttons. Now it is a plain method from two + * rectangles to a vector, so a test just sets up the input, calls the method, + * and checks the number, with no editor and no mocks.

+ * + *

Java notes to self. The {@code @Test} annotation marks a method JUnit runs. + * {@code assertEquals(expected, actual, EPS)} compares two doubles and the third + * argument is the tolerance, because comparing decimals for exact equality is + * unreliable. The fixture below is two boxes, a fixed selection and a smaller + * figure that moves, and each test comment states the expected delta.

+ */ +public class AlignmentTest { + + private static final double EPS = 1e-9; + + /** Selection bounds: x=0, y=0, width=100, height=100. */ + private Rectangle2D.Double selection() { + return new Rectangle2D.Double(0, 0, 100, 100); + } + + /** A 20 by 20 figure sitting near the middle, at (40, 40). */ + private Rectangle2D.Double figure() { + return new Rectangle2D.Double(40, 40, 20, 20); + } + + // ---- Best case, each direction moves the figure where it should ---- + + @Test + public void northMovesTopEdgeToSelectionTop() { + // top of selection is y=0, figure top is y=40, so dy = -40 and dx = 0 + Point2D.Double d = Alignment.NORTH.delta(figure(), selection()); + assertEquals(0, d.x, EPS); + assertEquals(-40, d.y, EPS); + } + + @Test + public void southMovesBottomEdgeToSelectionBottom() { + // selection bottom is y=100, figure bottom is 40+20=60, so dy = +40 + Point2D.Double d = Alignment.SOUTH.delta(figure(), selection()); + assertEquals(0, d.x, EPS); + assertEquals(40, d.y, EPS); + } + + @Test + public void westMovesLeftEdgeToSelectionLeft() { + // selection left is x=0, figure left is x=40, so dx = -40 + Point2D.Double d = Alignment.WEST.delta(figure(), selection()); + assertEquals(-40, d.x, EPS); + assertEquals(0, d.y, EPS); + } + + @Test + public void eastMovesRightEdgeToSelectionRight() { + // selection right is x=100, figure right is 40+20=60, so dx = +40 + Point2D.Double d = Alignment.EAST.delta(figure(), selection()); + assertEquals(40, d.x, EPS); + assertEquals(0, d.y, EPS); + } + + @Test + public void verticalCentresOnHorizontalMidline() { + // selection mid y = 50, figure centre y = 40 + 10 = 50, already centred + Point2D.Double d = Alignment.VERTICAL.delta(figure(), selection()); + assertEquals(0, d.x, EPS); + assertEquals(0, d.y, EPS); + } + + @Test + public void horizontalCentresOnVerticalMidline() { + // selection mid x = 50, figure centre x = 50, already centred + Point2D.Double d = Alignment.HORIZONTAL.delta(figure(), selection()); + assertEquals(0, d.x, EPS); + assertEquals(0, d.y, EPS); + } + + // ---- Boundary cases ---- + + @Test + public void alreadyAlignedFigureDoesNotMove() { + // A figure already flush with the top should get a zero delta for NORTH. + Rectangle2D.Double flushTop = new Rectangle2D.Double(10, 0, 20, 20); + Point2D.Double d = Alignment.NORTH.delta(flushTop, selection()); + assertEquals(0, d.x, EPS); + assertEquals(0, d.y, EPS); + } + + @Test + public void figureLargerThanSelectionStillProducesConsistentDelta() { + // A figure wider than the selection still lines its left edge up for WEST. + Rectangle2D.Double wide = new Rectangle2D.Double(5, 40, 200, 20); + Point2D.Double d = Alignment.WEST.delta(wide, selection()); + assertEquals(-5, d.x, EPS); + assertEquals(0, d.y, EPS); + } + + @Test + public void deltaIsNeverNull() { + for (Alignment a : Alignment.values()) { + assertEquals(false, a.delta(figure(), selection()) == null); + } + } +} diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/DistributionTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/DistributionTest.java new file mode 100644 index 000000000..b17c612fc --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/DistributionTest.java @@ -0,0 +1,109 @@ +/* + * DistributionTest.java + * + * New file added for the align and distribute feature. + * JHotDraw is distributed under the GNU LGPL v2.1. + */ +package org.jhotdraw.draw.action; + +import java.awt.geom.Rectangle2D; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; + +/** + * Unit tests for the {@link Distribution} geometry. + * + *

Distribution is the new feature, so it gets its own best case and boundary + * tests. The method under test maps a list of boxes to a list of new + * coordinates, so the tests check the returned array directly.

+ * + *

Java note to self. {@code assertArrayEquals(expected, actual, EPS)} checks + * two double arrays element by element within a tolerance. The best case fixes + * three boxes with an uneven gap and checks the middle one moves to even the + * spacing. The boundary cases are the two inputs where distribution must do + * nothing, a selection of two boxes and of one box, because there is no inner + * figure to move.

+ */ +public class DistributionTest { + + private static final double EPS = 1e-9; + + private Rectangle2D.Double box(double x, double y, double w, double h) { + return new Rectangle2D.Double(x, y, w, h); + } + + // ---- Best case ---- + + @Test + public void horizontalEqualisesGapsBetweenThreeBoxes() { + // Three 10 wide boxes. Left at x=0, right at x=100, middle bunched at 20. + // Total width = 30, span = 110, leftover = 80 over 2 gaps = 40 each. + // Positions become 0, then 0+10+40=50, then the end stays at 100. + List boxes = Arrays.asList( + box(0, 0, 10, 10), + box(20, 0, 10, 10), + box(100, 0, 10, 10)); + double[] xs = Distribution.HORIZONTAL.newPositions(boxes); + assertArrayEquals(new double[]{0, 50, 100}, xs, EPS); + } + + @Test + public void verticalEqualisesGapsBetweenThreeBoxes() { + // Same idea on the y axis. + List boxes = Arrays.asList( + box(0, 0, 10, 10), + box(0, 20, 10, 10), + box(0, 100, 10, 10)); + double[] ys = Distribution.VERTICAL.newPositions(boxes); + assertArrayEquals(new double[]{0, 50, 100}, ys, EPS); + } + + @Test + public void distributionKeepsTheOutermostBoxesFixed() { + List boxes = Arrays.asList( + box(0, 0, 10, 10), + box(33, 0, 10, 10), + box(70, 0, 10, 10), + box(100, 0, 10, 10)); + double[] xs = Distribution.HORIZONTAL.newPositions(boxes); + // first and last never move + assertEquals(0, xs[0], EPS); + assertEquals(100, xs[3], EPS); + } + + @Test + public void inputOrderDoesNotChangeTheResultMapping() { + // The same three boxes as the best case but supplied out of order. The + // result lines up with the input by index, so the box that started at + // x=20 (index 0 here) still ends up at the middle coordinate 50. + List boxes = Arrays.asList( + box(20, 0, 10, 10), + box(100, 0, 10, 10), + box(0, 0, 10, 10)); + double[] xs = Distribution.HORIZONTAL.newPositions(boxes); + assertEquals(50, xs[0], EPS); + assertEquals(100, xs[1], EPS); + assertEquals(0, xs[2], EPS); + } + + // ---- Boundary cases ---- + + @Test + public void twoBoxesAreLeftUnchanged() { + List boxes = Arrays.asList( + box(0, 0, 10, 10), + box(100, 0, 10, 10)); + double[] xs = Distribution.HORIZONTAL.newPositions(boxes); + assertArrayEquals(new double[]{0, 100}, xs, EPS); + } + + @Test + public void oneBoxIsLeftUnchanged() { + List boxes = Arrays.asList(box(42, 0, 10, 10)); + double[] xs = Distribution.HORIZONTAL.newPositions(boxes); + assertArrayEquals(new double[]{42}, xs, EPS); + } +} From 91e1c2102efdf312ac5c88a5a49397beb416a143 Mon Sep 17 00:00:00 2001 From: phongsakon Date: Sun, 31 May 2026 14:23:11 +0200 Subject: [PATCH 4/5] Add JGiven BDD scenarios for align and distribute Mapped the user story to Given-When-Then scenarios with JGiven and AssertJ. One stage holds the steps, and the three scenarios cover the story's promises: aligning north lines up the top edges, distributing makes the gaps equal, and distributing two figures changes nothing. --- jhotdraw-core/pom.xml | 15 +++ .../bdd/AlignDistributeScenarioTest.java | 58 +++++++++ .../draw/action/bdd/AlignDistributeStage.java | 115 ++++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/draw/action/bdd/AlignDistributeScenarioTest.java create mode 100644 jhotdraw-core/src/test/java/org/jhotdraw/draw/action/bdd/AlignDistributeStage.java diff --git a/jhotdraw-core/pom.xml b/jhotdraw-core/pom.xml index 9f6ad3a04..3f8182570 100644 --- a/jhotdraw-core/pom.xml +++ b/jhotdraw-core/pom.xml @@ -47,6 +47,21 @@ 4.13.2 test
+ + + com.tngtech.jgiven + jgiven-junit + 1.3.1 + test + + + org.assertj + assertj-core + 3.24.2 + test + ${project.groupId} jhotdraw-actions diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/bdd/AlignDistributeScenarioTest.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/bdd/AlignDistributeScenarioTest.java new file mode 100644 index 000000000..4f22fad05 --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/bdd/AlignDistributeScenarioTest.java @@ -0,0 +1,58 @@ +/* + * AlignDistributeScenarioTest.java + * + * New file added for the align and distribute feature. + * JHotDraw is distributed under the GNU LGPL v2.1. + */ +package org.jhotdraw.draw.action.bdd; + +import com.tngtech.jgiven.junit.SimpleScenarioTest; +import org.jhotdraw.draw.action.Alignment; +import org.jhotdraw.draw.action.Distribution; +import org.junit.Test; + +/** + * BDD scenarios for the align and distribute user story. + * + *

This maps the user story onto runnable scenarios. The story reads, as + * someone editing a drawing I want to align or distribute the figures I have + * selected so the diagram looks tidy. Each test below is one acceptance + * criterion written as Given a selection, When I run the command, Then the + * figures end up where the story promises.

+ * + *

Java note to self. Extending {@code SimpleScenarioTest} + * is what gives this class the {@code given()}, {@code when()}, and {@code then()} + * methods, each returning the one stage so the steps can be chained. I used the + * Simple variant because a single stage holds all three step groups.

+ */ +public class AlignDistributeScenarioTest extends SimpleScenarioTest { + + @Test + public void aligning_to_the_north_lines_up_the_top_edges() { + given().a_figure_at_x_$_y_$_with_width_$_height_$(0, 10, 20, 20) + .and().a_figure_at_x_$_y_$_with_width_$_height_$(40, 60, 20, 20) + .and().a_figure_at_x_$_y_$_with_width_$_height_$(80, 30, 20, 20) + .and().the_figures_are_selected(); + when().I_align_them_to_the(Alignment.NORTH); + then().every_figure_shares_the_same_top_edge(); + } + + @Test + public void distributing_horizontally_makes_the_gaps_equal() { + given().a_figure_at_x_$_y_$_with_width_$_height_$(0, 0, 10, 10) + .and().a_figure_at_x_$_y_$_with_width_$_height_$(20, 0, 10, 10) + .and().a_figure_at_x_$_y_$_with_width_$_height_$(100, 0, 10, 10) + .and().the_figures_are_selected(); + when().I_distribute_them(Distribution.HORIZONTAL); + then().the_gaps_between_neighbours_are_equal(); + } + + @Test + public void distributing_two_figures_changes_nothing() { + given().a_figure_at_x_$_y_$_with_width_$_height_$(0, 0, 10, 10) + .and().a_figure_at_x_$_y_$_with_width_$_height_$(100, 0, 10, 10) + .and().the_figures_are_selected(); + when().I_distribute_them(Distribution.HORIZONTAL); + then().the_positions_are_unchanged(); + } +} diff --git a/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/bdd/AlignDistributeStage.java b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/bdd/AlignDistributeStage.java new file mode 100644 index 000000000..8d21c1cbd --- /dev/null +++ b/jhotdraw-core/src/test/java/org/jhotdraw/draw/action/bdd/AlignDistributeStage.java @@ -0,0 +1,115 @@ +/* + * AlignDistributeStage.java + * + * New file added for the align and distribute feature. + * JHotDraw is distributed under the GNU LGPL v2.1. + */ +package org.jhotdraw.draw.action.bdd; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import com.tngtech.jgiven.annotation.Quoted; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.jhotdraw.draw.action.Alignment; +import org.jhotdraw.draw.action.Distribution; + +/** + * A single JGiven stage holding the Given, When, and Then steps for the align + * and distribute scenarios. + * + *

Where a unit test speaks in coordinates, BDD describes the feature in the + * words of the user story. Each public method below is one line of a + * Given-When-Then sentence, and JGiven turns the method names into a readable + * report. The stage holds the scenario state, the figures and the result, so the + * scenario class itself stays short and declarative.

+ * + *

Java notes to self. A JGiven stage is a normal class whose methods return + * {@code this} so they can be chained. {@code @ProvidedScenarioState} marks the + * fields JGiven shares between the steps. {@code @Quoted} just makes the argument + * show up in quotes in the generated report. One stage is enough here because + * the feature is small, a bigger feature would split Given, When, and Then into + * three stages.

+ */ +public class AlignDistributeStage extends Stage { + + @ProvidedScenarioState + private final List figures = new ArrayList<>(); + + @ProvidedScenarioState + private Rectangle2D.Double selectionBounds; + + @ProvidedScenarioState + private final List alignDeltas = new ArrayList<>(); + + @ProvidedScenarioState + private double[] distributedPositions; + + // ---- Given ---- + + public AlignDistributeStage a_figure_at_x_$_y_$_with_width_$_height_$( + @Quoted double x, @Quoted double y, @Quoted double w, @Quoted double h) { + figures.add(new Rectangle2D.Double(x, y, w, h)); + return this; + } + + public AlignDistributeStage the_figures_are_selected() { + selectionBounds = null; + for (Rectangle2D.Double f : figures) { + if (selectionBounds == null) { + selectionBounds = (Rectangle2D.Double) f.clone(); + } else { + selectionBounds.add(f); + } + } + return this; + } + + // ---- When ---- + + public AlignDistributeStage I_align_them_to_the(@Quoted Alignment alignment) { + alignDeltas.clear(); + for (Rectangle2D.Double f : figures) { + alignDeltas.add(alignment.delta(f, selectionBounds)); + } + return this; + } + + public AlignDistributeStage I_distribute_them(@Quoted Distribution distribution) { + distributedPositions = distribution.newPositions(figures); + return this; + } + + // ---- Then ---- + + public AlignDistributeStage every_figure_shares_the_same_top_edge() { + // Applying the NORTH delta should land every figure on the same y. + List tops = new ArrayList<>(); + for (int i = 0; i < figures.size(); i++) { + tops.add(figures.get(i).y + alignDeltas.get(i).y); + } + Assertions.assertThat(tops).allMatch(t -> Math.abs(t - tops.get(0)) < 1e-9); + return this; + } + + public AlignDistributeStage the_gaps_between_neighbours_are_equal() { + // Sort the new leading coordinates and check the gaps all match. + double[] sorted = distributedPositions.clone(); + java.util.Arrays.sort(sorted); + double firstGap = sorted[1] - sorted[0]; + for (int i = 2; i < sorted.length; i++) { + Assertions.assertThat(sorted[i] - sorted[i - 1]).isCloseTo(firstGap, Assertions.within(1e-9)); + } + return this; + } + + public AlignDistributeStage the_positions_are_unchanged() { + for (int i = 0; i < figures.size(); i++) { + Assertions.assertThat(distributedPositions[i]).isCloseTo(figures.get(i).x, Assertions.within(1e-9)); + } + return this; + } +} From 4c1b7045532472250a3c0d259c4009a10ecea4fc Mon Sep 17 00:00:00 2001 From: phongsakon Date: Sun, 31 May 2026 17:06:38 +0200 Subject: [PATCH 5/5] Add CI workflow for build and test on pull requests GitHub Actions pipeline that builds all modules and runs the tests on JDK 11 for every pull request. Named ci-align.yml so it does not clash with the ci.yml a teammate added on another branch. --- .github/workflows/ci-align.yml | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/ci-align.yml diff --git a/.github/workflows/ci-align.yml b/.github/workflows/ci-align.yml new file mode 100644 index 000000000..6cf48f291 --- /dev/null +++ b/.github/workflows/ci-align.yml @@ -0,0 +1,38 @@ +# Continuous integration for the align and distribute feature. +# +# Continuous integration means every change is built and tested automatically +# when it is proposed, so problems show up at the moment of the pull request +# instead of at merge time. This workflow builds the whole multi-module project +# and runs the tests on every pull request and on pushes to my feature branches. +# +# The file is named ci-align.yml on purpose. A teammate already added a ci.yml on +# another branch, so a separate name lets both pipelines run side by side and +# avoids a conflict on the workflow file when the branches merge. +name: CI Align and Distribute + +on: + pull_request: + push: + branches: + - "phongsakon/**" + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Check out the code + uses: actions/checkout@v4 + + # The project compiles at Java 1.8 source level but is built with JDK 11, + # so the runner uses Temurin 11 to match the local toolchain. + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "11" + cache: maven + + # A clean build of every module. Tests run by default, which is the point + # of the pipeline, so there is no -DskipTests here. + - name: Build and test with Maven + run: mvn --batch-mode --no-transfer-progress clean install