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