View Javadoc

1   /**********************************************
2    * Copyright (C) 2009 Lukas Laag
3    * This file is part of Vectomatic.
4    * 
5    * Vectomatic is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    * 
10   * Vectomatic is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   * 
15   * You should have received a copy of the GNU General Public License
16   * along with Vectomatic.  If not, see http://www.gnu.org/licenses/
17   **********************************************/
18  package org.vectomatic.client.rep.controller;
19  
20  import java.util.ArrayList;
21  import java.util.Iterator;
22  import java.util.List;
23  
24  import org.vectomatic.client.rep.RepApplication;
25  import org.vectomatic.client.rep.command.ICommand;
26  import org.vectomatic.client.rep.command.TransformShapeCommand;
27  import org.vectomatic.client.rep.view.Cursor;
28  import org.vectomatic.client.rep.view.DrawingView;
29  import org.vectomatic.common.model.Attribute;
30  import org.vectomatic.common.model.FloatAttributeValue;
31  import org.vectomatic.common.model.Shape;
32  import org.vectomatic.common.model.geometry.BoundingBox;
33  import org.vectomatic.common.model.geometry.Point;
34  import org.vectomatic.common.model.geometry.Rect;
35  import org.vectomatic.common.model.geometry.ShapeGroup;
36  import org.vectomatic.common.model.geometry.TransformMatrix;
37  import org.vectomatic.common.model.style.Color;
38  import org.vectomatic.common.model.style.NoneStyle;
39  
40  import com.google.gwt.user.client.DOM;
41  import com.google.gwt.user.client.Event;
42  import com.google.gwt.user.client.ui.KeyboardListener;
43  import com.google.gwt.user.client.ui.MenuBar;
44  
45  /**
46   * Controller to respond to translate/rotate/resize shape requests and
47   * turn them into TransformShapeCommand
48   */
49  public class SelectShapeController extends ControllerBase {
50  	private static final float HANDLE_SIZE = 3f;
51  	private static final int HANDLE_MOVE = 10;
52  	private static final int HANDLE_NONE = 11;
53  		
54  	/**
55  	 * The cursor hovers over the selection and changes to reflect
56  	 * possible actions
57  	 */
58  	private static final int MODE_HOVER = 0;
59  	/**
60  	 * The user selects shapes using the selection rectangle
61  	 */
62  	private static final int MODE_RECT = 1;
63  	/**
64  	 * The user translates the selection
65  	 */
66  	private static final int MODE_TRANSLATE = 2;
67  	/**
68  	 * The user rotates the selection
69  	 */
70  	private static final int MODE_ROTATE = 3;
71  	/**
72  	 * The user scales the selection along the X axis
73  	 */
74  	private static final int MODE_SCALE = 4;
75  	
76  	private static final Cursor[] _cursors = {
77  		Cursor.CURSOR_NW_RESIZE,
78  		Cursor.CURSOR_SW_RESIZE,
79  		Cursor.CURSOR_SE_RESIZE,
80  		Cursor.CURSOR_NE_RESIZE,
81  		Cursor.CURSOR_N_RESIZE,
82  		Cursor.CURSOR_W_RESIZE,
83  		Cursor.CURSOR_S_RESIZE,
84  		Cursor.CURSOR_E_RESIZE,
85  		Cursor.CURSOR_MOVE,
86  		Cursor.CURSOR_ROTATE,
87  		Cursor.CURSOR_POINTER,
88  		Cursor.CURSOR_POINTER
89  	};
90  
91  	private TransformMatrix _mTmp, _mOld, _mInv;
92  	private Rect _rect;
93  	private Point _p0, _p1, _p2, _p3;
94  	private float _r1, _r2, _r3;
95  	private int _mode;
96  	private int _handle;
97  	private MouseControllerButton _button;
98  	private DeleteController _deleteController;
99  	private ContextualMenuVisitor _contextVisitor;
100 	
101 	public SelectShapeController(RepApplication app, DeleteController deleteController, ContextualMenuVisitor contextVisitor) {
102 		super(app);
103 		_deleteController = deleteController;
104 		_contextVisitor = contextVisitor;
105 		_button = new MouseControllerButton(_app.getIcons().selectIcon().createImage(), _app.getConstants().selectCommand(), this);
106 		_mTmp = new TransformMatrix();
107 		_mOld = new TransformMatrix();
108 		_mInv = new TransformMatrix();
109 		_rect = new Rect();
110 		_rect.setAttribute(Attribute.LINE_STYLE, Color.BLACK);
111 		_rect.setAttribute(Attribute.LINE_OPACITY, new FloatAttributeValue(1f));
112 		_rect.setAttribute(Attribute.FILL_STYLE, NoneStyle.NONE);
113 		_rect.setAttribute(Attribute.FILL_OPACITY, new FloatAttributeValue(0f));
114 		_rect.setAttribute(Attribute.LINE_WIDTH, new FloatAttributeValue(1f));
115 
116 		_p0 = new Point();
117 		_p1 = new Point();
118 		_p2 = new Point();
119 		_p3 = new Point();
120 	}
121 
122 	public MouseControllerButton getButton() {
123 		return _button;
124 	}
125 
126 	@Override
127 	public void render(DrawingView view) {
128 		Shape rootShape = _app.getSelection().getRootShape();
129 		if (rootShape != null) {
130 			BoundingBox bbox = rootShape.getBoundingBox();
131 			rootShape.getTransform().copyTo(_mTmp);
132 
133 			/*if (_mode == MODE_ROTATE) {
134 				view.beginPath();
135 				bbox.getPoint(BoundingBox.PT_C, _p0).transform(m);
136 				view.moveTo(_p0.x, _p0.y);
137 				// Radius of the feedback circle is 80% of the bbox smaller dimension
138 				float r = 0.8f * 0.5f * Math.min(bbox.getWidth(), bbox.getHeight());
139 				RepApplication.app.debugArea.setText(Float.toString(r));
140 				view.moveTo(_p0.x, _p0.y);
141 				view.lineTo(_p0.x + r * (float)Math.cos (_r2), _p0.y + r * (float)Math.sin (_r2));
142 				view.arc(_p0.x, _p0.y, r, _r2, _r3, true);
143 				view.moveTo(_p0.x + r * (float)Math.cos (_r3), _p0.y + r * (float)Math.sin (_r3));
144 				view.lineTo(_p0.x, _p0.y);
145 				view.stroke();
146 			}*/
147 
148 			rootShape.acceptVisitor(view.getRenderer());
149 			Color.BLACK.acceptVisitor(view.getStrokeStyleVisitor());
150 			Color.BLACK.acceptVisitor(view.getFillStyleVisitor());
151 			view.setGlobalAlpha(1f);
152 			view.setLineWidth(1f);
153 			view.beginPath();
154 			bbox.getPoint(BoundingBox.PT_NE, _p0).transform(_mTmp);
155 			view.moveTo(_p0.x, _p0.y);
156 			for (int i = BoundingBox.PT_NW; i <= BoundingBox.PT_NE; i++) {
157 				bbox.getPoint(i, _p0).transform(_mTmp);
158 				view.lineTo(_p0.x, _p0.y);
159 			}
160 			bbox.getPoint(BoundingBox.PT_E, _p0);
161 			bbox.getPoint(BoundingBox.PT_E, _p0).transform(_mTmp);
162 			view.moveTo(_p0.x, _p0.y);
163 			bbox.getPoint(BoundingBox.PT_R, _p0);
164 			_p0.transform(_mTmp);
165 			view.lineTo(_p0.x, _p0.y);
166 			view.stroke();
167 			view.beginPath();
168 			bbox.getPoint(BoundingBox.PT_R, _p0);
169 			_p0.transform(_mTmp);
170 			view.arc(_p0.x, _p0.y, HANDLE_SIZE, 0f, (float)(2 * Math.PI), true);
171 			view.fill();
172 			for (int i = BoundingBox.PT_NW; i <= BoundingBox.PT_E; i++) {
173 				bbox.getPoint(i, _p0).transform(_mTmp);
174 				view.fillRect(_p0.x - HANDLE_SIZE, _p0.y - HANDLE_SIZE, HANDLE_SIZE *  2, HANDLE_SIZE * 2);
175 			}
176 		}
177 		if (_mode == MODE_RECT) {
178 			Color.BLACK.acceptVisitor(view.getStrokeStyleVisitor());
179 			Color.BLACK.acceptVisitor(view.getFillStyleVisitor());
180 			view.setGlobalAlpha(1f);
181 			view.getRenderer().visitRect(_rect);
182 		}
183 	}
184 	
185 	@Override
186 	public void activate(DrawingView view) {
187 		_mode = MODE_HOVER;
188 		_app.getSelection().select(new ArrayList<Shape>());
189 		view.setCursor(_cursors[_handle]);
190 	}
191 
192 	@Override
193 	public void mouseDown(DrawingView view, Point p, int modifiers) {
194 		p.copyTo(_p1);
195 		view.toModelCoordinates(p);
196 		Event event = DOM.eventGetCurrentEvent();
197 		if (DOM.eventGetButton(event) == Event.BUTTON_RIGHT) {
198 			MenuBar bar = null;
199 			if (view.getPicker().pick(p, _app.getSelection().iterator()) == null) {
200 				Shape shape = view.getPicker().pick(p, _app.getModel().reverseIterator());
201 				if (shape != null) {
202 					List<Shape> newSelection = new ArrayList<Shape>();
203 					newSelection.add(shape);
204 					_app.getSelection().select(newSelection);				
205 					bar = _contextVisitor.getContextualMenu(_app.getSelection());
206 				}
207 			} else {
208 				bar = _contextVisitor.getContextualMenu(_app.getSelection());
209 			}
210 			if (bar != null) {
211 				ControllerContextItem.popup.setWidget(bar);
212 				ControllerContextItem.popup.setPopupPosition(DOM.eventGetClientX(event), DOM.eventGetClientY(event));
213 				ControllerContextItem.popup.show();
214 			}
215 		} else {
216 			boolean ctrlPressed = (modifiers & KeyboardListener.MODIFIER_CTRL) != 0;
217 			Shape rootShape = _app.getSelection().getRootShape();
218 			
219 			_handle = getHandle(view, p);
220 			switch(_handle) {
221 				case BoundingBox.PT_NW:
222 				case BoundingBox.PT_SW:
223 				case BoundingBox.PT_SE:
224 				case BoundingBox.PT_NE:
225 				case BoundingBox.PT_N:
226 				case BoundingBox.PT_W:
227 				case BoundingBox.PT_S:
228 				case BoundingBox.PT_E:
229 					_mode = MODE_SCALE;
230 					rootShape.getTransform().copyTo(_mOld);
231 					// Store the first point clicked
232 					rootShape.getScaling(_p1);
233 					rootShape.getTransform().invert(_mInv);
234 					break;
235 					
236 				case BoundingBox.PT_R:
237 					_mode = MODE_ROTATE;
238 					rootShape.getTransform().copyTo(_mOld);
239 					
240 					// Store the shape original rotation and the original transform matrix
241 					_r1 = rootShape.getRotation();
242 					rootShape.getTransform().invert(_mInv);
243 					
244 					// Compute the vector from the selection bounding box center
245 					// to the mousedown point
246 					p.transform(_mInv).subtract(rootShape.getBoundingBox().getPoint(BoundingBox.PT_C, _p0));
247 					
248 					// Compute the angle (PT_R, PT_C, p)
249 					_r2 = (float)(Math.acos(p.x / p.length()));
250 					if (p.y < 0) {
251 						_r2 = -_r2;
252 					}
253 					_r3 = _r2;
254 					break;
255 				case HANDLE_MOVE:
256 					if (ctrlPressed) {
257 						List<Shape> oldSelection = _app.getSelection().getSelectedShapes();
258 						List<Shape> newSelection = new ArrayList<Shape>(oldSelection);
259 						newSelection.remove(view.getPicker().pick(p, _app.getSelection().iterator()));
260 						_app.getSelection().select(newSelection);
261 						_mode = MODE_HOVER;
262 					} else {
263 						_mode = MODE_TRANSLATE;
264 						rootShape.getTransform().copyTo(_mOld);
265 						// Store the first point clicked
266 						p.copyTo(_p1);
267 						// Store the vector from the clicked point to the
268 						// bounding box center
269 						p.subtract(rootShape.getTranslation(_p2), _p2);
270 					}
271 					break;
272 				case HANDLE_NONE:
273 					{
274 						Shape shape = view.getPicker().pick(p, _app.getModel().reverseIterator());
275 						if (shape == null) {
276 							RepApplication.app.debugPrint("rect");
277 							if (!ctrlPressed) {
278 								_app.getSelection().select(new ArrayList<Shape>());
279 							}
280 							_mode = MODE_RECT;
281 							_rect.setTranslation(p);
282 							_rect.setRotation(-view.getRotation());
283 							_rect.setScaling(Point.UNIT);
284 							view.getScaling(_p3);
285 						} else {
286 							RepApplication.app.debugPrint("select");
287 							if (!ctrlPressed) {
288 								_app.getSelection().select(new ArrayList<Shape>());
289 							}
290 							List<Shape> oldSelection = _app.getSelection().getSelectedShapes();
291 							List<Shape> newSelection = new ArrayList<Shape>(oldSelection);
292 							newSelection.add(shape);
293 							rootShape = _app.getSelection().select(newSelection);
294 							_mode = MODE_TRANSLATE;
295 							rootShape.getTransform().copyTo(_mOld);
296 							// Store the first point clicked
297 							p.copyTo(_p1);
298 							// Store the vector from the clicked point to the
299 							// bounding box center
300 							p.subtract(rootShape.getTranslation(_p2), _p2);
301 						}
302 		
303 					}
304 					break;
305 			}
306 		}
307 	}
308 	
309 	private int getHandle(DrawingView view, Point p) {
310 		Shape rootShape = _app.getSelection().getRootShape();
311 		if (rootShape != null) {
312 			BoundingBox bbox = rootShape.getBoundingBox();
313 			
314 			// Test click in N,E,S,W,NE,NW,SW,SE control points
315 			for (int i = BoundingBox.PT_NW; i <= BoundingBox.PT_E; i++) {
316 				bbox.getPoint(i, _p0).transform(rootShape.getTransform());
317 				if (p.x >= _p0.x - HANDLE_SIZE && p.x <= _p0.x + HANDLE_SIZE
318 				 && p.y >= _p0.y - HANDLE_SIZE && p.y <= _p0.y + HANDLE_SIZE) {
319 					return i;
320 				}
321 			}
322 			
323 			// Test click in R control point
324 			bbox.getPoint(BoundingBox.PT_R, _p0).transform(rootShape.getTransform());
325 			if (p.subtract(_p0, _p0).length() <= HANDLE_SIZE) {
326 				return BoundingBox.PT_R;
327 			}
328 
329 			// Test click in one of the selected shapes.
330 			if (view.getPicker().pick(p, _app.getSelection().iterator()) != null) {
331 				return HANDLE_MOVE;
332 			}
333 		}
334 		return HANDLE_NONE;
335 	}
336 	
337 	@Override
338 	public void mouseMove(DrawingView view, Point p, int modifiers) {
339 		p.copyTo(_p0);
340 		view.toModelCoordinates(p);
341 		Shape rootShape = _app.getSelection().getRootShape();
342 		switch(_mode) {
343 			case MODE_RECT:
344 				{
345 					_p0.add(_p1, _p2).multiply(0.5f).subtract(_p1);
346 					_p2.x /= _p3.x;
347 					_p2.y /= _p3.y;
348 					_rect.setScaling(_p2);
349 					float cos = (float)Math.cos(view.getRotation());
350 					float sin = (float)Math.sin(view.getRotation());
351 					p.x = p.x - _p2.x * cos - _p2.y * sin;
352 					p.y = p.y + _p2.x * sin - _p2.y * cos;
353 					_rect.setTranslation(p);			
354 				}
355 				break;
356 			case MODE_HOVER:
357 				{
358 					_handle = getHandle(view, p);
359 					view.setCursor(_cursors[_handle]);
360 				}
361 				break;
362 			case MODE_TRANSLATE:
363 				{
364 					rootShape.setTranslation(p.subtract(_p2));
365 				}
366 				break;
367 			case MODE_ROTATE:
368 				{
369 					// Compute the vector from the selection bounding box center
370 					// to the mousedown point
371 					p.transform(_mInv).subtract(rootShape.getBoundingBox().getPoint(BoundingBox.PT_C, _p0));
372 					
373 					// Compute the angle (PT_R, PT_C, p)
374 					float d = p.length();
375 					if (d > 0f) {
376 						_r3 = (float)(Math.acos(p.x / d));
377 						if (p.y < 0) {
378 							_r3 = -_r3;
379 						}
380 						float dr = _r3 - _r2;
381 						rootShape.getScaling(_p0);
382 						if (_p0.x * _p0.y < 0) {
383 							dr = - dr;
384 						}
385 						rootShape.setRotation(_r1 + dr);
386 					}
387 				}
388 				break;
389 			case MODE_SCALE:
390 				{
391 					BoundingBox bbox = rootShape.getBoundingBox();
392 					rootShape.getScaling(_p2);
393 					bbox.getPoint(BoundingBox.PT_C, _p3);
394 					bbox.getPoint(_handle, _p0).subtract(_p3);
395 					p.transform(_mInv).subtract(_p3);
396 					switch(_handle) {
397 						case BoundingBox.PT_NW:
398 						case BoundingBox.PT_SW:
399 						case BoundingBox.PT_SE:
400 						case BoundingBox.PT_NE:
401 						case BoundingBox.PT_N:
402 						case BoundingBox.PT_S:
403 							_p2.y = _p1.y * p.y / _p0.y;
404 					}
405 					switch(_handle) {
406 						case BoundingBox.PT_NW:
407 						case BoundingBox.PT_SW:
408 						case BoundingBox.PT_SE:
409 						case BoundingBox.PT_NE:
410 						case BoundingBox.PT_W:
411 						case BoundingBox.PT_E:
412 							_p2.x = _p1.x * p.x / _p0.x;
413 					}
414 					if (rootShape instanceof ShapeGroup && ((ShapeGroup)rootShape).containsRotatedShape()) {
415 						// scaling(a,b) and rotation(r) are not commutative if a != b
416 						// such a transformation would skew the shape
417 						_p2.x = Math.max(_p2.x, _p2.y);
418 						_p2.y = _p2.x;
419 					}
420 					rootShape.setScaling(_p2);
421 				}
422 				break;
423 		}
424 	}
425 
426 	@Override
427 	public void mouseUp(DrawingView view, Point p, int modifiers) {
428 		mouseMove(view, p, modifiers);
429 		deactivate(view);
430 	}
431 	
432 	@Override
433 	public void keyDown(DrawingView view, char keyCode, int modifiers) {
434 		RepApplication.app.debugPrint("keyDown: " + keyCode);
435 		if (keyCode == KeyboardListener.KEY_DELETE ||  keyCode == KeyboardListener.KEY_BACKSPACE && _app.getSelection().getSelectedShapes().size() > 0) {
436 			_deleteController.activate(view);
437 		}
438 	}
439 	
440 	@Override
441 	public void deactivate(DrawingView view) {
442 		Shape rootShape = _app.getSelection().getRootShape();
443 		switch(_mode) {
444 			case MODE_RECT:
445 				{
446 					List<Shape> newSelection = new ArrayList<Shape>();
447 					// Skip selection if the selection rect has a 0
448 					// width or height
449 					_rect.getScaling(_p0);
450 					if (_p0.x != 0f && _p0.y != 0f) {
451 						// Select all the shapes contained in the selection rect
452 						Iterator<Shape> iterator = _app.getModel().iterator();
453 						RepApplication.app.debugPrint("size = " + _app.getModel().count());
454 						while (iterator.hasNext()) {
455 							Shape shape = iterator.next();
456 							BoundingBox bbox = shape.getBoundingBox();
457 							boolean inSelectionRect = true;
458 							
459 							// Transform the bounding box vertices into the selection rect coordinate system
460 							shape.getTransform().preMultiply(_rect.getTransform().invert(_mTmp), _mTmp);
461 							for (int i = BoundingBox.PT_NW; i <= BoundingBox.PT_NE; i++) {
462 								if (!_rect.getBoundingBox().containsPoint(bbox.getPoint(i, _p1).transform(_mTmp))) {
463 									inSelectionRect = false;
464 									break;
465 								}
466 							}
467 							if (inSelectionRect) {
468 								newSelection.add(shape);
469 							}
470 						}
471 					}
472 					_app.getSelection().select(newSelection);
473 				}
474 				break;
475 			case MODE_TRANSLATE:
476 				{
477 					// Compute the translation in _p0
478 					rootShape.getTranslation(_p0).subtract(_p1).add(_p2);
479 					if (_p0.squaredLength() > 0f) {
480 						// Compute the transform T = M(k+1).Inv(M(k))
481 						_mOld.invert(_mTmp).preMultiply(rootShape.getTransform());
482 						ICommand command = new TransformShapeCommand(_app, _mTmp, _mOld, TransformShapeCommand.TRANSLATE, _app.getSelector(), this);
483 						command.execute();
484 						_app.getHistory().addCommand(command);
485 					}
486 				}
487 				break;
488 			case MODE_ROTATE:
489 				{
490 					float dr = _r3 - _r2;
491 					if (dr != 0f) {
492 						// Compute the transform T = M(k+1).Inv(M(k))
493 						_mOld.invert(_mTmp).preMultiply(rootShape.getTransform());
494 						ICommand command = new TransformShapeCommand(_app, _mTmp, _mOld, TransformShapeCommand.ROTATE, _app.getSelector(), this);
495 						command.execute();
496 						_app.getHistory().addCommand(command);
497 					}
498 				}
499 				break;
500 			case MODE_SCALE:
501 				{
502 					rootShape.getScaling(_p2);
503 					if (!_p1.equals(_p2)) {
504 						// Compute the transform T = M(k+1).Inv(M(k))
505 						_mOld.invert(_mTmp).preMultiply(rootShape.getTransform());
506 						ICommand command = new TransformShapeCommand(_app, _mTmp, _mOld, TransformShapeCommand.SCALE, _app.getSelector(), this);
507 						command.execute();
508 						_app.getHistory().addCommand(command);
509 					}
510 				}
511 				break;
512 		}
513 		_mode = MODE_HOVER;
514 		RepApplication.app.debugPrint("end deactivate1");
515 		RepApplication.app.debugPrint("end deactivate2");
516 	}
517 }