package org.simantics.diagram.elements; import java.awt.BasicStroke; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import org.simantics.g2d.canvas.impl.CanvasContext; import org.simantics.scenegraph.g2d.G2DNode; import org.simantics.scenegraph.g2d.events.EventTypes; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent; import org.simantics.scenegraph.utils.NodeUtil; /** * Invisible resize node for resizing rectangular elements * * @author Teemu Lempinen * */ public class ResizeNode extends G2DNode { private static final long serialVersionUID = 7997312998598071328L; /** * Interface for listeners listening resize events in nodes * @author Teemu Lempinen * */ public interface ResizeListener { /** * Triggered when a node has been resized * @param newBounds new bounds for the node */ public void elementResized(Rectangle2D newBounds, AffineTransform transform, boolean synchronizeToBackend); } /** * Enumeration for indicating which side of the resize bounds should affect translate * properties. * @author Teemu Lempinen * */ public enum TranslateEdge { NONE, NORTH, SOUTH, EAST, WEST; } private boolean dragging = false; private ResizeListener resizeListener; private Rectangle2D bounds; private Stroke stroke; private TranslateEdge xTranslateEdge = TranslateEdge.WEST; private TranslateEdge YTranslateEdge = TranslateEdge.NORTH; int cursor = Cursor.DEFAULT_CURSOR; /** * Create a new Resize node with default border width (1) */ public ResizeNode() { this(1); } /** * Create a new Resize node * * @param borderWidth Width of the border for handling mouse dragging */ public ResizeNode(float borderWidth) { this.stroke = new BasicStroke(borderWidth); } @PropertySetter("Bounds") @SyncField("bounds") public void setBounds(Rectangle2D bounds) { assert(bounds != null); this.bounds = bounds; } @PropertySetter("stroke") @SyncField("stroke") public void setStroke(Stroke stroke) { this.stroke = stroke; } /** * Is dragging (resizing) active * @return */ public boolean dragging() { return dragging; } /** * Set a ResizeListener for this node * @param listener ResizeListener */ public void setResizeListener(ResizeListener listener) { this.resizeListener = listener; } @Override public void cleanup() { removeEventHandler(this); super.cleanup(); } @Override public void init() { super.init(); addEventHandler(this); } /** * Outline stroke shape of the bounds of this node * * @return Outline stroke shape of the bounds of this node */ protected Shape getOutline() { return stroke.createStrokedShape(new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight())); } /** * Dragging is started on mouse pressed, if mouse was pressed on the edge of bounds */ @Override protected boolean mouseButtonPressed(MouseButtonPressedEvent event) { if(bounds != null && NodeUtil.isSelected(this, 1)) { // get mouse position Point2D local = controlToLocal( event.controlPosition ); local = parentToLocal(local); // Get outline of this node Shape outline = getOutline(); if (outline.contains(local)) { dragging = true; return true; } } return super.mouseButtonPressed(event); } /** * Get resize cursor for the location where the drag started. * * @param local Point of origin for drag * @return Cursor int */ private int getCursorDirection(Point2D local) { float width = ((BasicStroke)stroke).getLineWidth(); // Check the direction of the resize int cursor = 0; if (local.getX() >= bounds.getX() - width / 2 && local.getX() <= bounds.getX() + width / 2) // West side cursor = Cursor.W_RESIZE_CURSOR; else if (local.getX() >= bounds.getMaxX() - width / 2 && local.getX() <= bounds.getMaxX() + width / 2) // East size cursor = Cursor.E_RESIZE_CURSOR; if(local.getY() >= bounds.getY() - width / 2 && local.getY() <= bounds.getY() + width / 2) { // North side if(cursor == Cursor.W_RESIZE_CURSOR) cursor = Cursor.NW_RESIZE_CURSOR; else if(cursor == Cursor.E_RESIZE_CURSOR) cursor = Cursor.NE_RESIZE_CURSOR; else cursor = Cursor.N_RESIZE_CURSOR; } else if(local.getY() >= bounds.getMaxY() - width / 2 && local.getY() <= bounds.getMaxY() + width / 2) { // South side if(cursor == Cursor.W_RESIZE_CURSOR) cursor = Cursor.SW_RESIZE_CURSOR; else if(cursor == Cursor.E_RESIZE_CURSOR) cursor = Cursor.SE_RESIZE_CURSOR; else cursor = cursor | Cursor.S_RESIZE_CURSOR; } return cursor; } double dragTolerance = 0.5; @Override protected boolean mouseMoved(MouseMovedEvent e) { if(dragging) { // If dragging is active and mouse is moved enough, resize the element Point2D local = controlToLocal( e.controlPosition ); local = parentToLocal(local); Rectangle2D bounds = getBoundsInLocal().getBounds2D(); if(Math.abs(bounds.getMaxX() - local.getX()) > dragTolerance || Math.abs(bounds.getMaxY() - local.getY()) > dragTolerance) { resize(local, false); } return true; } else if(NodeUtil.isSelected(this, 1)){ // Dragging is not active. Change mouse cursor if entered or exited border Point2D local = controlToLocal( e.controlPosition ); local = parentToLocal(local); Shape outline = getOutline(); if (outline.contains(local)) { cursor = getCursorDirection(local); CanvasContext ctx = (CanvasContext)e.getContext(); ctx.getMouseCursorContext().setCursor(e.mouseId, Cursor.getPredefinedCursor(cursor)); } else if(cursor != 0) { cursor = 0; CanvasContext ctx = (CanvasContext)e.getContext(); ctx.getMouseCursorContext().setCursor(e.mouseId, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } return super.mouseMoved(e); } @Override protected boolean mouseDragged(MouseDragBegin e) { if(dragging) { return true; // Consume event } else { return false; } } private static double minSize = 5; // Minimum size for an element private void resize(Point2D local, boolean synchronize) { Rectangle2D bounds = getBoundsInLocal().getBounds2D(); double x = bounds.getX(); double y = bounds.getY(); double w = bounds.getWidth(); double h = bounds.getHeight(); double dx = 0, dy = 0; /* * Resize to east and north: * * Move x and y coordinates and resize to get maxX and maxY to stay in their place */ if(cursor == Cursor.W_RESIZE_CURSOR || cursor == Cursor.NW_RESIZE_CURSOR || cursor == Cursor.SW_RESIZE_CURSOR) { double dw = local.getX() - x; if(w - dw < minSize) dw = w - minSize; w = w - dw; if(TranslateEdge.WEST.equals(xTranslateEdge)) dx = dw; } if(cursor == Cursor.N_RESIZE_CURSOR || cursor == Cursor.NW_RESIZE_CURSOR || cursor == Cursor.NE_RESIZE_CURSOR ) { double dh = local.getY() - y; if(h - dh < minSize) dh = h - minSize; h = h - dh; if(TranslateEdge.NORTH.equals(YTranslateEdge)) dy = dh; } /* * Resize to west and south: * * Adjust width and height */ if(cursor == Cursor.E_RESIZE_CURSOR || cursor == Cursor.NE_RESIZE_CURSOR || cursor == Cursor.SE_RESIZE_CURSOR) { double dw = local.getX() - bounds.getMaxX(); w = w + dw > minSize ? w + dw : minSize; if(TranslateEdge.EAST.equals(xTranslateEdge)) dx = w - bounds.getWidth(); } if(cursor == Cursor.S_RESIZE_CURSOR || cursor == Cursor.SW_RESIZE_CURSOR || cursor == Cursor.SE_RESIZE_CURSOR) { double dh = local.getY() - bounds.getMaxY(); h = h + dh > minSize ? h + dh : minSize; if(TranslateEdge.SOUTH.equals(YTranslateEdge)) dy = h - bounds.getHeight(); } /* * Set bounds and transform to the element before calling resize listener. * This prevents unwanted movement due to unsynchronized transform and bounds. */ this.bounds.setRect(x, y, w, h); AffineTransform at = new AffineTransform(); at.translate(dx, dy); if(resizeListener != null) resizeListener.elementResized(this.bounds, at, synchronize); } @Override protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) { if(dragging) { // Stop resizing and set the size to its final state. Point2D local = controlToLocal( e.controlPosition ); local = parentToLocal(local); resize(local, true); dragging = false; // Revert cursor to normal. CanvasContext ctx = (CanvasContext)e.getContext(); cursor = Cursor.DEFAULT_CURSOR; ctx.getMouseCursorContext().setCursor(e.mouseId, Cursor.getPredefinedCursor(cursor)); } return super.mouseButtonReleased(e); } @Override public int getEventMask() { return super.getEventMask() | EventTypes.MouseButtonPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonReleasedMask ; } @Override public Rectangle2D getBoundsInLocal() { if(bounds == null) return null; return bounds.getBounds2D(); } @Override public void render(Graphics2D g2d) { // Do not draw anything } /** * Set which edge should affect X-translation * @param xTranslateEdge TranslateEdge.NONE, EAST or WEST */ public void setxTranslateEdge(TranslateEdge xTranslateEdge) { this.xTranslateEdge = xTranslateEdge; } /** * Set which edge should affect Y-translation * @param yTranslateEdge TranslateEdge.NONE, SOUTH or NORTH */ public void setYTranslateEdge(TranslateEdge yTranslateEdge) { YTranslateEdge = yTranslateEdge; } }