1 package org.simantics.diagram.elements;
3 import java.awt.BasicStroke;
4 import java.awt.Cursor;
5 import java.awt.Graphics2D;
7 import java.awt.Stroke;
8 import java.awt.geom.AffineTransform;
9 import java.awt.geom.Point2D;
10 import java.awt.geom.Rectangle2D;
12 import org.simantics.g2d.canvas.impl.CanvasContext;
13 import org.simantics.scenegraph.g2d.G2DNode;
14 import org.simantics.scenegraph.g2d.events.EventTypes;
15 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
16 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
17 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
18 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
19 import org.simantics.scenegraph.utils.NodeUtil;
22 * Invisible resize node for resizing rectangular elements
24 * @author Teemu Lempinen
27 public class ResizeNode extends G2DNode {
29 private static final long serialVersionUID = 7997312998598071328L;
33 * Interface for listeners listening resize events in nodes
34 * @author Teemu Lempinen
37 public interface ResizeListener {
40 * Triggered when a node has been resized
41 * @param newBounds new bounds for the node
43 public void elementResized(Rectangle2D newBounds, AffineTransform transform, boolean synchronizeToBackend);
47 * Enumeration for indicating which side of the resize bounds should affect translate
49 * @author Teemu Lempinen
52 public enum TranslateEdge {
53 NONE, NORTH, SOUTH, EAST, WEST;
57 private boolean dragging = false;
58 private ResizeListener resizeListener;
59 private Rectangle2D bounds;
60 private Stroke stroke;
61 private TranslateEdge xTranslateEdge = TranslateEdge.WEST;
62 private TranslateEdge YTranslateEdge = TranslateEdge.NORTH;
64 int cursor = Cursor.DEFAULT_CURSOR;
67 * Create a new Resize node with default border width (1)
74 * Create a new Resize node
76 * @param borderWidth Width of the border for handling mouse dragging
78 public ResizeNode(float borderWidth) {
79 this.stroke = new BasicStroke(borderWidth);
82 @PropertySetter("Bounds")
84 public void setBounds(Rectangle2D bounds) {
85 assert(bounds != null);
89 @PropertySetter("stroke")
91 public void setStroke(Stroke stroke) {
96 * Is dragging (resizing) active
99 public boolean dragging() {
104 * Set a ResizeListener for this node
105 * @param listener ResizeListener
107 public void setResizeListener(ResizeListener listener) {
108 this.resizeListener = listener;
112 public void cleanup() {
113 removeEventHandler(this);
120 addEventHandler(this);
124 * Outline stroke shape of the bounds of this node
126 * @return Outline stroke shape of the bounds of this node
128 protected Shape getOutline() {
129 return stroke.createStrokedShape(new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight()));
133 * Dragging is started on mouse pressed, if mouse was pressed on the edge of bounds
136 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
137 if(bounds != null && NodeUtil.isSelected(this, 1)) {
138 // get mouse position
139 Point2D local = controlToLocal( event.controlPosition );
140 local = parentToLocal(local);
142 // Get outline of this node
143 Shape outline = getOutline();
145 if (outline.contains(local)) {
150 return super.mouseButtonPressed(event);
154 * Get resize cursor for the location where the drag started.
156 * @param local Point of origin for drag
159 private int getCursorDirection(Point2D local) {
160 float width = ((BasicStroke)stroke).getLineWidth();
162 // Check the direction of the resize
164 if (local.getX() >= bounds.getX() - width / 2 &&
165 local.getX() <= bounds.getX() + width / 2)
167 cursor = Cursor.W_RESIZE_CURSOR;
168 else if (local.getX() >= bounds.getMaxX() - width / 2 &&
169 local.getX() <= bounds.getMaxX() + width / 2)
171 cursor = Cursor.E_RESIZE_CURSOR;
173 if(local.getY() >= bounds.getY() - width / 2 &&
174 local.getY() <= bounds.getY() + width / 2) {
176 if(cursor == Cursor.W_RESIZE_CURSOR)
177 cursor = Cursor.NW_RESIZE_CURSOR;
178 else if(cursor == Cursor.E_RESIZE_CURSOR)
179 cursor = Cursor.NE_RESIZE_CURSOR;
181 cursor = Cursor.N_RESIZE_CURSOR;
182 } else if(local.getY() >= bounds.getMaxY() - width / 2 &&
183 local.getY() <= bounds.getMaxY() + width / 2) {
185 if(cursor == Cursor.W_RESIZE_CURSOR)
186 cursor = Cursor.SW_RESIZE_CURSOR;
187 else if(cursor == Cursor.E_RESIZE_CURSOR)
188 cursor = Cursor.SE_RESIZE_CURSOR;
190 cursor = cursor | Cursor.S_RESIZE_CURSOR;
195 double dragTolerance = 0.5;
198 protected boolean mouseMoved(MouseMovedEvent e) {
200 // If dragging is active and mouse is moved enough, resize the element
201 Point2D local = controlToLocal( e.controlPosition );
202 local = parentToLocal(local);
204 Rectangle2D bounds = getBoundsInLocal().getBounds2D();
206 if(Math.abs(bounds.getMaxX() - local.getX()) > dragTolerance ||
207 Math.abs(bounds.getMaxY() - local.getY()) > dragTolerance) {
208 resize(local, false);
211 } else if(NodeUtil.isSelected(this, 1)){
212 // Dragging is not active. Change mouse cursor if entered or exited border
213 Point2D local = controlToLocal( e.controlPosition );
214 local = parentToLocal(local);
216 Shape outline = getOutline();
217 if (outline.contains(local)) {
218 cursor = getCursorDirection(local);
219 CanvasContext ctx = (CanvasContext)e.getContext();
220 ctx.getMouseCursorContext().setCursor(e.mouseId, Cursor.getPredefinedCursor(cursor));
221 } else if(cursor != 0) {
223 CanvasContext ctx = (CanvasContext)e.getContext();
224 ctx.getMouseCursorContext().setCursor(e.mouseId, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
228 return super.mouseMoved(e);
232 protected boolean mouseDragged(MouseDragBegin e) {
234 return true; // Consume event
240 private static double minSize = 5; // Minimum size for an element
242 private void resize(Point2D local, boolean synchronize) {
243 Rectangle2D bounds = getBoundsInLocal().getBounds2D();
244 double x = bounds.getX();
245 double y = bounds.getY();
246 double w = bounds.getWidth();
247 double h = bounds.getHeight();
248 double dx = 0, dy = 0;
251 * Resize to east and north:
253 * Move x and y coordinates and resize to get maxX and maxY to stay in their place
255 if(cursor == Cursor.W_RESIZE_CURSOR || cursor == Cursor.NW_RESIZE_CURSOR || cursor == Cursor.SW_RESIZE_CURSOR) {
256 double dw = local.getX() - x;
262 if(TranslateEdge.WEST.equals(xTranslateEdge))
266 if(cursor == Cursor.N_RESIZE_CURSOR || cursor == Cursor.NW_RESIZE_CURSOR || cursor == Cursor.NE_RESIZE_CURSOR ) {
267 double dh = local.getY() - y;
272 if(TranslateEdge.NORTH.equals(YTranslateEdge))
277 * Resize to west and south:
279 * Adjust width and height
281 if(cursor == Cursor.E_RESIZE_CURSOR || cursor == Cursor.NE_RESIZE_CURSOR || cursor == Cursor.SE_RESIZE_CURSOR) {
282 double dw = local.getX() - bounds.getMaxX();
283 w = w + dw > minSize ? w + dw : minSize;
285 if(TranslateEdge.EAST.equals(xTranslateEdge))
286 dx = w - bounds.getWidth();
289 if(cursor == Cursor.S_RESIZE_CURSOR || cursor == Cursor.SW_RESIZE_CURSOR || cursor == Cursor.SE_RESIZE_CURSOR) {
290 double dh = local.getY() - bounds.getMaxY();
291 h = h + dh > minSize ? h + dh : minSize;
293 if(TranslateEdge.SOUTH.equals(YTranslateEdge))
294 dy = h - bounds.getHeight();
298 * Set bounds and transform to the element before calling resize listener.
299 * This prevents unwanted movement due to unsynchronized transform and bounds.
301 this.bounds.setRect(x, y, w, h);
303 AffineTransform at = new AffineTransform();
304 at.translate(dx, dy);
305 if(resizeListener != null)
306 resizeListener.elementResized(this.bounds, at, synchronize);
310 protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) {
312 // Stop resizing and set the size to its final state.
313 Point2D local = controlToLocal( e.controlPosition );
314 local = parentToLocal(local);
319 // Revert cursor to normal.
320 CanvasContext ctx = (CanvasContext)e.getContext();
321 cursor = Cursor.DEFAULT_CURSOR;
322 ctx.getMouseCursorContext().setCursor(e.mouseId, Cursor.getPredefinedCursor(cursor));
324 return super.mouseButtonReleased(e);
328 public int getEventMask() {
329 return super.getEventMask() | EventTypes.MouseButtonPressedMask
330 | EventTypes.MouseMovedMask
331 | EventTypes.MouseButtonReleasedMask
336 public Rectangle2D getBoundsInLocal() {
337 if(bounds == null) return null;
338 return bounds.getBounds2D();
342 public void render(Graphics2D g2d) {
343 // Do not draw anything
347 * Set which edge should affect X-translation
348 * @param xTranslateEdge TranslateEdge.NONE, EAST or WEST
350 public void setxTranslateEdge(TranslateEdge xTranslateEdge) {
351 this.xTranslateEdge = xTranslateEdge;
355 * Set which edge should affect Y-translation
356 * @param yTranslateEdge TranslateEdge.NONE, SOUTH or NORTH
358 public void setYTranslateEdge(TranslateEdge yTranslateEdge) {
359 YTranslateEdge = yTranslateEdge;