]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram/src/org/simantics/diagram/elements/ResizeNode.java
Use element transform when doing pick check
[simantics/platform.git] / bundles / org.simantics.diagram / src / org / simantics / diagram / elements / ResizeNode.java
1 package org.simantics.diagram.elements;
2
3 import java.awt.BasicStroke;
4 import java.awt.Cursor;
5 import java.awt.Graphics2D;
6 import java.awt.Shape;
7 import java.awt.Stroke;
8 import java.awt.geom.AffineTransform;
9 import java.awt.geom.Point2D;
10 import java.awt.geom.Rectangle2D;
11
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;
20
21 /**
22  * Invisible resize node for resizing rectangular elements
23  * 
24  * @author Teemu Lempinen
25  *
26  */
27 public class ResizeNode extends G2DNode {
28         
29         private static final long serialVersionUID = 7997312998598071328L;
30
31         
32         /**
33          * Interface for listeners listening resize events in nodes
34          * @author Teemu Lempinen
35          *
36          */
37         public interface ResizeListener {
38             
39             /**
40              * Triggered when a node has been resized
41              * @param newBounds new bounds for the node
42              */
43             public void elementResized(Rectangle2D newBounds, AffineTransform transform, boolean synchronizeToBackend);
44         }
45         
46         /**
47          * Enumeration for indicating which side of the resize bounds should affect translate
48          * properties. 
49          * @author Teemu Lempinen
50          *
51          */
52         public enum TranslateEdge {
53             NONE, NORTH, SOUTH, EAST, WEST;
54         }
55         
56         
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;
63
64         int cursor = Cursor.DEFAULT_CURSOR;
65         
66         /**
67          * Create a new Resize node with default border width (1)
68          */
69         public ResizeNode() {
70                 this(1);
71         }
72          
73         /**
74          * Create a new Resize node
75          * 
76          * @param borderWidth Width of the border for handling mouse dragging
77          */
78         public ResizeNode(float borderWidth) {
79                 this.stroke = new BasicStroke(borderWidth);
80         }
81
82         @PropertySetter("Bounds")
83         @SyncField("bounds")
84         public void setBounds(Rectangle2D bounds) {
85                 assert(bounds != null);
86                 this.bounds = bounds;
87         }
88
89         @PropertySetter("stroke")
90         @SyncField("stroke")
91         public void setStroke(Stroke stroke) {
92                 this.stroke = stroke;
93         }
94
95         /**
96          * Is dragging (resizing) active
97          * @return
98          */
99         public boolean dragging() {
100                 return dragging;
101         }
102
103         /**
104          * Set a ResizeListener for this node
105          * @param listener ResizeListener
106          */
107         public void setResizeListener(ResizeListener listener) {
108                 this.resizeListener = listener;
109         }
110
111         @Override
112         public void cleanup() {
113                 removeEventHandler(this);
114                 super.cleanup();
115         }
116
117         @Override
118         public void init() {
119                 super.init();
120                 addEventHandler(this);
121         }
122
123         /**
124          * Outline stroke shape of the bounds of this node
125          * 
126          * @return Outline stroke shape of the bounds of this node
127          */
128         protected Shape getOutline() {
129                 return stroke.createStrokedShape(new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight()));
130         }
131
132         /**
133          * Dragging is started on mouse pressed, if mouse was pressed on the edge of bounds
134          */
135         @Override
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);
141
142                         // Get outline of this node
143                         Shape outline = getOutline();
144
145                         if (outline.contains(local)) {
146                                 dragging = true;
147                                 return true;
148                         }
149                 }
150                 return super.mouseButtonPressed(event);
151         }
152
153         /**
154          * Get resize cursor for the location where the drag started.
155          * 
156          * @param local Point of origin for drag
157          * @return Cursor int
158          */
159         private int getCursorDirection(Point2D local) {
160                 float width = ((BasicStroke)stroke).getLineWidth();
161
162                 // Check the direction of the resize
163                 int cursor = 0;
164                 if (local.getX() >= bounds.getX() - width / 2 && 
165                                 local.getX() <= bounds.getX() + width / 2)
166                         // West side
167                         cursor = Cursor.W_RESIZE_CURSOR;
168                 else if  (local.getX() >= bounds.getMaxX() - width / 2 && 
169                                 local.getX() <= bounds.getMaxX() + width / 2)
170                         // East size
171                         cursor = Cursor.E_RESIZE_CURSOR;
172
173                 if(local.getY() >= bounds.getY() - width / 2 && 
174                                 local.getY() <= bounds.getY() + width / 2) {
175                         // North side
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;
180                         else
181                                 cursor = Cursor.N_RESIZE_CURSOR;
182                 } else if(local.getY() >= bounds.getMaxY() - width / 2 && 
183                                 local.getY() <= bounds.getMaxY() + width / 2) {
184                         // South side
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;
189                         else
190                                 cursor = cursor | Cursor.S_RESIZE_CURSOR;
191                 }
192                 return cursor;
193         }
194
195         double dragTolerance = 0.5;
196         
197         @Override
198         protected boolean mouseMoved(MouseMovedEvent e) {
199                 if(dragging) {
200                         // If dragging is active and mouse is moved enough, resize the element
201                         Point2D local = controlToLocal( e.controlPosition );
202                         local = parentToLocal(local);
203
204                         Rectangle2D bounds = getBoundsInLocal().getBounds2D();
205
206                         if(Math.abs(bounds.getMaxX() - local.getX()) > dragTolerance ||
207                                         Math.abs(bounds.getMaxY() - local.getY()) > dragTolerance) {
208                                 resize(local, false);
209                         }
210                         return true;
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);
215
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) {
222                                 cursor = 0;
223                                 CanvasContext ctx = (CanvasContext)e.getContext();
224                                 ctx.getMouseCursorContext().setCursor(e.mouseId, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
225                         }
226
227                 }
228                 return super.mouseMoved(e);
229         }
230
231         @Override
232         protected boolean mouseDragged(MouseDragBegin e) {
233                 if(dragging) {
234                         return true; // Consume event
235                 } else {
236                         return false;
237                 }
238         }
239         
240         private static double minSize = 5; // Minimum size for an element
241
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;
249
250                 /*
251                  * Resize to east and north:
252                  * 
253                  * Move x and y coordinates and resize to get maxX and maxY to stay in their place
254                  */
255                 if(cursor == Cursor.W_RESIZE_CURSOR || cursor == Cursor.NW_RESIZE_CURSOR || cursor == Cursor.SW_RESIZE_CURSOR) {
256                         double dw = local.getX() - x;
257                         if(w - dw < minSize)
258                                 dw = w - minSize;
259                         w = w - dw;
260                         
261                         
262                         if(TranslateEdge.WEST.equals(xTranslateEdge))
263                             dx = dw;
264                 } 
265                 
266                 if(cursor == Cursor.N_RESIZE_CURSOR || cursor == Cursor.NW_RESIZE_CURSOR || cursor == Cursor.NE_RESIZE_CURSOR ) {
267                         double dh = local.getY() - y;
268                         if(h - dh < minSize)
269                                 dh = h - minSize;
270                         h = h - dh;
271                         
272                         if(TranslateEdge.NORTH.equals(YTranslateEdge))
273                             dy = dh;
274                 } 
275                 
276                 /*
277                  * Resize to west and south:
278                  * 
279                  * Adjust width and height
280                  */
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;
284                         
285                         if(TranslateEdge.EAST.equals(xTranslateEdge))
286                             dx = w - bounds.getWidth();
287                 }
288
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;
292
293                     if(TranslateEdge.SOUTH.equals(YTranslateEdge))
294                         dy = h - bounds.getHeight();
295                 }
296
297                 /*
298                  *  Set bounds and transform to the element before calling resize listener.
299                  *  This prevents unwanted movement due to unsynchronized transform and bounds.
300                  */
301                 this.bounds.setRect(x, y, w, h);
302
303         AffineTransform at = new AffineTransform();
304                 at.translate(dx, dy);
305                 if(resizeListener != null)
306                         resizeListener.elementResized(this.bounds, at, synchronize);
307         }
308
309         @Override
310         protected boolean mouseButtonReleased(MouseButtonReleasedEvent e) {
311                 if(dragging) {
312                         // Stop resizing and set the size to its final state. 
313                         Point2D local = controlToLocal( e.controlPosition );
314                         local = parentToLocal(local);
315
316                         resize(local, true);
317                         dragging = false;
318
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));
323                 }
324                 return super.mouseButtonReleased(e);
325         }
326
327         @Override
328         public int getEventMask() {
329                 return super.getEventMask() | EventTypes.MouseButtonPressedMask
330                                 | EventTypes.MouseMovedMask
331                                 | EventTypes.MouseButtonReleasedMask
332                                 ;
333         }
334
335         @Override
336         public Rectangle2D getBoundsInLocal() {
337                 if(bounds == null) return null;
338                 return bounds.getBounds2D();
339         }
340
341         @Override
342         public void render(Graphics2D g2d) {
343                 // Do not draw anything
344         }
345         
346         /**
347          * Set which edge should affect X-translation 
348          * @param xTranslateEdge TranslateEdge.NONE, EAST or WEST
349          */
350         public void setxTranslateEdge(TranslateEdge xTranslateEdge) {
351         this.xTranslateEdge = xTranslateEdge;
352     }
353         
354            /**
355      * Set which edge should affect Y-translation 
356      * @param yTranslateEdge TranslateEdge.NONE, SOUTH or NORTH
357      */
358         public void setYTranslateEdge(TranslateEdge yTranslateEdge) {
359         YTranslateEdge = yTranslateEdge;
360     }
361 }