mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
orthogonal shift drag
This commit is contained in:
@@ -59,8 +59,11 @@ public interface IMouseProcessor {
|
|||||||
* @param point
|
* @param point
|
||||||
* @param dragAction
|
* @param dragAction
|
||||||
* @param forceSelectionRect
|
* @param forceSelectionRect
|
||||||
|
* @param shiftDown when true, the whole drag is constrained to the horizontal or vertical
|
||||||
|
* direction of the initial gesture. Shift alone maps to the same drag action as no
|
||||||
|
* modifier, so it has to be passed separately.
|
||||||
*/
|
*/
|
||||||
void dragStarted(Point point, int dragAction, boolean forceSelectionRect);
|
void dragStarted(Point point, int dragAction, boolean forceSelectionRect, boolean shiftDown);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if it's possible to drop over the specified point.
|
* Checks if it's possible to drop over the specified point.
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ public class Presenter implements IPlugInPort {
|
|||||||
|
|
||||||
public static final int ICON_SIZE = 32;
|
public static final int ICON_SIZE = 32;
|
||||||
|
|
||||||
private static final int MAX_RECENT_FILES = 20;
|
private static final int MAX_RECENT_FILES = 20;
|
||||||
|
|
||||||
|
// screen pixels the cursor has to travel with shift held before the dominant direction is
|
||||||
|
// picked, so that hand jitter right after pressing shift does not choose the axis
|
||||||
|
private static final int AXIS_LOCK_THRESHOLD = 4;
|
||||||
|
|
||||||
private Project currentProject;
|
private Project currentProject;
|
||||||
|
|
||||||
@@ -160,6 +164,12 @@ public class Presenter implements IPlugInPort {
|
|||||||
private Project preDragProject = null;
|
private Project preDragProject = null;
|
||||||
private int dragAction;
|
private int dragAction;
|
||||||
private Point2D previousScaledPoint;
|
private Point2D previousScaledPoint;
|
||||||
|
// Shift held when a drag starts constrains it to the horizontal or vertical line through the
|
||||||
|
// drag start point for the whole drag. The state is not followed mid-drag because native DnD on
|
||||||
|
// macOS keeps reporting the modifiers captured at the start.
|
||||||
|
private Point2D dragStartPoint = null;
|
||||||
|
private boolean dragShiftDown = false;
|
||||||
|
private AxisLock lockedAxis = null;
|
||||||
|
|
||||||
private DIYTest test = null;
|
private DIYTest test = null;
|
||||||
|
|
||||||
@@ -659,7 +669,9 @@ public class Presenter implements IPlugInPort {
|
|||||||
} else {
|
} else {
|
||||||
// On the second click, add the component to the
|
// On the second click, add the component to the
|
||||||
// project.
|
// project.
|
||||||
addPendingComponentsToProject(scaledPoint, componentTypeSlot, template, model, oldProject);
|
addPendingComponentsToProject(
|
||||||
|
constrainToAxis(instantiationManager.getFirstControlPoint(), scaledPoint, shiftDown),
|
||||||
|
componentTypeSlot, template, model, oldProject);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -916,7 +928,8 @@ public class Presenter implements IPlugInPort {
|
|||||||
instantiationManager.updateSingleClick(previousScaledPoint, isSnapToGrid(),
|
instantiationManager.updateSingleClick(previousScaledPoint, isSnapToGrid(),
|
||||||
currentProject.getGridSpacing());
|
currentProject.getGridSpacing());
|
||||||
} else {
|
} else {
|
||||||
refresh = instantiationManager.updatePointByPoint(previousScaledPoint);
|
refresh = instantiationManager.updatePointByPoint(
|
||||||
|
constrainToAxis(instantiationManager.getFirstControlPoint(), previousScaledPoint, shiftDown));
|
||||||
}
|
}
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
messageDispatcher.dispatchMessage(EventType.REPAINT);
|
messageDispatcher.dispatchMessage(EventType.REPAINT);
|
||||||
@@ -1282,15 +1295,16 @@ public class Presenter implements IPlugInPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dragStarted(Point point, int dragAction, boolean forceSelectionRect) {
|
public void dragStarted(Point point, int dragAction, boolean forceSelectionRect, boolean shiftDown) {
|
||||||
LOG.debug(String.format("dragStarted(%s, %s)", point, dragAction));
|
LOG.debug(String.format("dragStarted(%s, %s, %s)", point, dragAction, shiftDown));
|
||||||
|
|
||||||
// record a test step if needed
|
// record a test step if needed
|
||||||
if (test != null) {
|
if (test != null) {
|
||||||
Map<String, Object> params = new HashMap<String, Object>();
|
Map<String, Object> params = new HashMap<String, Object>();
|
||||||
params.put("point", point);
|
params.put("point", point);
|
||||||
params.put("dragAction", dragAction);
|
params.put("dragAction", dragAction);
|
||||||
params.put("forceSelectionRect", forceSelectionRect);
|
params.put("forceSelectionRect", forceSelectionRect);
|
||||||
|
params.put("shiftDown", shiftDown);
|
||||||
test.addStep(DIYTest.DRAG_START, params);
|
test.addStep(DIYTest.DRAG_START, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1311,6 +1325,9 @@ public class Presenter implements IPlugInPort {
|
|||||||
this.preDragProject = currentProject.clone();
|
this.preDragProject = currentProject.clone();
|
||||||
Point2D scaledPoint = scalePoint(point);
|
Point2D scaledPoint = scalePoint(point);
|
||||||
this.previousDragPoint = scaledPoint;
|
this.previousDragPoint = scaledPoint;
|
||||||
|
this.dragStartPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY());
|
||||||
|
this.dragShiftDown = shiftDown;
|
||||||
|
this.lockedAxis = null;
|
||||||
List<IDIYComponent<?>> components = forceSelectionRect ? null : findComponentsAtScaled(scaledPoint, false);
|
List<IDIYComponent<?>> components = forceSelectionRect ? null : findComponentsAtScaled(scaledPoint, false);
|
||||||
if (controlPointMap != null && !this.controlPointMap.isEmpty()) {
|
if (controlPointMap != null && !this.controlPointMap.isEmpty()) {
|
||||||
// If we're dragging control points reset selection.
|
// If we're dragging control points reset selection.
|
||||||
@@ -1498,12 +1515,15 @@ public class Presenter implements IPlugInPort {
|
|||||||
Point2D scaledPoint = scalePoint(point);
|
Point2D scaledPoint = scalePoint(point);
|
||||||
if (controlPointMap != null && !controlPointMap.isEmpty()) {
|
if (controlPointMap != null && !controlPointMap.isEmpty()) {
|
||||||
// We're dragging control point(s).
|
// We're dragging control point(s).
|
||||||
int dx = (int) (scaledPoint.getX() - previousDragPoint.getX());
|
Point2D targetPoint = constrainToAxis(dragStartPoint, scaledPoint, dragShiftDown);
|
||||||
int dy = (int) (scaledPoint.getY() - previousDragPoint.getY());
|
int dx = (int) (targetPoint.getX() - previousDragPoint.getX());
|
||||||
|
int dy = (int) (targetPoint.getY() - previousDragPoint.getY());
|
||||||
|
|
||||||
Point2D actualD = moveComponents(this.controlPointMap, dx, dy, isSnapToGrid(), isSnapToObjects());
|
Point2D actualD = moveComponents(this.controlPointMap, dx, dy, isSnapToGrid(), isSnapToObjects(),
|
||||||
if (actualD == null)
|
lockedAxis);
|
||||||
|
if (actualD == null) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// if there's at least one component with only one control point being dragged it means that we are resizing
|
// if there's at least one component with only one control point being dragged it means that we are resizing
|
||||||
if (controlPointMap.size() == 1 && controlPointMap.entrySet().stream()
|
if (controlPointMap.size() == 1 && controlPointMap.entrySet().stream()
|
||||||
@@ -1573,7 +1593,36 @@ public class Presenter implements IPlugInPort {
|
|||||||
return new ResizeDimensions(width, height, length);
|
return new ResizeDimensions(width, height, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Projects the point onto the horizontal or vertical line through the anchor while shift is held.
|
||||||
|
* The axis is picked from the dominant direction of the gesture once it leaves the dead zone and
|
||||||
|
* stays locked until shift is released.
|
||||||
|
*/
|
||||||
|
private Point2D constrainToAxis(Point2D anchor, Point2D point, boolean shiftDown) {
|
||||||
|
if (!shiftDown || anchor == null || point == null) {
|
||||||
|
lockedAxis = null;
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
double dx = point.getX() - anchor.getX();
|
||||||
|
double dy = point.getY() - anchor.getY();
|
||||||
|
if (lockedAxis == null) {
|
||||||
|
if (Math.max(Math.abs(dx), Math.abs(dy)) < AXIS_LOCK_THRESHOLD / drawingManager.getZoomLevel()) {
|
||||||
|
return new Point2D.Double(anchor.getX(), anchor.getY());
|
||||||
|
}
|
||||||
|
lockedAxis = Math.abs(dx) >= Math.abs(dy) ? AxisLock.HORIZONTAL : AxisLock.VERTICAL;
|
||||||
|
}
|
||||||
|
if (lockedAxis == AxisLock.HORIZONTAL) {
|
||||||
|
return new Point2D.Double(point.getX(), anchor.getY());
|
||||||
|
}
|
||||||
|
return new Point2D.Double(anchor.getX(), point.getY());
|
||||||
|
}
|
||||||
|
|
||||||
private Point2D moveComponents(Map<IDIYComponent<?>, Set<Integer>> controlPointMap, int dx, int dy, boolean snapToGrid, boolean snapToObjects) {
|
private Point2D moveComponents(Map<IDIYComponent<?>, Set<Integer>> controlPointMap, int dx, int dy, boolean snapToGrid, boolean snapToObjects) {
|
||||||
|
return moveComponents(controlPointMap, dx, dy, snapToGrid, snapToObjects, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Point2D moveComponents(Map<IDIYComponent<?>, Set<Integer>> controlPointMap, int dx, int dy,
|
||||||
|
boolean snapToGrid, boolean snapToObjects, AxisLock lockedAxis) {
|
||||||
// After we make the transfer and snap to grid, calculate actual dx
|
// After we make the transfer and snap to grid, calculate actual dx
|
||||||
// and dy. We'll use them to translate the previous drag point.
|
// and dy. We'll use them to translate the previous drag point.
|
||||||
double actualDx = 0;
|
double actualDx = 0;
|
||||||
@@ -1620,6 +1669,13 @@ public class Presenter implements IPlugInPort {
|
|||||||
actualDy = dy;
|
actualDy = dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// snapping an off-grid point would otherwise nudge it across the locked axis
|
||||||
|
if (lockedAxis == AxisLock.HORIZONTAL && dy == 0) {
|
||||||
|
actualDy = 0;
|
||||||
|
} else if (lockedAxis == AxisLock.VERTICAL && dx == 0) {
|
||||||
|
actualDx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (actualDx == 0 && actualDy == 0) {
|
if (actualDx == 0 && actualDy == 0) {
|
||||||
// Nothing to move.
|
// Nothing to move.
|
||||||
return null;
|
return null;
|
||||||
@@ -1887,6 +1943,9 @@ public class Presenter implements IPlugInPort {
|
|||||||
|
|
||||||
messageDispatcher.dispatchMessage(EventType.REPAINT);
|
messageDispatcher.dispatchMessage(EventType.REPAINT);
|
||||||
dragInProgress = false;
|
dragInProgress = false;
|
||||||
|
dragStartPoint = null;
|
||||||
|
dragShiftDown = false;
|
||||||
|
lockedAxis = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -3198,4 +3257,8 @@ public class Presenter implements IPlugInPort {
|
|||||||
public Set<ComponentGroup> getComponentGroups() {
|
public Set<ComponentGroup> getComponentGroups() {
|
||||||
return currentProject.getGroupsEx();
|
return currentProject.getGroupsEx();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum AxisLock {
|
||||||
|
HORIZONTAL, VERTICAL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ public class TestPlayer {
|
|||||||
res.add(new StepResult(i, step.getAction(), "OK", false));
|
res.add(new StepResult(i, step.getAction(), "OK", false));
|
||||||
break;
|
break;
|
||||||
case DIYTest.DRAG_START:
|
case DIYTest.DRAG_START:
|
||||||
plugInPort.dragStarted((Point)step.getParams().get("point"), (int)step.getParams().get("dragAction"), (boolean)step.getParams().get("forceSelectionRect"));
|
// tests recorded before shift was passed in carry no value for it
|
||||||
|
plugInPort.dragStarted((Point)step.getParams().get("point"), (int)step.getParams().get("dragAction"), (boolean)step.getParams().get("forceSelectionRect"),
|
||||||
|
Boolean.TRUE.equals(step.getParams().get("shiftDown")));
|
||||||
res.add(new StepResult(i, step.getAction(), "OK", false));
|
res.add(new StepResult(i, step.getAction(), "OK", false));
|
||||||
break;
|
break;
|
||||||
case DIYTest.DRAG_OVER:
|
case DIYTest.DRAG_OVER:
|
||||||
|
|||||||
@@ -589,6 +589,10 @@
|
|||||||
<changeType>NEW_FEATURE</changeType>
|
<changeType>NEW_FEATURE</changeType>
|
||||||
<description>Added 'Align & Distribute' to the Edit menu and the right-click menu - align the selected components by their left, right, top or bottom edges or their centers, or space them evenly by their centers or by the gaps between them</description>
|
<description>Added 'Align & Distribute' to the Edit menu and the right-click menu - align the selected components by their left, right, top or bottom edges or their centers, or space them evenly by their centers or by the gaps between them</description>
|
||||||
</org.diylc.appframework.update.Change>
|
</org.diylc.appframework.update.Change>
|
||||||
|
<org.diylc.appframework.update.Change>
|
||||||
|
<changeType>NEW_FEATURE</changeType>
|
||||||
|
<description>Holding 'Shift' when starting to drag components or control points keeps the whole drag strictly horizontal or vertical, in the direction of the initial gesture. Holding 'Shift' while drawing a new wire or two-point component does the same</description>
|
||||||
|
</org.diylc.appframework.update.Change>
|
||||||
</changes>
|
</changes>
|
||||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||||
</org.diylc.appframework.update.Version>
|
</org.diylc.appframework.update.Version>
|
||||||
|
|||||||
+2
-1
@@ -54,7 +54,8 @@ class CanvasDragGestureListener implements DragGestureListener {
|
|||||||
forceReSelection = me.getButton() != MouseEvent.BUTTON1;
|
forceReSelection = me.getButton() != MouseEvent.BUTTON1;
|
||||||
}
|
}
|
||||||
canvasPanel.setClickInProgress(false);
|
canvasPanel.setClickInProgress(false);
|
||||||
presenter.dragStarted(dge.getDragOrigin(), dge.getDragAction(), forceReSelection);
|
presenter.dragStarted(dge.getDragOrigin(), dge.getDragAction(), forceReSelection,
|
||||||
|
e.isShiftDown());
|
||||||
// Get the appropriate cursor from the presenter (e.g., HAND_CURSOR over components)
|
// Get the appropriate cursor from the presenter (e.g., HAND_CURSOR over components)
|
||||||
// Preserve the cursor context (HAND_CURSOR, CROSSHAIR_CURSOR, etc.) for drag operations
|
// Preserve the cursor context (HAND_CURSOR, CROSSHAIR_CURSOR, etc.) for drag operations
|
||||||
// On Linux, DEFAULT_CURSOR during drag can show "No" cursor, so use MOVE_CURSOR as fallback
|
// On Linux, DEFAULT_CURSOR during drag can show "No" cursor, so use MOVE_CURSOR as fallback
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.diylc;
|
package org.diylc;
|
||||||
|
|
||||||
import java.awt.Point;
|
import java.awt.Point;
|
||||||
|
import java.awt.geom.Point2D;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
@@ -17,6 +18,7 @@ import org.diylc.common.ComponentType;
|
|||||||
import org.diylc.common.EventType;
|
import org.diylc.common.EventType;
|
||||||
import org.diylc.common.IPlugIn;
|
import org.diylc.common.IPlugIn;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
|
import org.diylc.core.IDIYComponent;
|
||||||
import org.diylc.core.Template;
|
import org.diylc.core.Template;
|
||||||
import org.diylc.presenter.Presenter;
|
import org.diylc.presenter.Presenter;
|
||||||
import org.diylc.swing.ActionFactory;
|
import org.diylc.swing.ActionFactory;
|
||||||
@@ -101,7 +103,7 @@ public class PresenterTests extends TestBase {
|
|||||||
Assert.assertNotNull(receivedTooltip[0]);
|
Assert.assertNotNull(receivedTooltip[0]);
|
||||||
|
|
||||||
// Start drag - should hide tooltip
|
// Start drag - should hide tooltip
|
||||||
presenter.dragStarted(new Point(1000, 1000), java.awt.dnd.DnDConstants.ACTION_MOVE, false);
|
presenter.dragStarted(new Point(1000, 1000), java.awt.dnd.DnDConstants.ACTION_MOVE, false, false);
|
||||||
Assert.assertNull(receivedTooltip[0]);
|
Assert.assertNull(receivedTooltip[0]);
|
||||||
|
|
||||||
// End drag
|
// End drag
|
||||||
@@ -221,7 +223,58 @@ public class PresenterTests extends TestBase {
|
|||||||
treeModel.refresh();
|
treeModel.refresh();
|
||||||
Assert.assertTrue(presenter.getVisibleComponentTypes().containsKey("Tubes"));
|
Assert.assertTrue(presenter.getVisibleComponentTypes().containsKey("Tubes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShiftDragLocksAxis() {
|
||||||
|
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
|
||||||
|
IDIYComponent<?> resistor = presenter.getCurrentProject().getComponents().get(0);
|
||||||
|
Point2D start = new Point2D.Double(resistor.getControlPoint(1).getX(), resistor.getControlPoint(1).getY());
|
||||||
|
|
||||||
|
// hovering a control point of the selected component arms it for dragging
|
||||||
|
presenter.mouseMoved(new Point(1200, 1000), false, false, false);
|
||||||
|
presenter.dragStarted(new Point(1200, 1000), java.awt.dnd.DnDConstants.ACTION_MOVE, false, true);
|
||||||
|
|
||||||
|
// mostly horizontal gesture locks the horizontal axis
|
||||||
|
presenter.dragOver(new Point(1300, 1030));
|
||||||
|
Assert.assertTrue(resistor.getControlPoint(1).getX() > start.getX());
|
||||||
|
Assert.assertEquals(start.getY(), resistor.getControlPoint(1).getY());
|
||||||
|
|
||||||
|
// the axis holds for the rest of the drag even when the cursor swings toward the vertical
|
||||||
|
presenter.dragOver(new Point(1300, 1200));
|
||||||
|
Assert.assertEquals(start.getY(), resistor.getControlPoint(1).getY());
|
||||||
|
presenter.dragEnded(new Point(1300, 1200));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDragWithoutShiftIsFree() {
|
||||||
|
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
|
||||||
|
IDIYComponent<?> resistor = presenter.getCurrentProject().getComponents().get(0);
|
||||||
|
Point2D start = new Point2D.Double(resistor.getControlPoint(1).getX(), resistor.getControlPoint(1).getY());
|
||||||
|
|
||||||
|
presenter.mouseMoved(new Point(1200, 1000), false, false, false);
|
||||||
|
presenter.dragStarted(new Point(1200, 1000), java.awt.dnd.DnDConstants.ACTION_MOVE, false, false);
|
||||||
|
presenter.dragOver(new Point(1300, 1100));
|
||||||
|
Assert.assertTrue(resistor.getControlPoint(1).getX() > start.getX());
|
||||||
|
Assert.assertTrue(resistor.getControlPoint(1).getY() > start.getY());
|
||||||
|
presenter.dragEnded(new Point(1300, 1100));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShiftLocksAxisWhilePlacing() {
|
||||||
|
Map<String, List<ComponentType>> componentTypes = presenter.getComponentTypes();
|
||||||
|
ComponentType resistorType = componentTypes.get("Passive").stream()
|
||||||
|
.filter(x -> x.getName().equals("Resistor")).findFirst().get();
|
||||||
|
presenter.setNewComponentTypeSlot(resistorType, null, null, false);
|
||||||
|
presenter.mouseClicked(new Point(1000, 1000), IPlugInPort.BUTTON1, false, false, false, 1);
|
||||||
|
presenter.mouseMoved(new Point(1030, 1200), false, true, false);
|
||||||
|
presenter.mouseClicked(new Point(1030, 1200), IPlugInPort.BUTTON1, false, true, false, 1);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, presenter.getCurrentProject().getComponents().size());
|
||||||
|
IDIYComponent<?> resistor = presenter.getCurrentProject().getComponents().get(0);
|
||||||
|
Assert.assertEquals(resistor.getControlPoint(0).getX(), resistor.getControlPoint(1).getX());
|
||||||
|
Assert.assertTrue(resistor.getControlPoint(1).getY() > resistor.getControlPoint(0).getY());
|
||||||
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
// public void testLoadVariants() {
|
// public void testLoadVariants() {
|
||||||
// try {
|
// try {
|
||||||
|
|||||||
Reference in New Issue
Block a user