1 /* |
|
2 * CDDL HEADER START |
|
3 * |
|
4 * The contents of this file are subject to the terms of the |
|
5 * Common Development and Distribution License (the "License"). |
|
6 * You may not use this file except in compliance with the License. |
|
7 * |
|
8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE |
|
9 * or http://www.opensolaris.org/os/licensing. |
|
10 * See the License for the specific language governing permissions |
|
11 * and limitations under the License. |
|
12 * |
|
13 * When distributing Covered Code, include this CDDL HEADER in each |
|
14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE. |
|
15 * If applicable, add the following below this CDDL HEADER, with the |
|
16 * fields enclosed by brackets "[]" replaced with your own identifying |
|
17 * information: Portions Copyright [yyyy] [name of copyright owner] |
|
18 * |
|
19 * CDDL HEADER END |
|
20 */ |
|
21 |
|
22 /* |
|
23 * Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved. |
|
24 */ |
|
25 |
|
26 package com.oracle.solaris.vp.util.swing.time; |
|
27 |
|
28 import java.awt.*; |
|
29 import java.awt.event.*; |
|
30 import java.awt.geom.*; |
|
31 import java.awt.image.BufferedImage; |
|
32 import java.beans.*; |
|
33 import java.util.Calendar; |
|
34 import javax.swing.*; |
|
35 import org.jdesktop.animation.timing.*; |
|
36 import com.oracle.solaris.vp.util.misc.finder.Finder; |
|
37 import com.oracle.solaris.vp.util.swing.*; |
|
38 |
|
39 @SuppressWarnings({"serial"}) |
|
40 public class AnalogClock extends JPanel implements MouseListener, |
|
41 MouseMotionListener, TimingTarget { |
|
42 |
|
43 // |
|
44 // Static data |
|
45 // |
|
46 |
|
47 private static final int _DIAMETER = 150; |
|
48 |
|
49 // |
|
50 // Instance data |
|
51 // |
|
52 |
|
53 private TimeModel model; |
|
54 private TimeSelectionModel selModel; |
|
55 |
|
56 private PropertyChangeListener timeListener = |
|
57 new PropertyChangeListener() { |
|
58 @Override |
|
59 public void propertyChange(PropertyChangeEvent event) { |
|
60 timeChanged(); |
|
61 } |
|
62 }; |
|
63 |
|
64 // Cached images to speed up repaints |
|
65 private BufferedImage baseImage; |
|
66 private BufferedImage timeImage; |
|
67 private BufferedImage centerImage; |
|
68 |
|
69 private int frameWidth; |
|
70 private Point centerPoint; |
|
71 private Point mousePoint; |
|
72 private boolean interactive; |
|
73 private boolean useToolTips; |
|
74 private SunIcon sunIcon = new SunIcon(); |
|
75 private MoonIcon moonIcon = new MoonIcon(); |
|
76 private StarIcon starIcon = new StarIcon(); |
|
77 private Shape amPm; |
|
78 private Point hourPoint; |
|
79 private Point minPoint; |
|
80 private Point secPoint; |
|
81 |
|
82 private Animator amPmAnimator = new Animator(300, this); |
|
83 private float amPmAnimOffset; |
|
84 private int amPmAnimFactor; |
|
85 |
|
86 // UI prefs |
|
87 private Color centerColor = Color.black; |
|
88 private Color frameColor = ColorUtil.darker( |
|
89 UIManager.getColor("Panel.background"), .2f); |
|
90 private Color faceColor = Color.white; |
|
91 private Color hourHandColor = Color.black; |
|
92 private Color hourMarkColor = Color.black; |
|
93 private float lightAngle = (float)Math.toRadians(122); |
|
94 private Color minuteHandColor = Color.black; |
|
95 private Color minuteMarkColor = Color.gray; |
|
96 private Color secondHandColor = Color.red.darker(); |
|
97 private Color highlightColor = new Color(0, 0, 204); |
|
98 private Color amColor = new Color(148, 188, 246); |
|
99 private Color pmColor = new Color(0, 0, 78); |
|
100 |
|
101 // |
|
102 // Constructors |
|
103 // |
|
104 |
|
105 /** |
|
106 * Constructs an {@code AnalogClock} for the given model. |
|
107 */ |
|
108 public AnalogClock(TimeModel model) { |
|
109 setTimeModel(model); |
|
110 setTimeSelectionModel(new SimpleTimeSelectionModel()); |
|
111 setOpaque(false); |
|
112 setPreferredDiameter(_DIAMETER); |
|
113 } |
|
114 |
|
115 /** |
|
116 * Constructs an {@code AnalogClock} with a zero {@link |
|
117 * TimeModel#getTimeOffset offset} from the system clock. |
|
118 */ |
|
119 public AnalogClock() { |
|
120 this(new SimpleTimeModel()); |
|
121 } |
|
122 |
|
123 // |
|
124 // MouseListener methods |
|
125 // |
|
126 |
|
127 @Override |
|
128 public void mouseClicked(MouseEvent e) { |
|
129 int selectedField = selModel.getSelectedField(); |
|
130 if (selectedField == Calendar.AM_PM && GUIUtil.isUnmodifiedClick(e, 1)) |
|
131 { |
|
132 amPmAnimOffset = (float)Math.PI; |
|
133 |
|
134 amPmAnimFactor = model.getCalendar().get( |
|
135 selectedField) == Calendar.AM ? 1 : -1; |
|
136 |
|
137 // 43200000 = 1000 * 60 * 60 * 12 = milliseconds in 12-hour change |
|
138 long delta = amPmAnimFactor * 43200000; |
|
139 model.setTimeOffset(model.getTimeOffset() + delta); |
|
140 |
|
141 amPmAnimator.start(); |
|
142 } |
|
143 } |
|
144 |
|
145 @Override |
|
146 public void mouseEntered(MouseEvent e) { |
|
147 } |
|
148 |
|
149 @Override |
|
150 public void mouseExited(MouseEvent e) { |
|
151 mousePoint = null; |
|
152 repaint(); |
|
153 } |
|
154 |
|
155 @Override |
|
156 public void mousePressed(MouseEvent e) { |
|
157 mousePoint = null; |
|
158 repaint(); |
|
159 } |
|
160 |
|
161 @Override |
|
162 public void mouseReleased(MouseEvent e) { |
|
163 mouseMoved(e); |
|
164 } |
|
165 |
|
166 // |
|
167 // MouseMotionListener methods |
|
168 // |
|
169 |
|
170 @Override |
|
171 public void mouseDragged(MouseEvent e) { |
|
172 int selectedField = selModel.getSelectedField(); |
|
173 if (selectedField != Calendar.HOUR && |
|
174 selectedField != Calendar.MINUTE && |
|
175 selectedField != Calendar.SECOND) { |
|
176 return; |
|
177 } |
|
178 |
|
179 Point centerPoint = this.centerPoint; |
|
180 Point dragPoint = e.getPoint(); |
|
181 int dY = centerPoint.y - dragPoint.y; |
|
182 int dX = dragPoint.x - centerPoint.x; |
|
183 float angle = (float)Math.atan2(dY, dX); |
|
184 |
|
185 int newVal; |
|
186 int period; |
|
187 |
|
188 if (selectedField == Calendar.HOUR) { |
|
189 newVal = (int)radiansToHour(angle); |
|
190 period = 12; |
|
191 } else { |
|
192 newVal = (int)radiansToMin(angle); |
|
193 period = 60; |
|
194 } |
|
195 |
|
196 Calendar cal = model.getCalendar(); |
|
197 int curVal = cal.get(selectedField); |
|
198 int factor = (newVal - curVal + period) % period; |
|
199 if (factor >= period / 2) { |
|
200 factor -= period; |
|
201 } |
|
202 |
|
203 long timeOffset = model.getTimeOffset() + factor * |
|
204 timeFieldToMillis(selectedField); |
|
205 model.setTimeOffset(timeOffset); |
|
206 } |
|
207 |
|
208 @Override |
|
209 public void mouseMoved(MouseEvent e) { |
|
210 mousePoint = e.getPoint(); |
|
211 repaint(); |
|
212 } |
|
213 |
|
214 // |
|
215 // TimingTarget methods |
|
216 // |
|
217 |
|
218 @Override |
|
219 public void begin() { |
|
220 } |
|
221 |
|
222 @Override |
|
223 public void end() { |
|
224 } |
|
225 |
|
226 @Override |
|
227 public void repeat() { |
|
228 } |
|
229 |
|
230 @Override |
|
231 public void timingEvent(float fraction) { |
|
232 amPmAnimOffset = amPmAnimFactor * (float)Math.PI * (1 - fraction); |
|
233 timeImage = null; |
|
234 repaint(); |
|
235 } |
|
236 |
|
237 // |
|
238 // JComponent methods |
|
239 // |
|
240 |
|
241 @Override |
|
242 protected void paintComponent(Graphics g1) { |
|
243 super.paintComponent(g1); |
|
244 |
|
245 Graphics2D g = (Graphics2D)g1; |
|
246 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, |
|
247 RenderingHints.VALUE_ANTIALIAS_ON); |
|
248 |
|
249 Insets insets = getInsets(); |
|
250 int width = getWidth() - insets.left - insets.right; |
|
251 int height = getHeight() - insets.top - insets.bottom; |
|
252 |
|
253 if (width <= 0 || height <= 0) { |
|
254 return; |
|
255 } |
|
256 |
|
257 // TimeModel is square |
|
258 int diameter = Math.min(width, height); |
|
259 |
|
260 int diff = height - width; |
|
261 int hDiff = diff / 2; |
|
262 if (diff > 0) { |
|
263 diameter = width; |
|
264 insets.top += hDiff; |
|
265 insets.bottom += diff - hDiff; |
|
266 } else if (diff < 0) { |
|
267 diameter = height; |
|
268 insets.left -= hDiff; |
|
269 insets.right -= diff - hDiff; |
|
270 } |
|
271 |
|
272 g.translate(insets.left, insets.top); |
|
273 |
|
274 createBaseImage(diameter); |
|
275 g.drawImage(baseImage, 0, 0, null); |
|
276 |
|
277 g.translate(frameWidth, frameWidth); |
|
278 |
|
279 diameter -= 2 * frameWidth; |
|
280 int radius = Math.round(diameter/ 2f); |
|
281 |
|
282 createTimeImage(diameter); |
|
283 g.drawImage(timeImage, 0, 0, null); |
|
284 |
|
285 Point centerPoint = new Point(radius, radius); |
|
286 Point mousePoint = this.mousePoint; |
|
287 |
|
288 if (mousePoint != null) { |
|
289 int selectedField = -1; |
|
290 mousePoint = (Point)mousePoint.clone(); |
|
291 mousePoint.translate(-frameWidth - insets.left, |
|
292 -frameWidth - insets.top); |
|
293 |
|
294 Line2D line = new Line2D.Float(); |
|
295 |
|
296 line.setLine(centerPoint, hourPoint); |
|
297 double hourDist = line.ptSegDist(mousePoint); |
|
298 |
|
299 line.setLine(centerPoint, minPoint); |
|
300 double minDist = line.ptSegDist(mousePoint); |
|
301 |
|
302 line.setLine(centerPoint, secPoint); |
|
303 double secDist = line.ptSegDist(mousePoint); |
|
304 |
|
305 // Is the mouse close enough to a hand to call it selected? |
|
306 double closest = Math.min(hourDist, Math.min(minDist, secDist)); |
|
307 if (closest / diameter <= .02f) { |
|
308 if (hourDist == closest) { |
|
309 selectedField = Calendar.HOUR; |
|
310 drawHourHand(g, diameter, centerPoint, hourPoint); |
|
311 } else if (minDist == closest) { |
|
312 selectedField = Calendar.MINUTE; |
|
313 drawMinuteHand(g, diameter, centerPoint, minPoint); |
|
314 } else { |
|
315 selectedField = Calendar.SECOND; |
|
316 drawSecondHand(g, diameter, centerPoint, secPoint); |
|
317 } |
|
318 } else if (amPm.contains(mousePoint)) { |
|
319 selectedField = Calendar.AM_PM; |
|
320 } |
|
321 |
|
322 setSelectedField(selectedField); |
|
323 } |
|
324 |
|
325 createCenterImage(diameter); |
|
326 g.drawImage(centerImage, 0, 0, null); |
|
327 |
|
328 centerPoint.translate(frameWidth + insets.left, |
|
329 frameWidth + insets.top); |
|
330 |
|
331 this.centerPoint = centerPoint; |
|
332 |
|
333 g.translate(-frameWidth - insets.left, -frameWidth - insets.top); |
|
334 } |
|
335 |
|
336 // |
|
337 // AnalogClock methods |
|
338 // |
|
339 |
|
340 protected float amPmToRadians() { |
|
341 Calendar cal = model.getCalendar(); |
|
342 int hour = cal.get(Calendar.HOUR); |
|
343 int minute = cal.get(Calendar.MINUTE); |
|
344 int second = cal.get(Calendar.SECOND); |
|
345 int amPm = cal.get(Calendar.AM_PM); |
|
346 return amPmToRadians(amPm, hour, minute, second); |
|
347 } |
|
348 |
|
349 protected Shape drawAmPm(Graphics2D g, int diameter) { |
|
350 // Create shapes |
|
351 |
|
352 float outerMargin = diameter * .17f; |
|
353 float innerMargin = diameter * .105f; |
|
354 |
|
355 diameter -= Math.round(2 * outerMargin); |
|
356 float radius = diameter / 2f; |
|
357 |
|
358 int offset = Math.round(outerMargin); |
|
359 g.translate(offset, offset); |
|
360 |
|
361 Arc2D.Float outer = new Arc2D.Float(0, 0, |
|
362 diameter, diameter, 0, 180, Arc2D.OPEN); |
|
363 |
|
364 int innerDiam = Math.round(2 * innerMargin); |
|
365 Arc2D.Float inner = new Arc2D.Float(radius - innerMargin, |
|
366 radius - innerMargin, innerDiam, innerDiam, 180, -180, Arc2D.OPEN); |
|
367 |
|
368 Line2D.Float[] lines = { |
|
369 new Line2D.Float(outer.getEndPoint(), inner.getStartPoint()), |
|
370 new Line2D.Float(inner.getEndPoint(), outer.getStartPoint()), |
|
371 }; |
|
372 |
|
373 GeneralPath window = new GeneralPath(outer); |
|
374 window.append(lines[0], true); |
|
375 window.append(inner, true); |
|
376 window.append(lines[1], true); |
|
377 window.closePath(); |
|
378 |
|
379 // Fill background |
|
380 |
|
381 float angle = (float)Math.PI - amPmToRadians() - amPmAnimOffset; |
|
382 float cos = (float)Math.cos(angle); |
|
383 float sin = (float)Math.sin(angle); |
|
384 float xDelta = radius * cos; |
|
385 float yDelta = radius * sin; |
|
386 |
|
387 g.setPaint(new GradientPaint( |
|
388 Math.round(radius + xDelta), Math.round(radius + yDelta), amColor, |
|
389 Math.round(radius - xDelta), Math.round(radius - yDelta), pmColor)); |
|
390 g.fill(window); |
|
391 |
|
392 // Paint icons |
|
393 |
|
394 Shape clip = g.getClip(); |
|
395 g.setClip(window); |
|
396 |
|
397 float arcWidth = radius - innerMargin; |
|
398 float midPointToCtr = innerMargin + arcWidth * .5f; |
|
399 int sunIconDiam = (int)(arcWidth * .7f); |
|
400 float sunIconRad = sunIconDiam / 2f; |
|
401 xDelta = midPointToCtr * cos; |
|
402 yDelta = midPointToCtr * sin; |
|
403 |
|
404 sunIcon.setDiameter(sunIconDiam); |
|
405 sunIcon.paintIcon(this, g, Math.round(radius + xDelta - sunIconRad), |
|
406 Math.round(radius + yDelta - sunIconRad)); |
|
407 |
|
408 int moonIconDiam = (int)(sunIconDiam * sunIcon.getOrbSizePercentage()); |
|
409 float moonIconRad = moonIconDiam / 2f; |
|
410 |
|
411 moonIcon.setDiameter(moonIconDiam); |
|
412 moonIcon.paintIcon(this, g, Math.round(radius - xDelta - moonIconRad), |
|
413 Math.round(radius - yDelta - moonIconRad)); |
|
414 |
|
415 int starIconDiam = (int)(moonIconDiam * .4f); |
|
416 float starIconRad = starIconDiam / 2f; |
|
417 |
|
418 starIcon.setDiameter(starIconDiam); |
|
419 |
|
420 // Angle (degree) offset from moon, percent distance between arcs |
|
421 float[] starData = { |
|
422 -48f, .55f, |
|
423 -30f, .22f, |
|
424 -25f, .7f, |
|
425 5f, .85f, |
|
426 20f, .25f, |
|
427 25f, .65f, |
|
428 45f, .35f, |
|
429 }; |
|
430 |
|
431 for (int i = 0; i < starData.length; i += 2) { |
|
432 float starAngle = (float)Math.toRadians(starData[i]); |
|
433 float starDist = innerMargin + arcWidth * starData[i + 1]; |
|
434 |
|
435 int x = Math.round(radius - starDist * |
|
436 (float)Math.cos(angle + starAngle) - starIconRad); |
|
437 |
|
438 int y = Math.round(radius - starDist * |
|
439 (float)Math.sin(angle + starAngle) - starIconRad); |
|
440 |
|
441 starIcon.paintIcon(this, g, x, y); |
|
442 } |
|
443 |
|
444 g.setClip(clip); |
|
445 |
|
446 // Paint inset border |
|
447 |
|
448 Stroke stroke = g.getStroke(); |
|
449 g.setStroke(new BasicStroke(diameter / 80)); |
|
450 |
|
451 cos = (float)Math.cos(lightAngle); |
|
452 sin = (float)Math.sin(lightAngle); |
|
453 xDelta = radius * cos; |
|
454 yDelta = radius * sin; |
|
455 |
|
456 Color color = Color.gray; |
|
457 Color shadow = ColorUtil.darker(color, .25f); |
|
458 Color light = ColorUtil.darker(Color.white, .25f); |
|
459 |
|
460 g.setPaint(new GradientPaint(radius + xDelta, radius - yDelta, shadow, |
|
461 radius - xDelta, radius + yDelta, light, false)); |
|
462 g.draw(outer); |
|
463 |
|
464 xDelta = innerMargin * cos; |
|
465 yDelta = innerMargin * sin; |
|
466 |
|
467 g.setPaint(new GradientPaint(radius + xDelta, radius - yDelta, light, |
|
468 radius - xDelta, radius + yDelta, shadow, false)); |
|
469 g.draw(inner); |
|
470 |
|
471 float theta = lightAngle - (float)Math.PI / 2f; |
|
472 float pct = (1 - (float)Math.cos(theta)) / 2f; |
|
473 |
|
474 g.setColor(ColorUtil.blend(light, shadow, pct)); |
|
475 g.draw(lines[0]); |
|
476 g.draw(lines[1]); |
|
477 |
|
478 g.setStroke(stroke); |
|
479 g.translate(-offset, -offset); |
|
480 |
|
481 Shape shape = AffineTransform.getTranslateInstance(offset, offset). |
|
482 createTransformedShape(window); |
|
483 |
|
484 return shape; |
|
485 } |
|
486 |
|
487 protected void drawCenterDot(Graphics2D g, int diameter) { |
|
488 float radius = diameter / 2f; |
|
489 int dotDiam = Math.round(diameter * .06f); |
|
490 float dotRadius = dotDiam / 2f; |
|
491 float pctFromCtr = .75f; |
|
492 float distFromCtr = dotRadius * pctFromCtr; |
|
493 float xCenter = radius + distFromCtr * (float)Math.cos(lightAngle); |
|
494 float yCenter = radius - distFromCtr * (float)Math.sin(lightAngle); |
|
495 |
|
496 Color focusColor = ColorUtil.lighter(centerColor, .65f); |
|
497 float[] fractions = {0.0f, 1.0f}; |
|
498 Color[] colors = {focusColor, centerColor}; |
|
499 RadialGradientPaint paint = new RadialGradientPaint( |
|
500 xCenter, yCenter, dotRadius, fractions, colors, |
|
501 MultipleGradientPaint.CycleMethod.NO_CYCLE); |
|
502 g.setPaint(paint); |
|
503 int coord = Math.round(radius - dotRadius); |
|
504 g.fillOval(coord, coord, dotDiam, dotDiam); |
|
505 } |
|
506 |
|
507 protected void drawFace(Graphics2D g, int diameter) { |
|
508 g.setColor(faceColor); |
|
509 g.fillOval(0, 0, diameter, diameter); |
|
510 } |
|
511 |
|
512 protected int drawFrame(Graphics2D g, int diameter) { |
|
513 // Draw outline |
|
514 int frameWidth = 0; |
|
515 float radius = diameter / 2f; |
|
516 |
|
517 float cos = (float)Math.cos(lightAngle); |
|
518 float sin = (float)Math.sin(lightAngle); |
|
519 float pctFromEdge = .67f; |
|
520 float xStart = radius * (1 + cos); |
|
521 float yStart = radius * (1 - sin); |
|
522 float xEnd = radius * (1 + pctFromEdge * cos); |
|
523 float yEnd = radius * (1 - pctFromEdge * sin); |
|
524 |
|
525 g.setPaint(new GradientPaint(xStart, yStart, |
|
526 ColorUtil.darker(Color.white, .25f), xEnd, yEnd, |
|
527 ColorUtil.darker(frameColor, .25f), false)); |
|
528 g.fillOval(frameWidth, frameWidth, diameter, diameter); |
|
529 |
|
530 // Draw outer edge |
|
531 int gap = Math.round(diameter * .006f); |
|
532 frameWidth += gap; |
|
533 diameter -= 2 * gap; |
|
534 radius = diameter / 2f; |
|
535 |
|
536 xStart = radius * (1 + cos); |
|
537 yStart = radius * (1 - sin); |
|
538 xEnd = radius * (1 + pctFromEdge * cos); |
|
539 yEnd = radius * (1 - pctFromEdge * sin); |
|
540 |
|
541 g.setPaint(new GradientPaint(xStart, yStart, Color.white, xEnd, yEnd, |
|
542 frameColor, false)); |
|
543 g.fillOval(frameWidth, frameWidth, diameter, diameter); |
|
544 |
|
545 // Draw inner edge |
|
546 gap = Math.round(diameter * .025f); |
|
547 frameWidth += gap; |
|
548 diameter -= 2 * gap; |
|
549 radius = diameter / 2f; |
|
550 |
|
551 cos = (float)Math.cos(lightAngle + Math.PI); |
|
552 sin = (float)Math.sin(lightAngle + Math.PI); |
|
553 pctFromEdge = .5f; |
|
554 xStart = radius * (1 + cos); |
|
555 yStart = radius * (1 - sin); |
|
556 xEnd = radius * (1 + pctFromEdge * cos); |
|
557 yEnd = radius * (1 - pctFromEdge * sin); |
|
558 |
|
559 g.setPaint(new GradientPaint(xStart, yStart, |
|
560 ColorUtil.lighter(frameColor, .55f), xEnd, yEnd, frameColor, |
|
561 false)); |
|
562 g.fillOval(frameWidth, frameWidth, diameter, diameter); |
|
563 |
|
564 gap = Math.round(diameter * .011f); |
|
565 frameWidth += gap; |
|
566 |
|
567 return frameWidth; |
|
568 } |
|
569 |
|
570 protected void drawHand(Graphics2D g, Color color, float handWidth, |
|
571 Point p1, Point p2) { |
|
572 |
|
573 Stroke stroke = g.getStroke(); |
|
574 g.setStroke(new BasicStroke(handWidth, BasicStroke.CAP_ROUND, |
|
575 BasicStroke.JOIN_MITER)); |
|
576 |
|
577 g.setColor(color); |
|
578 g.drawLine(p1.x, p1.y, p2.x, p2.y); |
|
579 |
|
580 g.setStroke(stroke); |
|
581 } |
|
582 |
|
583 protected Point drawHand(Graphics2D g, Color color, float handWidth, |
|
584 float handLength, float angle, int diameter) { |
|
585 |
|
586 float radius = diameter / 2f; |
|
587 float cos = (float)Math.cos(angle); |
|
588 float sin = (float)Math.sin(angle); |
|
589 int x = Math.round(radius + handLength * cos); |
|
590 int y = Math.round(radius - handLength * sin); |
|
591 |
|
592 // Shadow |
|
593 float offset = diameter * .02f; |
|
594 cos = (float)Math.cos(lightAngle + Math.PI); |
|
595 sin = (float)Math.sin(lightAngle + Math.PI); |
|
596 int xOffset = Math.round(offset * cos); |
|
597 int yOffset = -Math.round(offset * sin); |
|
598 |
|
599 drawHand(g, ColorUtil.alpha(Color.black, .2f), handWidth, |
|
600 new Point((int)radius + xOffset, (int)radius + yOffset), |
|
601 new Point(x + xOffset, y + yOffset)); |
|
602 |
|
603 // Hand |
|
604 Point p = new Point(x, y); |
|
605 drawHand(g, color, handWidth, new Point((int)radius, (int)radius), p); |
|
606 |
|
607 return p; |
|
608 } |
|
609 |
|
610 protected void drawHourHand(Graphics2D g, int diameter, Point p1, |
|
611 Point p2) { |
|
612 drawHand(g, highlightColor, diameter * .02f, p1, p2); |
|
613 } |
|
614 |
|
615 protected Point drawHourHand(Graphics2D g, int diameter) { |
|
616 return drawHand(g, hourHandColor, diameter * .02f, diameter * .275f, |
|
617 hourToRadians(), diameter); |
|
618 } |
|
619 |
|
620 protected void drawHourMarks(Graphics2D g, int diameter) { |
|
621 float markWidth = diameter * .015f; |
|
622 float markLength = diameter * .04f; |
|
623 float pctFromCtr = .9f; |
|
624 float radius = diameter / 2f; |
|
625 |
|
626 Stroke stroke = g.getStroke(); |
|
627 g.setStroke(new BasicStroke(markWidth, BasicStroke.CAP_ROUND, |
|
628 BasicStroke.JOIN_MITER)); |
|
629 g.setColor(hourMarkColor); |
|
630 |
|
631 float angleConst = (float)Math.PI / 6f; |
|
632 float distFromCtrStart = radius * pctFromCtr; |
|
633 float distFromCtrEnd = distFromCtrStart - markLength; |
|
634 |
|
635 for (int i = 0; i < 12; i++) { |
|
636 float angle = i * angleConst; |
|
637 float cos = (float)Math.cos(angle); |
|
638 float sin = (float)Math.sin(angle); |
|
639 |
|
640 int xStart = Math.round(radius + distFromCtrStart * cos); |
|
641 int yStart = Math.round(radius - distFromCtrStart * sin); |
|
642 |
|
643 int xEnd = Math.round(radius + distFromCtrEnd * cos); |
|
644 int yEnd = Math.round(radius - distFromCtrEnd * sin); |
|
645 |
|
646 g.drawLine(xStart, yStart, xEnd, yEnd); |
|
647 } |
|
648 |
|
649 g.setStroke(stroke); |
|
650 } |
|
651 |
|
652 protected void drawMinuteHand(Graphics2D g, int diameter, Point p1, |
|
653 Point p2) { |
|
654 drawHand(g, highlightColor, diameter * .02f, p1, p2); |
|
655 } |
|
656 |
|
657 protected Point drawMinuteHand(Graphics2D g, int diameter) { |
|
658 return drawHand(g, minuteHandColor, diameter * .02f, diameter * .405f, |
|
659 minToRadians(), diameter); |
|
660 } |
|
661 |
|
662 protected void drawMinuteMarks(Graphics2D g, int diameter) { |
|
663 float markWidth = diameter * .009f; |
|
664 float markLength = diameter * .015f; |
|
665 float pctFromCtr = .9f; |
|
666 float radius = diameter / 2f; |
|
667 |
|
668 Stroke stroke = g.getStroke(); |
|
669 g.setStroke(new BasicStroke(markWidth, BasicStroke.CAP_ROUND, |
|
670 BasicStroke.JOIN_MITER)); |
|
671 g.setColor(minuteMarkColor); |
|
672 |
|
673 float angleConst = (float)Math.PI / 30f; |
|
674 float distFromCtrStart = radius * pctFromCtr; |
|
675 float distFromCtrEnd = distFromCtrStart - markLength; |
|
676 |
|
677 for (int i = 0; i < 60; i++) { |
|
678 if (i % 5 != 0) { |
|
679 float angle = i * angleConst; |
|
680 float cos = (float)Math.cos(angle); |
|
681 float sin = (float)Math.sin(angle); |
|
682 |
|
683 int xStart = Math.round(radius + distFromCtrStart * cos); |
|
684 int yStart = Math.round(radius - distFromCtrStart * sin); |
|
685 |
|
686 int xEnd = Math.round(radius + distFromCtrEnd * cos); |
|
687 int yEnd = Math.round(radius - distFromCtrEnd * sin); |
|
688 |
|
689 g.drawLine(xStart, yStart, xEnd, yEnd); |
|
690 } |
|
691 } |
|
692 |
|
693 g.setStroke(stroke); |
|
694 } |
|
695 |
|
696 protected void drawSecondHand(Graphics2D g, int diameter, Point p1, |
|
697 Point p2) { |
|
698 drawHand(g, highlightColor, diameter * .011f, p1, p2); |
|
699 } |
|
700 |
|
701 protected Point drawSecondHand(Graphics2D g, int diameter) { |
|
702 return drawHand(g, secondHandColor, diameter * .011f, |
|
703 diameter * .405f, secToRadians(), diameter); |
|
704 } |
|
705 |
|
706 protected void drawShadow(Graphics2D g, int diameter) { |
|
707 // This puts the shadow around the entire face, which also looks nice |
|
708 // float offset = 0f; |
|
709 // float[] fractions = {0f, .95f, 1f}; |
|
710 |
|
711 float radius = diameter / 2f; |
|
712 Color shadow = Color.black; |
|
713 Color focus = ColorUtil.alpha(Color.black, 0f); |
|
714 float[] fractions = {0f, .94f, 1f}; |
|
715 Color[] colors = {focus, focus, shadow}; |
|
716 |
|
717 float cos = (float)Math.cos(lightAngle + Math.PI); |
|
718 float sin = (float)Math.sin(lightAngle + Math.PI); |
|
719 float offset = radius * .02f; |
|
720 float xOffset = offset * cos; |
|
721 float yOffset = -offset * sin; |
|
722 RadialGradientPaint paint = new RadialGradientPaint(radius + xOffset, |
|
723 radius + yOffset, radius + offset, fractions, colors, |
|
724 MultipleGradientPaint.CycleMethod.NO_CYCLE); |
|
725 |
|
726 g.setPaint(paint); |
|
727 g.fillOval(0, 0, diameter, diameter); |
|
728 } |
|
729 |
|
730 /** |
|
731 * Gets the color at the AM end of the AM-PM gradient. |
|
732 */ |
|
733 public Color getAmColor() { |
|
734 return amColor; |
|
735 } |
|
736 |
|
737 /** |
|
738 * Gets the {@code Animator} the controls the animation of the interactive |
|
739 * change from AM to PM and back. |
|
740 */ |
|
741 public Animator getAmPmAnimator() { |
|
742 return amPmAnimator; |
|
743 } |
|
744 |
|
745 /** |
|
746 * Gets the color of the center dial of the clock. |
|
747 */ |
|
748 public Color getCenterColor() { |
|
749 return centerColor; |
|
750 } |
|
751 |
|
752 /** |
|
753 * Gets the color of the face of the clock. |
|
754 */ |
|
755 public Color getFaceColor() { |
|
756 return faceColor; |
|
757 } |
|
758 |
|
759 /** |
|
760 * Gets the color of the outer frame of the clock. |
|
761 */ |
|
762 public Color getFrameColor() { |
|
763 return frameColor; |
|
764 } |
|
765 |
|
766 /** |
|
767 * Gets the color to use when highlighting a hand. |
|
768 */ |
|
769 public Color getHighlightColor() { |
|
770 return highlightColor; |
|
771 } |
|
772 |
|
773 public Color getHourHandColor() { |
|
774 return hourHandColor; |
|
775 } |
|
776 |
|
777 /** |
|
778 * Gets the color of the hour mark on the clock face. |
|
779 */ |
|
780 public Color getHourMarkColor() { |
|
781 return hourMarkColor; |
|
782 } |
|
783 |
|
784 /** |
|
785 * Gets the angle of the light, in radians, with 0 at three o'clock, shining |
|
786 * onto the clock. |
|
787 */ |
|
788 public float getLightAngle() { |
|
789 return lightAngle; |
|
790 } |
|
791 |
|
792 public Color getMinuteHandColor() { |
|
793 return minuteHandColor; |
|
794 } |
|
795 |
|
796 /** |
|
797 * Gets the color of the minute mark on the clock face. |
|
798 */ |
|
799 public Color getMinuteMarkColor() { |
|
800 return minuteMarkColor; |
|
801 } |
|
802 |
|
803 /** |
|
804 * Gets the color at the PM end of the AM-PM gradient. |
|
805 */ |
|
806 public Color getPmColor() { |
|
807 return pmColor; |
|
808 } |
|
809 |
|
810 public Color getSecondHandColor() { |
|
811 return secondHandColor; |
|
812 } |
|
813 |
|
814 public TimeModel getTimeModel() { |
|
815 return model; |
|
816 } |
|
817 |
|
818 public TimeSelectionModel getTimeSelectionModel() { |
|
819 return selModel; |
|
820 } |
|
821 |
|
822 /** |
|
823 * Gets whether to use instructional tooltips over the interactive portions |
|
824 * of the clock. |
|
825 */ |
|
826 public boolean getUseToolTips() { |
|
827 return useToolTips; |
|
828 } |
|
829 |
|
830 protected float hourToRadians() { |
|
831 Calendar cal = model.getCalendar(); |
|
832 int hour = cal.get(Calendar.HOUR); |
|
833 int minute = cal.get(Calendar.MINUTE); |
|
834 int second = cal.get(Calendar.SECOND); |
|
835 return hourToRadians(hour, minute, second); |
|
836 } |
|
837 |
|
838 /** |
|
839 * Gets whether this {@code AnalogClock}'s time can be set by dragging the |
|
840 * hands. |
|
841 */ |
|
842 public boolean isInteractive() { |
|
843 return interactive; |
|
844 } |
|
845 |
|
846 protected float minToRadians() { |
|
847 Calendar cal = model.getCalendar(); |
|
848 int minute = cal.get(Calendar.MINUTE); |
|
849 int second = cal.get(Calendar.SECOND); |
|
850 return minToRadians(minute, second); |
|
851 } |
|
852 |
|
853 protected float secToRadians() { |
|
854 Calendar cal = model.getCalendar(); |
|
855 int second = cal.get(Calendar.SECOND); |
|
856 return secToRadians(second); |
|
857 } |
|
858 |
|
859 /** |
|
860 * Sets the color at the AM end of the AM-PM gradient. |
|
861 */ |
|
862 public void setAmColor(Color amColor) { |
|
863 this.amColor = amColor; |
|
864 repaint(); |
|
865 } |
|
866 |
|
867 /** |
|
868 * Sets the color of the center dial of the clock. |
|
869 */ |
|
870 public void setCenterColor(Color centerColor) { |
|
871 this.centerColor = centerColor; |
|
872 repaint(); |
|
873 } |
|
874 |
|
875 /** |
|
876 * Sets the preferred widht and height of this {@code AnalogClock} to {@code |
|
877 * diameter}. |
|
878 */ |
|
879 public void setPreferredDiameter(int diameter) { |
|
880 setPreferredSize(new Dimension(diameter, diameter)); |
|
881 } |
|
882 |
|
883 /** |
|
884 * Sets the color of the face of the clock. |
|
885 */ |
|
886 public void setFaceColor(Color faceColor) { |
|
887 this.faceColor = faceColor; |
|
888 baseImage = null; |
|
889 repaint(); |
|
890 } |
|
891 |
|
892 /** |
|
893 * Sets the color of the outer frame of the clock. |
|
894 */ |
|
895 public void setFrameColor(Color frameColor) { |
|
896 this.frameColor = frameColor; |
|
897 baseImage = null; |
|
898 repaint(); |
|
899 } |
|
900 |
|
901 /** |
|
902 * Sets the color to use when highlighting a hand. |
|
903 */ |
|
904 public void setHighlightColor(Color highlightColor) { |
|
905 this.highlightColor = highlightColor; |
|
906 } |
|
907 |
|
908 public void setHourHandColor(Color hourHandColor) { |
|
909 this.hourHandColor = hourHandColor; |
|
910 repaint(); |
|
911 } |
|
912 |
|
913 /** |
|
914 * Sets the color of the hour mark on the clock face. |
|
915 */ |
|
916 public void setHourMarkColor(Color hourMarkColor) { |
|
917 this.hourMarkColor = hourMarkColor; |
|
918 baseImage = null; |
|
919 repaint(); |
|
920 } |
|
921 |
|
922 /** |
|
923 * Sets whether this {@code AnalogClock}'s time can be set by dragging the |
|
924 * hands. |
|
925 */ |
|
926 public void setInteractive(boolean interactive) { |
|
927 if (this.interactive != interactive) { |
|
928 this.interactive = interactive; |
|
929 if (interactive) { |
|
930 addMouseListener(this); |
|
931 addMouseMotionListener(this); |
|
932 } else { |
|
933 removeMouseListener(this); |
|
934 removeMouseMotionListener(this); |
|
935 mousePoint = null; |
|
936 } |
|
937 } |
|
938 } |
|
939 |
|
940 /** |
|
941 * Sets the angle of the light, in radians, with 0 at three o'clock, shining |
|
942 * onto the clock. |
|
943 */ |
|
944 public void setLightAngle(float lightAngle) { |
|
945 this.lightAngle = lightAngle; |
|
946 baseImage = null; |
|
947 timeImage = null; |
|
948 centerImage = null; |
|
949 repaint(); |
|
950 } |
|
951 |
|
952 public void setMinuteHandColor(Color minuteHandColor) { |
|
953 this.minuteHandColor = minuteHandColor; |
|
954 repaint(); |
|
955 } |
|
956 |
|
957 /** |
|
958 * Sets the color of the minute mark on the clock face. |
|
959 */ |
|
960 public void setMinuteMarkColor(Color minuteMarkColor) { |
|
961 this.minuteMarkColor = minuteMarkColor; |
|
962 baseImage = null; |
|
963 repaint(); |
|
964 } |
|
965 |
|
966 /** |
|
967 * Sets the color at the PM end of the AM-PM gradient. |
|
968 */ |
|
969 public void setPmColor(Color pmColor) { |
|
970 this.pmColor = pmColor; |
|
971 repaint(); |
|
972 } |
|
973 |
|
974 public void setSecondHandColor(Color secondHandColor) { |
|
975 this.secondHandColor = secondHandColor; |
|
976 repaint(); |
|
977 } |
|
978 |
|
979 public void setTimeModel(TimeModel model) { |
|
980 if (this.model != model) { |
|
981 if (this.model != null) { |
|
982 this.model.removePropertyChangeListener(timeListener); |
|
983 } |
|
984 |
|
985 model.addPropertyChangeListener(TimeModel.PROPERTY_TIME, |
|
986 timeListener); |
|
987 |
|
988 this.model = model; |
|
989 timeChanged(); |
|
990 } |
|
991 } |
|
992 |
|
993 public void setTimeSelectionModel(TimeSelectionModel selModel) { |
|
994 this.selModel = selModel; |
|
995 } |
|
996 |
|
997 /** |
|
998 * Sets whether to use instructional tooltips over the interactive portions |
|
999 * of the clock. |
|
1000 */ |
|
1001 public void setUseToolTips(boolean useToolTips) { |
|
1002 if (this.useToolTips != useToolTips) { |
|
1003 this.useToolTips = useToolTips; |
|
1004 setToolTipText(); |
|
1005 } |
|
1006 } |
|
1007 |
|
1008 // |
|
1009 // Private methods |
|
1010 // |
|
1011 |
|
1012 private void createBaseImage(int diameter) { |
|
1013 if (baseImage == null || baseImage.getWidth() != diameter) { |
|
1014 baseImage = new BufferedImage(diameter, diameter, |
|
1015 BufferedImage.TYPE_INT_ARGB); |
|
1016 |
|
1017 Graphics2D g = baseImage.createGraphics(); |
|
1018 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, |
|
1019 RenderingHints.VALUE_ANTIALIAS_ON); |
|
1020 |
|
1021 frameWidth = drawFrame(g, diameter); |
|
1022 diameter -= 2 * frameWidth; |
|
1023 |
|
1024 g.translate(frameWidth, frameWidth); |
|
1025 |
|
1026 drawFace(g, diameter); |
|
1027 drawHourMarks(g, diameter); |
|
1028 drawMinuteMarks(g, diameter); |
|
1029 drawShadow(g, diameter); |
|
1030 |
|
1031 g.translate(-frameWidth, -frameWidth); |
|
1032 } |
|
1033 } |
|
1034 |
|
1035 private void createCenterImage(int diameter) { |
|
1036 if (centerImage == null || centerImage.getWidth() != diameter) { |
|
1037 centerImage = new BufferedImage(diameter, diameter, |
|
1038 BufferedImage.TYPE_INT_ARGB); |
|
1039 |
|
1040 Graphics2D g = centerImage.createGraphics(); |
|
1041 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, |
|
1042 RenderingHints.VALUE_ANTIALIAS_ON); |
|
1043 |
|
1044 drawCenterDot(g, diameter); |
|
1045 } |
|
1046 } |
|
1047 |
|
1048 private void createTimeImage(int diameter) { |
|
1049 if (timeImage == null || timeImage.getWidth() != diameter) { |
|
1050 timeImage = new BufferedImage(diameter, diameter, |
|
1051 BufferedImage.TYPE_INT_ARGB); |
|
1052 |
|
1053 Graphics2D g = timeImage.createGraphics(); |
|
1054 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, |
|
1055 RenderingHints.VALUE_ANTIALIAS_ON); |
|
1056 |
|
1057 amPm = drawAmPm(g, diameter); |
|
1058 hourPoint = drawHourHand(g, diameter); |
|
1059 minPoint = drawMinuteHand(g, diameter); |
|
1060 secPoint = drawSecondHand(g, diameter); |
|
1061 } |
|
1062 } |
|
1063 |
|
1064 private void setSelectedField(int selectedField) { |
|
1065 setCursor(selectedField == -1 ? null : |
|
1066 Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
|
1067 selModel.setSelectedField(selectedField); |
|
1068 |
|
1069 setToolTipText(); |
|
1070 } |
|
1071 |
|
1072 private void setToolTipText() { |
|
1073 String resource = null; |
|
1074 |
|
1075 if (useToolTips) { |
|
1076 switch (selModel.getSelectedField()) { |
|
1077 case Calendar.AM_PM: |
|
1078 resource = "time.tooltip.ampm"; |
|
1079 break; |
|
1080 |
|
1081 case Calendar.HOUR: |
|
1082 resource = "time.tooltip.hour"; |
|
1083 break; |
|
1084 |
|
1085 case Calendar.MINUTE: |
|
1086 resource = "time.tooltip.minute"; |
|
1087 break; |
|
1088 |
|
1089 case Calendar.SECOND: |
|
1090 resource = "time.tooltip.second"; |
|
1091 break; |
|
1092 } |
|
1093 } |
|
1094 |
|
1095 setToolTipText(resource == null ? null : Finder.getString(resource)); |
|
1096 } |
|
1097 |
|
1098 private void timeChanged() { |
|
1099 timeImage = null; |
|
1100 repaint(); |
|
1101 } |
|
1102 |
|
1103 // |
|
1104 // Static methods |
|
1105 // |
|
1106 |
|
1107 public static float amPmToRadians(int amPm, int hour, int minute, |
|
1108 int second) { |
|
1109 |
|
1110 hour = (hour % 12) + (amPm == Calendar.PM ? 12 : 0); |
|
1111 return (6 - (hour + minute / 60f + second / 3600f)) * (float)Math.PI / |
|
1112 12f; |
|
1113 } |
|
1114 |
|
1115 public static float hourToRadians(int hour, int minute, int second) { |
|
1116 return (15 - (hour + minute / 60f + second / 3600f)) * (float)Math.PI / |
|
1117 6f; |
|
1118 } |
|
1119 |
|
1120 public static float minToRadians(int minute, int second) { |
|
1121 return (75 - (minute + second / 60f)) * (float)Math.PI / 30f; |
|
1122 } |
|
1123 |
|
1124 public static float radiansToHour(float radians) { |
|
1125 float value = (-6 * radians / (float)Math.PI) + 3; |
|
1126 if (value < 1) { |
|
1127 value += 12; |
|
1128 } |
|
1129 return value; |
|
1130 } |
|
1131 |
|
1132 public static float radiansToMin(float radians) { |
|
1133 float value = (-30 * radians / (float)Math.PI) + 15; |
|
1134 if (value < 0) { |
|
1135 value += 60; |
|
1136 } |
|
1137 return value; |
|
1138 } |
|
1139 |
|
1140 public static float secToRadians(int second) { |
|
1141 return (75 - second) * (float)Math.PI / 30f; |
|
1142 } |
|
1143 |
|
1144 /** |
|
1145 * Converts {@code Calendar} time fields to milliseconds. |
|
1146 * |
|
1147 * @param calendarField |
|
1148 * {@code Calendar.MILLISECOND}, |
|
1149 * {@code Calendar.SECOND}, |
|
1150 * {@code Calendar.MINUTE}, |
|
1151 * {@code Calendar.HOUR}, |
|
1152 * {@code Calendar.HOUR_OF_DAY}, or |
|
1153 * {@code Calendar.AM_PM}, |
|
1154 * |
|
1155 * @return the number of milliseconds in a unit of the given field, or |
|
1156 * zero if {@code calendarField} is not any of the allowed |
|
1157 * fields |
|
1158 */ |
|
1159 public static long timeFieldToMillis(int calendarField) { |
|
1160 long millis = 0; |
|
1161 switch (calendarField) { |
|
1162 case Calendar.MILLISECOND: |
|
1163 millis = 1; |
|
1164 break; |
|
1165 |
|
1166 case Calendar.SECOND: |
|
1167 millis = 1000; |
|
1168 break; |
|
1169 |
|
1170 case Calendar.MINUTE: |
|
1171 millis = 60000; |
|
1172 break; |
|
1173 |
|
1174 case Calendar.HOUR: |
|
1175 case Calendar.HOUR_OF_DAY: |
|
1176 millis = 3600000; |
|
1177 break; |
|
1178 |
|
1179 case Calendar.AM_PM: |
|
1180 millis = 43200000; |
|
1181 break; |
|
1182 } |
|
1183 |
|
1184 return millis; |
|
1185 } |
|
1186 |
|
1187 public static void main(String args[]) { |
|
1188 AnalogClock clock = new AnalogClock(); |
|
1189 clock.setInteractive(true); |
|
1190 clock.setPreferredDiameter(300); |
|
1191 |
|
1192 TimeModelUpdater updater = new TimeModelUpdater(clock.getTimeModel()); |
|
1193 updater.start(); |
|
1194 |
|
1195 JFrame frame = new JFrame(); |
|
1196 Container c = frame.getContentPane(); |
|
1197 c.setLayout(new BorderLayout()); |
|
1198 c.add(clock, BorderLayout.CENTER); |
|
1199 frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); |
|
1200 frame.pack(); |
|
1201 frame.setVisible(true); |
|
1202 } |
|
1203 } |
|