Skip to content

Commit 69db7eb

Browse files
committed
undo/redo for crop box resizing
1 parent e8d256e commit 69db7eb

File tree

10 files changed

+397
-91
lines changed

10 files changed

+397
-91
lines changed

src/main/java/pixelitor/Canvas.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ public Rectangle intersect(Rectangle rectangle) {
255255
);
256256
}
257257

258+
public Rectangle2D intersect(Rectangle2D rectangle) {
259+
return rectangle.createIntersection(getBounds());
260+
}
261+
258262
/**
259263
* Creates a temporary, transparent image with the size of this canvas.
260264
*/

src/main/java/pixelitor/compactions/Crop.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import pixelitor.history.CompositionReplacedEdit;
2929
import pixelitor.history.History;
3030
import pixelitor.history.MultiEdit;
31+
import pixelitor.history.PixelitorEdit;
3132
import pixelitor.layers.Layer;
3233
import pixelitor.layers.SmartObject;
3334
import pixelitor.selection.Selection;
@@ -63,18 +64,21 @@ public class Crop implements CompAction {
6364

6465
// whether a mask should be added to hide cropped areas
6566
private final boolean addMaskForHiding;
67+
private final PixelitorEdit cropBoxRestorationEdit;
6668

6769
/**
6870
* Configures a new crop operation.
6971
*/
7072
public Crop(Rectangle2D imCropRect,
7173
boolean fromSelection, boolean allowGrowing,
72-
boolean deleteCroppedPixels, boolean addMaskForHiding) {
74+
boolean deleteCroppedPixels, boolean addMaskForHiding,
75+
PixelitorEdit cropBoxRestorationEdit) {
7376
this.imCropRect = imCropRect;
7477
this.fromSelection = fromSelection;
7578
this.allowGrowing = allowGrowing;
7679
this.deleteCroppedPixels = deleteCroppedPixels;
7780
this.addMaskForHiding = addMaskForHiding;
81+
this.cropBoxRestorationEdit = cropBoxRestorationEdit;
7882
}
7983

8084
/**
@@ -155,8 +159,17 @@ public CompletableFuture<Composition> process(Composition srcComp) {
155159
view.ensurePositiveLocation();
156160

157161
String editName = addMaskForHiding ? "Crop and Hide" : "Crop";
158-
History.add(new CompositionReplacedEdit(
159-
editName, view, srcComp, croppedComp, canvasTransform, false));
162+
CompositionReplacedEdit compReplacedEdit = new CompositionReplacedEdit(
163+
editName, view, srcComp, croppedComp, canvasTransform, false);
164+
if (cropBoxRestorationEdit != null) {
165+
// this crop originated from the crop tool
166+
History.add(new MultiEdit(editName, croppedComp,
167+
cropBoxRestorationEdit, compReplacedEdit));
168+
} else {
169+
// crop from other sources (selection, content trim), no crop box to manage
170+
History.add(compReplacedEdit);
171+
}
172+
160173
view.replaceComp(croppedComp);
161174

162175
croppedComp.updateAllIconImages();
@@ -177,9 +190,11 @@ public CompletableFuture<Composition> process(Composition srcComp) {
177190
public static void toolCrop(Composition comp,
178191
Rectangle2D cropRect,
179192
boolean allowGrowing,
180-
boolean deleteCroppedPixels) {
193+
boolean deleteCroppedPixels,
194+
PixelitorEdit cropBoxRestorationEdit) {
181195
new Crop(cropRect, false, allowGrowing,
182-
deleteCroppedPixels, false).process(comp);
196+
deleteCroppedPixels, false,
197+
cropBoxRestorationEdit).process(comp);
183198
}
184199

185200
/**
@@ -194,7 +209,7 @@ public static void contentCrop(Composition comp) {
194209
Messages.showInfo("Nothing to Crop",
195210
"<html><b>%s</b> has no transparent border pixels to remove.".formatted(comp.getName()));
196211
} else {
197-
new Crop(bounds, false, false, true, false)
212+
new Crop(bounds, false, false, true, false, null)
198213
.process(comp);
199214
}
200215
}
@@ -265,7 +280,7 @@ private static void rectangularSelectionCrop(Composition comp,
265280
Selection sel,
266281
boolean addHidingMask) {
267282
new Crop(sel.getShapeBounds2D(), true,
268-
true, true, addHidingMask).process(comp);
283+
true, true, addHidingMask, null).process(comp);
269284
}
270285

271286
/**

src/main/java/pixelitor/history/PixelitorEdit.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 Laszlo Balazs-Csiki and Contributors
2+
* Copyright 2025 Laszlo Balazs-Csiki and Contributors
33
*
44
* This file is part of Pixelitor. Pixelitor is free software: you
55
* can redistribute it and/or modify it under the terms of the GNU
@@ -94,7 +94,13 @@ public void redo() throws CannotRedoException {
9494
}
9595

9696
if (!comp.isOpen()) {
97-
throw new CannotRedoException();
97+
if (this instanceof MultiEdit) {
98+
// make the redo of crop tool cropping work: comp
99+
// is the after-crop composition, not the open one
100+
return;
101+
} else {
102+
throw new CannotUndoException();
103+
}
98104
}
99105

100106
try {

src/main/java/pixelitor/tools/DragToolState.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public boolean isOK(ShapesTool tool) {
3535

3636
@Override
3737
public boolean isOK(CropTool tool) {
38-
return !tool.hasCropBox();
38+
return !tool.hasCropBox() && !tool.isCropEnabled();
3939
}
4040
},
4141
AFTER_FIRST_MOUSE_PRESS {
@@ -74,7 +74,7 @@ public boolean isOK(ShapesTool tool) {
7474

7575
@Override
7676
public boolean isOK(CropTool tool) {
77-
return tool.hasCropBox();
77+
return tool.hasCropBox() && tool.isCropEnabled();
7878
}
7979
};
8080

src/main/java/pixelitor/tools/crop/CropBox.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import pixelitor.tools.util.PRectangle;
2525
import pixelitor.utils.Cursors;
2626
import pixelitor.utils.Shapes;
27+
import pixelitor.utils.debug.DebugNode;
28+
import pixelitor.utils.debug.DebugNodes;
29+
import pixelitor.utils.debug.Debuggable;
2730

2831
import java.awt.Graphics2D;
2932
import java.awt.Point;
@@ -45,7 +48,7 @@
4548
/**
4649
* The cropping widget with draggable handles for resizing the crop area.
4750
*/
48-
public class CropBox implements ToolWidget {
51+
public class CropBox implements ToolWidget, Debuggable {
4952
// transformation modes for user interactions
5053
private static final int MODE_NONE = 0;
5154
private static final int MODE_MOVE = 1;
@@ -262,6 +265,14 @@ public PRectangle getCropRect() {
262265
return cropRect;
263266
}
264267

268+
/**
269+
* Captures the internal state of this {@link CropBox}
270+
* so that it can be returned to this state later.
271+
*/
272+
public Rectangle2D getImCropRect() {
273+
return (Rectangle2D) cropRect.getIm().clone();
274+
}
275+
265276
@Override
266277
public void coCoordsChanged(View view) {
267278
cropRect.coCoordsChanged(view);
@@ -442,5 +453,15 @@ public static void keepAspectRatio(Rectangle rect, int cursor,
442453
rect.setRect(co);
443454
}
444455
}
456+
457+
@Override
458+
public DebugNode createDebugNode(String key) {
459+
DebugNode node = new DebugNode(key, this);
460+
461+
node.add(DebugNodes.createRectangle2DNode("cropRect im", cropRect.getIm()));
462+
node.add(DebugNodes.createRectangleNode("cropRect co", cropRect.getCo()));
463+
464+
return node;
465+
}
445466
}
446467

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2025 Laszlo Balazs-Csiki and Contributors
3+
*
4+
* This file is part of Pixelitor. Pixelitor is free software: you
5+
* can redistribute it and/or modify it under the terms of the GNU
6+
* General Public License, version 3 as published by the Free
7+
* Software Foundation.
8+
*
9+
* Pixelitor is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with Pixelitor. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package pixelitor.tools.crop;
19+
20+
import pixelitor.Composition;
21+
import pixelitor.gui.View;
22+
import pixelitor.history.PixelitorEdit;
23+
import pixelitor.tools.Tools;
24+
25+
import javax.swing.undo.CannotRedoException;
26+
import javax.swing.undo.CannotUndoException;
27+
import java.awt.geom.Rectangle2D;
28+
29+
/**
30+
* Represents a change to the {@link CropBox} in the {@link CropTool}.
31+
*/
32+
public class CropBoxChangedEdit extends PixelitorEdit {
33+
// the crop box rectangle in image space before and after the change
34+
private final Rectangle2D rectBefore; // null if box creation
35+
private final Rectangle2D rectAfter; // null if box dismissal
36+
37+
private final boolean allowGrowingBefore;
38+
private final boolean allowGrowingAfter;
39+
40+
public CropBoxChangedEdit(String name, Composition comp,
41+
Rectangle2D rectBefore, Rectangle2D rectAfter,
42+
boolean allowGrowingBefore, boolean allowGrowingAfter) {
43+
super(name, comp);
44+
this.rectBefore = rectBefore;
45+
this.rectAfter = rectAfter;
46+
this.allowGrowingBefore = allowGrowingBefore;
47+
this.allowGrowingAfter = allowGrowingAfter;
48+
}
49+
50+
@Override
51+
public void undo() throws CannotUndoException {
52+
super.undo();
53+
restoreState(rectBefore, allowGrowingBefore);
54+
}
55+
56+
@Override
57+
public void redo() throws CannotRedoException {
58+
super.redo();
59+
restoreState(rectAfter, allowGrowingAfter);
60+
}
61+
62+
private void restoreState(Rectangle2D rectToRestore, boolean allowGrowingSetting) {
63+
View view = comp.getView();
64+
65+
Tools.CROP.setAllowGrowingUndoRedo(allowGrowingSetting);
66+
67+
if (rectToRestore == null) {
68+
Tools.CROP.clearBoxForUndoRedo();
69+
} else {
70+
Tools.CROP.setBoxForUndoRedo(rectToRestore, view);
71+
}
72+
73+
Tools.CROP.updateUIFromState(view);
74+
view.repaint();
75+
}
76+
}

0 commit comments

Comments
 (0)