View Javadoc

1   /*
2    * jcurl java curling software framework http://www.jcurl.org Copyright (C)
3    * 2005-2009 M. Rohrmoser
4    * 
5    * This program is free software; you can redistribute it and/or modify it under
6    * the terms of the GNU General Public License as published by the Free Software
7    * Foundation; either version 2 of the License, or (at your option) any later
8    * version.
9    * 
10   * This program is distributed in the hope that it will be useful, but WITHOUT
11   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12   * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13   * details.
14   * 
15   * You should have received a copy of the GNU General Public License along with
16   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
17   * Place, Suite 330, Boston, MA 02111-1307 USA
18   */
19  package org.jcurl.zui.piccolo;
20  
21  import java.awt.BasicStroke;
22  import java.awt.Color;
23  import java.awt.Cursor;
24  import java.awt.Paint;
25  import java.awt.Shape;
26  import java.awt.Stroke;
27  import java.awt.geom.AffineTransform;
28  import java.awt.geom.Arc2D;
29  import java.awt.geom.Line2D;
30  import java.awt.geom.Point2D;
31  import java.beans.PropertyChangeEvent;
32  import java.beans.PropertyChangeListener;
33  
34  import javax.swing.BoundedRangeModel;
35  import javax.swing.event.ChangeEvent;
36  import javax.swing.event.ChangeListener;
37  
38  import org.apache.commons.logging.Log;
39  import org.jcurl.core.api.IceSize;
40  import org.jcurl.core.api.RockProps;
41  import org.jcurl.core.api.RockSet;
42  import org.jcurl.core.log.JCLoggerFactory;
43  import org.jcurl.core.ui.BroomPromptModel;
44  import org.jcurl.core.ui.ChangeManager;
45  import org.jcurl.core.ui.IceShapes;
46  import org.jcurl.core.ui.Memento;
47  import org.jcurl.core.ui.BroomPromptModel.HandleMemento;
48  import org.jcurl.core.ui.BroomPromptModel.SplitMemento;
49  import org.jcurl.core.ui.BroomPromptModel.XYMemento;
50  import org.jcurl.math.MathVec;
51  
52  import edu.umd.cs.piccolo.PNode;
53  import edu.umd.cs.piccolo.event.PDragEventHandler;
54  import edu.umd.cs.piccolo.event.PInputEvent;
55  import edu.umd.cs.piccolo.nodes.PPath;
56  import edu.umd.cs.piccolo.util.PPaintContext;
57  import edu.umd.cs.piccolo.util.PPickPath;
58  
59  /** Piccolo View + Controller for {@link BroomPromptModel}s. */
60  public class BroomPromptSimple extends PNode implements PropertyChangeListener,
61  		ChangeListener {
62  	private static final Color dark = new IceShapes.RockColors().dark;
63  	private static final Color light = new IceShapes.RockColors().light;
64  	private static final Log log = JCLoggerFactory
65  			.getLogger(BroomPromptSimple.class);
66  	private static final Cursor MOVE_CURSOR = new Cursor(Cursor.MOVE_CURSOR);
67  	private static final double scale0 = 0;
68  	private static final int scale50 = 50;
69  	private static final long serialVersionUID = 3115716478135484000L;
70  	private static final Cursor UPDN_CURSOR = new Cursor(Cursor.N_RESIZE_CURSOR);
71  
72  	/**
73  	 * @param sh
74  	 * @param st
75  	 * @param sp
76  	 * @param minScale
77  	 *            Threshold for <a
78  	 *            href="http://www.cs.umd.edu/hcil/jazz/learn/patterns.shtml#Semantic%20Zooming">semantic
79  	 *            zooming</a>
80  	 */
81  	static PPath node(final Shape sh, final Stroke st, final Paint sp,
82  			final double minScale) {
83  		final PPath s = new PPath(sh, st) {
84  			private static final long serialVersionUID = 5255259239579383074L;
85  
86  			@Override
87  			public void paint(final PPaintContext aPaintContext) {
88  				if (minScale >= 0) {
89  					if (aPaintContext.getScale() < minScale)
90  						return;
91  				} else if (aPaintContext.getScale() > -minScale)
92  					return;
93  				super.paint(aPaintContext);
94  			}
95  		};
96  		// s.setStroke(st);
97  		s.setStrokePaint(sp);
98  		s.setPickable(false);
99  		return s;
100 	}
101 
102 	private ChangeManager changer = null;
103 	private Memento first = null;
104 	private final PNode handle;
105 	private Memento last = null;
106 	private BroomPromptModel model;
107 	private final PNode pie;
108 	private final PPath slider;
109 	private final float stickLength;
110 
111 	public BroomPromptSimple() {
112 		final boolean stickUp = false;
113 		final boolean bothSides = true;
114 		final int pieAngle = 150;
115 		final Color sp = Color.BLACK;
116 		final Color bgc = new Color(1, 1, 1, 0.5f);
117 		final Stroke fine = new BasicStroke(0.01f, BasicStroke.CAP_BUTT,
118 				BasicStroke.JOIN_MITER);
119 		final Stroke bold = new BasicStroke(0.03f, BasicStroke.CAP_ROUND,
120 				BasicStroke.JOIN_MITER);
121 		// final Font fo = new Font("SansSerif", Font.BOLD, 1);
122 		final float halo = RockProps.DEFAULT.getRadius();
123 		final float outer = 0.8f * RockProps.DEFAULT.getRadius();
124 		stickLength = (stickUp ? 1 : -1) * 5 * outer;
125 		final float inner = 0.5F * outer;
126 		setPickable(false);
127 		final BroomPromptSimple self = this;
128 		handle = new PNode() {
129 			private static final long serialVersionUID = -7641452321842902940L;
130 
131 			/**
132 			 * Return true if this node or any pickable descendends are picked.
133 			 * If a pick occurs the pickPath is modified so that this node is
134 			 * always returned as the picked node, event if it was a decendent
135 			 * node that initialy reported the pick.
136 			 * 
137 			 * @see PComposite
138 			 */
139 			@Override
140 			public boolean fullPick(final PPickPath pickPath) {
141 				if (super.fullPick(pickPath)) {
142 					PNode picked = pickPath.getPickedNode();
143 					// this code won't work with internal cameras, because
144 					// it doesn't pop
145 					// the cameras view transform.
146 					while (picked != self) {
147 						pickPath.popTransform(picked
148 								.getTransformReference(false));
149 						pickPath.popNode(picked);
150 						picked = pickPath.getPickedNode();
151 					}
152 					return true;
153 				}
154 				return false;
155 			}
156 		};
157 		{ // opaque Background
158 			final PNode bg = node(new Arc2D.Float(-halo, -halo, 2 * halo,
159 					2 * halo, 0, 360, Arc2D.OPEN), null, null, scale0);
160 			bg.setPaint(bgc);
161 			bg.setPickable(true);
162 			handle.addChild(bg);
163 		}
164 		{ // Cross-hair circles and pie
165 			final int off = 90;
166 			final int pieOff = 180;
167 			final int arrowLengthDegrees = 7;
168 			// colored pie:
169 			pie = node(new Arc2D.Float(-outer, -outer, 2 * outer, 2 * outer,
170 					off - pieOff, pieAngle, Arc2D.PIE), null, null, scale0);
171 			handle.addChild(pie);
172 			// inner circle:
173 			handle.addChild(node(new Arc2D.Float(-inner, -inner, 2 * inner,
174 					2 * inner, off, pieOff + pieAngle, Arc2D.OPEN), fine, sp,
175 					scale50));
176 			// outer circle:
177 			handle.addChild(node(new Arc2D.Float(-outer, -outer, 2 * outer,
178 					2 * outer, off, pieOff + pieAngle
179 							- (14 + arrowLengthDegrees), Arc2D.OPEN), fine, sp,
180 					scale50));
181 			handle.addChild(node(new Arc2D.Float(-outer, -outer, 2 * outer,
182 					2 * outer, off, pieOff + pieAngle, Arc2D.OPEN), fine, sp,
183 					-scale50));
184 			final double ar = Math.PI * (off + pieAngle) / 180.0;
185 			// radius
186 			// if (pieAngle % 90 != 0)
187 			handle.addChild(node(new Line2D.Double(0, 0, -outer * Math.cos(ar),
188 					outer * Math.sin(ar)), bold, sp, scale0));
189 
190 			// arrow:
191 			final float f = outer / 10;
192 			final PPath s = node(IceShapes.createArrowHead(f, 3 * f, 0.5f * f),
193 					null, null, scale50);
194 			s.setPaint(sp);
195 			final double a = Math.PI * (off + pieAngle - arrowLengthDegrees)
196 					/ 180.0;
197 			s.translate(-outer * Math.cos(a), outer * Math.sin(a));
198 			s.rotate(Math.PI * (90 - (off + pieAngle) + 8 + arrowLengthDegrees)
199 					/ 180.0);
200 			handle.addChild(s);
201 
202 			this.addChild(handle);
203 		}
204 		{ // y-axis:
205 			handle.addChild(node(new Line2D.Float(0, -Math.signum(stickLength)
206 					* halo, 0, stickLength), fine, sp, scale0));
207 			// x-axis:
208 			handle.addChild(node(new Line2D.Float(-halo, 0, halo, 0), fine, sp,
209 					scale0));
210 		}
211 		{ // slider
212 			slider = new PPath(IceShapes.createSlider(0.4f * outer, bothSides),
213 					fine);
214 			slider.setStrokePaint(sp);
215 			slider.setPickable(true);
216 			this.addChild(slider);
217 		}
218 		// Set up Event handling
219 		addInputEventListener(new PDragEventHandler() {
220 
221 			/** double-click: flip handle */
222 			@Override
223 			public void mouseClicked(final PInputEvent arg0) {
224 				super.mouseClicked(arg0);
225 				if (arg0.getClickCount() > 1) {
226 					arg0.setHandled(true);
227 					first = new HandleMemento(getModel(), getModel()
228 							.getOutTurn());
229 					last = new HandleMemento(getModel(), !getModel()
230 							.getOutTurn());
231 					ChangeManager.getTrivial(changer).undoable(first, last);
232 					first = last = null;
233 				}
234 			}
235 
236 			/** drag/move */
237 			@Override
238 			public void mouseDragged(final PInputEvent arg0) {
239 				arg0.setHandled(true);
240 				getModel().setValueIsAdjusting(true);
241 				if (false) {
242 					final Point2D p = arg0.getPositionRelativeTo(self
243 							.getParent());
244 					getModel().setBroom(p);
245 				} else
246 					view2model(new XYMemento(getModel(), arg0
247 							.getPositionRelativeTo(self.getParent())));
248 			}
249 
250 			@Override
251 			public void mouseEntered(final PInputEvent arg0) {
252 				super.mouseEntered(arg0);
253 				arg0.pushCursor(MOVE_CURSOR);
254 			}
255 
256 			@Override
257 			public void mouseExited(final PInputEvent arg0) {
258 				super.mouseExited(arg0);
259 				arg0.popCursor();
260 			}
261 
262 			@Override
263 			public void mousePressed(final PInputEvent arg0) {
264 				arg0.setHandled(true);
265 				first = new XYMemento(getModel(), getModel().getBroom());
266 			}
267 
268 			@Override
269 			public void mouseReleased(final PInputEvent pinputevent) {
270 				getModel().setValueIsAdjusting(false);
271 				if (first != null && last != null && first != last)
272 					ChangeManager.getTrivial(changer).undoable(first, last);
273 				first = last = null;
274 			}
275 		});
276 		slider.addInputEventListener(new PDragEventHandler() {
277 			@Override
278 			protected void endDrag(final PInputEvent pinputevent) {
279 				log.debug("speed");
280 			}
281 
282 			/** adjust the slider */
283 			@Override
284 			public void mouseDragged(final PInputEvent arg0) {
285 				arg0.setHandled(true);
286 				final Point2D p = arg0.getPositionRelativeTo(self);
287 				final BoundedRangeModel r = self.getModel()
288 						.getSplitTimeMillis();
289 				if (r == null)
290 					return;
291 				r.setValueIsAdjusting(true);
292 				view2model(new SplitMemento(getModel(), ratio2value(p.getY()
293 						/ stickLength, r)));
294 			}
295 
296 			@Override
297 			public void mouseEntered(final PInputEvent arg0) {
298 				super.mouseEntered(arg0);
299 				arg0.pushCursor(UPDN_CURSOR);
300 			}
301 
302 			@Override
303 			public void mouseExited(final PInputEvent arg0) {
304 				super.mouseExited(arg0);
305 				arg0.popCursor();
306 			}
307 
308 			@Override
309 			public void mousePressed(final PInputEvent arg0) {
310 				arg0.setHandled(true);
311 				first = new SplitMemento(getModel(), getModel()
312 						.getSplitTimeMillis().getValue());
313 			}
314 
315 			@Override
316 			public void mouseReleased(final PInputEvent pinputevent) {
317 				log.debug("speed");
318 				final BoundedRangeModel r = self.getModel()
319 						.getSplitTimeMillis();
320 				if (r == null)
321 					return;
322 				r.setValueIsAdjusting(false);
323 				if (first != null && last != null && first != last)
324 					ChangeManager.getTrivial(changer).undoable(first, last);
325 				first = last = null;
326 			}
327 		});
328 	}
329 
330 	private void adjustSlider(final BoundedRangeModel r) {
331 		// log.info(r.getValue() + "/" + r.getMaximum());
332 		slider.setPaint(IceShapes.sliderColor(r));
333 		slider.getTransformReference(true).setToTranslation(0,
334 				stickLength * IceShapes.value2ratio(r));
335 		slider.invalidateFullBounds();
336 		slider.invalidatePaint();
337 		// FIXME getModel().firePropertyChange("splitTimeMillis", r, r);
338 	}
339 
340 	public ChangeManager getChanger() {
341 		return changer;
342 	}
343 
344 	public BroomPromptModel getModel() {
345 		return model;
346 	}
347 
348 	public void propertyChange(final PropertyChangeEvent evt) {
349 		log.debug(evt.getPropertyName());
350 		if ("broom".equals(evt.getPropertyName()))
351 			setBroom((Point2D) evt.getNewValue());
352 		else if ("idx16".equals(evt.getPropertyName()))
353 			setIdx16((Integer) evt.getNewValue());
354 		else if ("outTurn".equals(evt.getPropertyName()))
355 			setOutTurn((Boolean) evt.getNewValue());
356 		else if ("splitTimeMillis".equals(evt.getPropertyName())) {
357 			final BoundedRangeModel os = (BoundedRangeModel) evt.getOldValue();
358 			if (os != null)
359 				os.removeChangeListener(this);
360 			setSlider((BoundedRangeModel) evt.getNewValue());
361 		}
362 	}
363 
364 	private int ratio2value(final double ra, final BoundedRangeModel r) {
365 		return r.getMaximum() + (int) ((r.getMinimum() - r.getMaximum()) * ra);
366 	}
367 
368 	/** adjust position + rotation */
369 	private void setBroom(final Point2D b) {
370 		if (b == null)
371 			return;
372 		final AffineTransform t = getTransformReference(true);
373 		t.setToIdentity();
374 		t.translate(b.getX(), b.getY());
375 		MathVec.rotate(t, b.getX(), b.getY() - IceSize.FAR_HACK_2_TEE);
376 		MathVec.rotate(t, 0, 1);
377 		invalidateFullBounds();
378 		invalidatePaint();
379 	}
380 
381 	public void setChanger(final ChangeManager changer) {
382 		final ChangeManager old = this.changer;
383 		if (old == changer)
384 			return;
385 		this.changer = changer;
386 		// firePropertyChange("changer", old, this.changer);
387 	}
388 
389 	/** adjust Color */
390 	private void setIdx16(final int i) {
391 		if (i < 0 || i >= RockSet.ROCKS_PER_SET)
392 			pie.setPaint(null);
393 		else
394 			pie.setPaint(RockSet.isDark(i) ? dark : light);
395 		pie.invalidatePaint();
396 	}
397 
398 	public void setModel(final BroomPromptModel model) {
399 		if (this.model == model)
400 			return;
401 		if (this.model != null) {
402 			this.model.removePropertyChangeListener(this);
403 			if (this.model.getSplitTimeMillis() != null)
404 				this.model.getSplitTimeMillis().removeChangeListener(this);
405 		}
406 		this.model = model;
407 		setVisible(this.model != null && model.getBroom() != null);
408 		if (this.model != null) {
409 			setBroom(this.model.getBroom());
410 			setIdx16(this.model.getIdx16());
411 			setOutTurn(this.model.getOutTurn());
412 			setSlider(this.model.getSplitTimeMillis());
413 			this.model.addPropertyChangeListener(this);
414 		}
415 		invalidatePaint();
416 	}
417 
418 	private void setOutTurn(final boolean ot) {
419 		handle.getTransformReference(true).setToScale(ot ? -1 : 1, 1);
420 		handle.invalidatePaint();
421 	}
422 
423 	private void setSlider(final BoundedRangeModel s) {
424 		slider.setVisible(s != null);
425 		if (s == null)
426 			return;
427 		s.addChangeListener(this);
428 		adjustSlider(s);
429 	}
430 
431 	public void stateChanged(final ChangeEvent e) {
432 		// log.info(e);
433 		if (e.getSource() instanceof BoundedRangeModel)
434 			adjustSlider((BoundedRangeModel) e.getSource());
435 	}
436 
437 	private void view2model(final Memento<?> m) {
438 		ChangeManager.getTrivial(changer).temporary(last = m);
439 	}
440 }