feat(ui): add box selection feature
- Selection: add addAll() method for bulk shape addition - Controller: box selection with mouse drag on empty space - ShapeDraftman: drawSelectionBox() for rubber-band rendering - ShapesView: currentSelectionBox field and setter
This commit is contained in:
@@ -9,6 +9,7 @@ import ovh.gasser.newshapes.util.Streamable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
@@ -38,6 +39,15 @@ public class Selection implements Streamable<Shape> {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public void addAll(Collection<Shape> shapes) {
|
||||
if (shapes == null) {
|
||||
return;
|
||||
}
|
||||
for (Shape shape : shapes) {
|
||||
add(shape);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return selectedShapes.isEmpty();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ public class Controller {
|
||||
private ResizeHandle activeHandle;
|
||||
private Point resizeOrigin;
|
||||
private boolean resizeMode;
|
||||
private Point selectionBoxStart;
|
||||
private Point selectionBoxEnd;
|
||||
private boolean boxSelecting;
|
||||
|
||||
Controller(ShapesView view, SCollection model) {
|
||||
this.view = view;
|
||||
@@ -54,6 +57,11 @@ public class Controller {
|
||||
resizing = false;
|
||||
activeHandle = null;
|
||||
resizeOrigin = null;
|
||||
boxSelecting = false;
|
||||
selectionBoxStart = null;
|
||||
selectionBoxEnd = null;
|
||||
view.setCurrentSelectionBox(null);
|
||||
view.repaint();
|
||||
}
|
||||
};
|
||||
this.view.addMouseMotionListener(adapter);
|
||||
@@ -67,10 +75,24 @@ public class Controller {
|
||||
}
|
||||
|
||||
private void handleMouseDragged(MouseEvent evt) {
|
||||
if (boxSelecting) {
|
||||
selectionBoxEnd = evt.getPoint();
|
||||
Rectangle box = createSelectionBox(selectionBoxStart, selectionBoxEnd);
|
||||
view.setCurrentSelectionBox(box);
|
||||
updateSelectionFromBox(box);
|
||||
view.repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
boxSelecting = false;
|
||||
|
||||
if (resizeMode && resizing && activeHandle != null) {
|
||||
logger.debug("RESIZING with handle {} at ({}, {})", activeHandle, evt.getX(), evt.getY());
|
||||
int dx = evt.getX() - resizeOrigin.x;
|
||||
int dy = evt.getY() - resizeOrigin.y;
|
||||
logger.debug("About to resize {} shapes, dx={}, dy={}", selection.getSelectedShapes().size(), dx, dy);
|
||||
for (Shape shape : selection) {
|
||||
logger.debug("Calling resize on shape: {}", shape);
|
||||
shape.resize(activeHandle, dx, dy);
|
||||
}
|
||||
resizeOrigin = evt.getPoint();
|
||||
@@ -85,8 +107,12 @@ public class Controller {
|
||||
} else {
|
||||
resizing = false;
|
||||
activeHandle = null;
|
||||
if (lastMousePos == null) {
|
||||
lastMousePos = evt.getPoint();
|
||||
}
|
||||
int dx = evt.getX() - lastMousePos.x;
|
||||
int dy = evt.getY() - lastMousePos.y;
|
||||
logger.debug("MOVING shapes with delta ({}, {})", dx, dy);
|
||||
for (Shape shape : selection) {
|
||||
shape.translate(dx, dy);
|
||||
}
|
||||
@@ -104,9 +130,11 @@ public class Controller {
|
||||
resizing = false;
|
||||
activeHandle = null;
|
||||
resizeOrigin = null;
|
||||
boxSelecting = false; // Clear box selection mode when clicking
|
||||
|
||||
if (resizeMode && !selection.isEmpty()) {
|
||||
ResizeHandle handle = getHandleAt(evt.getPoint());
|
||||
logger.debug("In resize mode, checking handle at ({}, {}): {}", evt.getX(), evt.getY(), handle);
|
||||
if (handle != null) {
|
||||
resizing = true;
|
||||
activeHandle = handle;
|
||||
@@ -122,18 +150,19 @@ public class Controller {
|
||||
if (!evt.isShiftDown()) {
|
||||
resetSelection();
|
||||
}
|
||||
if (!selection.getSelectedShapes().contains(s)) {
|
||||
lastMousePos = evt.getPoint();
|
||||
selection.add(s);
|
||||
logger.debug("Selecting {}", s);
|
||||
},
|
||||
this::resetSelection
|
||||
);
|
||||
view.repaint();
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
public void enterTextMode() {
|
||||
addingText = true;
|
||||
},
|
||||
() -> {
|
||||
// Clicked on empty space - start box selection
|
||||
resetSelection();
|
||||
selectionBoxStart = evt.getPoint();
|
||||
boxSelecting = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void placeTextAt(Point point) {
|
||||
@@ -153,20 +182,56 @@ public class Controller {
|
||||
}
|
||||
|
||||
private ResizeHandle getHandleAt(Point point) {
|
||||
final int handleSize = 5;
|
||||
final int handleSize = 10;
|
||||
for (Shape shape : selection) {
|
||||
Rectangle bounds = shape.getBounds();
|
||||
int cx = bounds.x + bounds.width / 2;
|
||||
int cy = bounds.y + bounds.height / 2;
|
||||
|
||||
if (point.x < cx && point.y < cy) return ResizeHandle.NW;
|
||||
if (point.x > cx && point.y < cy) return ResizeHandle.NE;
|
||||
if (point.x < cx && point.y > cy) return ResizeHandle.SW;
|
||||
if (point.x > cx && point.y > cy) return ResizeHandle.SE;
|
||||
if (point.y < cy) return ResizeHandle.N;
|
||||
if (point.y > cy) return ResizeHandle.S;
|
||||
if (point.x < cx) return ResizeHandle.W;
|
||||
if (point.x > cx) return ResizeHandle.E;
|
||||
// Always-present SE corner handle
|
||||
if (point.x >= bounds.x + bounds.width && point.x <= bounds.x + bounds.width + handleSize &&
|
||||
point.y >= bounds.y + bounds.height && point.y <= bounds.y + bounds.height + handleSize) {
|
||||
return ResizeHandle.SE;
|
||||
}
|
||||
|
||||
// Resize handles are only active in resize mode
|
||||
if (resizeMode) {
|
||||
// NW corner
|
||||
if (point.x >= bounds.x - handleSize && point.x <= bounds.x &&
|
||||
point.y >= bounds.y - handleSize && point.y <= bounds.y) {
|
||||
return ResizeHandle.NW;
|
||||
}
|
||||
// NE corner
|
||||
if (point.x >= bounds.x + bounds.width && point.x <= bounds.x + bounds.width + handleSize &&
|
||||
point.y >= bounds.y - handleSize && point.y <= bounds.y) {
|
||||
return ResizeHandle.NE;
|
||||
}
|
||||
// SW corner
|
||||
if (point.x >= bounds.x - handleSize && point.x <= bounds.x &&
|
||||
point.y >= bounds.y + bounds.height && point.y <= bounds.y + bounds.height + handleSize) {
|
||||
return ResizeHandle.SW;
|
||||
}
|
||||
// N edge
|
||||
int edgeX = bounds.x + bounds.width / 2 - handleSize / 2;
|
||||
if (point.x >= edgeX && point.x <= edgeX + handleSize &&
|
||||
point.y >= bounds.y - handleSize && point.y <= bounds.y) {
|
||||
return ResizeHandle.N;
|
||||
}
|
||||
// S edge
|
||||
if (point.x >= edgeX && point.x <= edgeX + handleSize &&
|
||||
point.y >= bounds.y + bounds.height && point.y <= bounds.y + bounds.height + handleSize) {
|
||||
return ResizeHandle.S;
|
||||
}
|
||||
// W edge
|
||||
int edgeY = bounds.y + bounds.height / 2 - handleSize / 2;
|
||||
if (point.x >= bounds.x - handleSize && point.x <= bounds.x &&
|
||||
point.y >= edgeY && point.y <= edgeY + handleSize) {
|
||||
return ResizeHandle.W;
|
||||
}
|
||||
// E edge
|
||||
if (point.x >= bounds.x + bounds.width && point.x <= bounds.x + bounds.width + handleSize &&
|
||||
point.y >= edgeY && point.y <= edgeY + handleSize) {
|
||||
return ResizeHandle.E;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -253,4 +318,19 @@ public class Controller {
|
||||
.filter(s -> s.getBounds().contains(evt.getPoint()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private Rectangle createSelectionBox(Point start, Point end) {
|
||||
int x = Math.min(start.x, end.x);
|
||||
int y = Math.min(start.y, end.y);
|
||||
int width = Math.abs(end.x - start.x);
|
||||
int height = Math.abs(end.y - start.y);
|
||||
return new Rectangle(x, y, width, height);
|
||||
}
|
||||
|
||||
private void updateSelectionFromBox(Rectangle box) {
|
||||
resetSelection();
|
||||
model.stream()
|
||||
.filter(s -> s.getBounds().intersects(box))
|
||||
.forEach(selection::add);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ public class ShapeDraftman implements ShapeVisitor {
|
||||
if ((selAttrs != null) && (selAttrs.selected)){
|
||||
Rectangle bounds = s.getBounds();
|
||||
this.g2d.setColor(Color.RED);
|
||||
int handleSize = 5;
|
||||
int handleSize = 10;
|
||||
this.g2d.drawRect(bounds.x - handleSize, bounds.y - handleSize, handleSize, handleSize);
|
||||
this.g2d.drawRect(bounds.x + bounds.width, bounds.y + bounds.height, handleSize, handleSize);
|
||||
if (resizeMode) {
|
||||
@@ -149,4 +149,14 @@ public class ShapeDraftman implements ShapeVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
public void drawSelectionBox(Rectangle box) {
|
||||
if (box == null) return;
|
||||
g2d.setXORMode(Color.WHITE);
|
||||
g2d.setColor(Color.BLUE);
|
||||
g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
|
||||
BasicStroke.JOIN_MITER, 1, new float[]{4, 4}, 0));
|
||||
g2d.drawRect(box.x, box.y, box.width, box.height);
|
||||
g2d.setPaintMode();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public class ShapesView extends JPanel {
|
||||
private final Controller controller;
|
||||
private ShapeVisitor draftman;
|
||||
private boolean resizeMode;
|
||||
private Rectangle currentSelectionBox;
|
||||
|
||||
public ShapesView(SCollection model) {
|
||||
this.model = model;
|
||||
@@ -25,6 +26,7 @@ public class ShapesView extends JPanel {
|
||||
this.draftman = new ShapeDraftman(g);
|
||||
((ShapeDraftman) this.draftman).setResizeMode(resizeMode);
|
||||
model.accept(draftman);
|
||||
((ShapeDraftman) this.draftman).drawSelectionBox(currentSelectionBox);
|
||||
}
|
||||
|
||||
public Controller getController() {
|
||||
@@ -38,4 +40,8 @@ public class ShapesView extends JPanel {
|
||||
public void setResizeMode(boolean resizeMode) {
|
||||
this.resizeMode = resizeMode;
|
||||
}
|
||||
|
||||
public void setCurrentSelectionBox(Rectangle box) {
|
||||
this.currentSelectionBox = box;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user