1
+ package org .collinm .tyme
2
+
3
+ import scala .swing .SwingApplication
4
+ import scala .swing .MainFrame
5
+ import java .awt .Color
6
+ import scala .swing .BoxPanel
7
+ import scala .swing .Orientation
8
+ import java .awt .Graphics2D
9
+ import java .awt .geom .Ellipse2D
10
+ import java .awt .BasicStroke
11
+ import scala .swing .event .Event
12
+ import scala .swing .Publisher
13
+ import java .awt .Dimension
14
+ import java .awt .RenderingHints
15
+ import java .awt .geom .Line2D
16
+ import org .collinm .tyme .utils .Geometry
17
+ import org .collinm .tyme .utils .Time
18
+
19
+ /** Main runtime */
20
+ object Clock extends SwingApplication {
21
+
22
+ def startup (args : Array [String ]) = {
23
+ val frame = new ClockFrame ()
24
+ frame.visible = true
25
+ // frame.peer.setUndecorated(true) // Remove title bar
26
+ }
27
+ }
28
+
29
+ /** The main window frame that houses the clock.
30
+ *
31
+ * Creates and owns the clock and it's refresh timer. Automatically maximizes
32
+ * itself on creation and instantiates a clock that fits nicely inside the frame
33
+ * (centered).
34
+ */
35
+ class ClockFrame extends MainFrame {
36
+ this .maximize()
37
+ val clock = new ElegantClockPanel (this .toolkit.getScreenSize())
38
+ val timer = new ClockTimer (30 )
39
+
40
+ listenTo(timer)
41
+ reactions += {
42
+ case rt : RedrawTime => clock.repaint
43
+ }
44
+
45
+ contents = clock
46
+ timer.start()
47
+ }
48
+
49
+ /** The Panel component that draws the clock.
50
+ *
51
+ * @param dimension
52
+ * The area the clock should fit in (with a border)
53
+ */
54
+ class ElegantClockPanel (dimension : Dimension ) extends BoxPanel (Orientation .Horizontal ) {
55
+ var staticItemsPainted = false
56
+ var lastStaticPaintSec = 0.0
57
+ // Minimum dimension will always be height after the window is maximized
58
+ // Figure out the basic measurements of the clock: diameter and border size for X and Y axis
59
+ val yOffset = dimension.getHeight() * 0.1
60
+ val diameter = dimension.getHeight() * 0.8
61
+ val xOffset = (dimension.getWidth() - diameter) / 2
62
+
63
+ // Create the edge of the clock and the hour and minute ticks
64
+ val outline = new Ellipse2D .Double (xOffset, yOffset, diameter, diameter)
65
+ val origin = (xOffset + diameter/ 2 , yOffset + diameter/ 2 )
66
+ def getCircleTick (percent : Double , origin : (Double , Double ), distFromOrigin : Double , diameter : Double ) = {
67
+ val (x, y) = Geometry .getPointOnCircle(percent, origin, distFromOrigin)
68
+ new Ellipse2D .Double (x - (diameter/ 2 ), y - (diameter/ 2 ), diameter, diameter)
69
+ }
70
+ val majorTickWidth = 10
71
+ val minorTickWidth = 2
72
+ val majorTicks = Range (1 , 13 ).map(hr => getCircleTick(hr/ 12.0 , origin, (diameter/ 2 )* 0.88 , majorTickWidth))
73
+ val minorTicks = Range (1 , 61 ).map(min => getCircleTick(min/ 60.0 , origin, (diameter/ 2 )* 0.88 , minorTickWidth))
74
+
75
+ // Instantiate the Stokes used to dra the edge and hands
76
+ background = Color .black
77
+ val outlineStroke = new BasicStroke (10 )
78
+ val minHrStroke = new BasicStroke (5 , BasicStroke .CAP_ROUND , BasicStroke .JOIN_ROUND )
79
+ val secStroke = new BasicStroke (2 , BasicStroke .CAP_ROUND , BasicStroke .JOIN_ROUND )
80
+
81
+ override def paintComponent (g : Graphics2D ): Unit = {
82
+ // Rendering hints for anti-aliasing
83
+ g.setRenderingHint(RenderingHints .KEY_ANTIALIASING , RenderingHints .VALUE_ANTIALIAS_ON )
84
+ g.setRenderingHint(RenderingHints .KEY_INTERPOLATION ,RenderingHints .VALUE_INTERPOLATION_BICUBIC )
85
+ super .paintComponent(g)
86
+
87
+ g.setColor(Color .LIGHT_GRAY )
88
+ if (! staticItemsPainted) {
89
+ // Draw edge of clock
90
+ g.setStroke(outlineStroke)
91
+ g.draw(outline)
92
+
93
+ // Draw ticks
94
+ majorTicks.map(tick => g.fill(tick))
95
+ minorTicks.map(tick => g.fill(tick))
96
+
97
+ staticItemsPainted = true
98
+ }
99
+
100
+ // Draw hands
101
+ val (hours, minutes, seconds) = Time .getTime()
102
+ g.setStroke(minHrStroke)
103
+ g.draw( getHand(hours/ 12 , origin, (diameter/ 2 )* 0.4 ) )
104
+ g.draw( getHand(minutes/ 60 , origin, (diameter/ 2 )* 0.6472 ) )
105
+ g.setStroke(secStroke)
106
+ g.draw( getHand(seconds/ 60 , origin, (diameter/ 2 )* 0.8 ) )
107
+
108
+ if (minutes > lastStaticPaintSec+ 5 ) staticItemsPainted = false
109
+ }
110
+
111
+ /** Get a clock hand (line).
112
+ *
113
+ * @param percent
114
+ * decimal representing where on the edge of the clock the hand should be pointing
115
+ * 0 = 12, 0.25 = 3, 0.5 = 6, 0.75 = 9
116
+ * @param origin
117
+ * 2-tuple for the center of the clock
118
+ * @param length
119
+ * how long the hand should be
120
+ * @return A Line2D for the clock hand
121
+ */
122
+ def getHand (percent : Double , origin : (Double , Double ), length : Double ) = {
123
+ val point = Geometry .getPointOnCircle(percent, origin, length)
124
+ new Line2D .Double (origin._1, origin._2, point._1, point._2)
125
+ }
126
+ }
127
+
128
+ /** Update timer for a clock
129
+ *
130
+ * Fires a RedrawTime Event every n milliseconds, where n results in
131
+ * the desired refresh rate.
132
+ *
133
+ * @param fps
134
+ * target frames per second (refresh rate)
135
+ */
136
+ class ClockTimer (fps : Int ) extends Publisher {
137
+ val delay = 1000 / fps
138
+ // Custom action to that fires our desired event, so we can use a Swing Timer
139
+ val timeout = new javax.swing.AbstractAction () { def actionPerformed (e : java.awt.event.ActionEvent ) = publish(RedrawTime ()) }
140
+ val timer = new javax.swing.Timer (delay, timeout) // Fire "timeout" ever "delay" milliseconds
141
+ timer.setRepeats(true )
142
+
143
+ def start () = timer.start()
144
+ def stop () = timer.stop()
145
+ }
146
+
147
+ case class RedrawTime () extends Event
0 commit comments