diff options
Diffstat (limited to 'apps/plugins/stopwatch.lua')
-rw-r--r-- | apps/plugins/stopwatch.lua | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/apps/plugins/stopwatch.lua b/apps/plugins/stopwatch.lua new file mode 100644 index 0000000000..c8fac3c000 --- /dev/null +++ b/apps/plugins/stopwatch.lua | |||
@@ -0,0 +1,341 @@ | |||
1 | --[[ | ||
2 | __________ __ ___. | ||
3 | Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
4 | Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
5 | Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
6 | Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
7 | \/ \/ \/ \/ \/ | ||
8 | $Id$ | ||
9 | |||
10 | Port of Stopwatch to Lua for touchscreen targets. | ||
11 | Original copyright: Copyright (C) 2004 Mike Holden | ||
12 | |||
13 | Copyright (C) 2009 by Maurus Cuelenaere | ||
14 | |||
15 | This program is free software; you can redistribute it and/or | ||
16 | modify it under the terms of the GNU General Public License | ||
17 | as published by the Free Software Foundation; either version 2 | ||
18 | of the License, or (at your option) any later version. | ||
19 | |||
20 | This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
21 | KIND, either express or implied. | ||
22 | |||
23 | ]]-- | ||
24 | |||
25 | require "actions" | ||
26 | require "buttons" | ||
27 | |||
28 | STOPWATCH_FILE = "/.rockbox/rocks/apps/stopwatch.dat" | ||
29 | |||
30 | |||
31 | local LapsView = { | ||
32 | lapTimes = {}, | ||
33 | timer = { | ||
34 | counting = false, | ||
35 | prevTotal = 0, | ||
36 | startAt = 0, | ||
37 | current = 0 | ||
38 | }, | ||
39 | vp = { | ||
40 | x = 80, | ||
41 | y = 0, | ||
42 | width = rb.LCD_WIDTH - 80, | ||
43 | height = rb.LCD_HEIGHT, | ||
44 | font = rb.FONT_UI, | ||
45 | fg_pattern = rb.lcd_get_foreground() | ||
46 | }, | ||
47 | scroll = { | ||
48 | prevY = 0, | ||
49 | cursorPos = 0 | ||
50 | } | ||
51 | } | ||
52 | |||
53 | function LapsView:init() | ||
54 | local _, _, h = rb.font_getstringsize("", self.vp.font) | ||
55 | |||
56 | self.vp.maxLaps = self.vp.height / h | ||
57 | self.vp.lapHeight = h | ||
58 | |||
59 | self:loadState() | ||
60 | end | ||
61 | |||
62 | function LapsView:display() | ||
63 | rb.set_viewport(self.vp) | ||
64 | rb.clear_viewport() | ||
65 | |||
66 | local nrOfLaps = math.min(self.vp.maxLaps, #self.lapTimes) | ||
67 | rb.lcd_puts_scroll(0, 0, ticksToString(self.timer.current)) | ||
68 | |||
69 | for i=1, nrOfLaps do | ||
70 | local idx = #self.lapTimes - self.scroll.cursorPos - i + 1 | ||
71 | if self.lapTimes[idx] ~= nil then | ||
72 | rb.lcd_puts_scroll(0, i, ticksToString(self.lapTimes, idx)) | ||
73 | end | ||
74 | end | ||
75 | |||
76 | rb.set_viewport(nil) | ||
77 | end | ||
78 | |||
79 | function LapsView:checkForScroll(btn, x, y) | ||
80 | if x > self.vp.x and x < self.vp.x + self.vp.width and | ||
81 | y > self.vp.y and y < self.vp.y + self.vp.height then | ||
82 | |||
83 | if bit.band(btn, rb.buttons.BUTTON_REL) == rb.buttons.BUTTON_REL then | ||
84 | self.scroll.prevY = 0 | ||
85 | else | ||
86 | if #self.lapTimes > self.vp.maxLaps and self.scroll.prevY ~= 0 then | ||
87 | self.scroll.cursorPos = self.scroll.cursorPos - | ||
88 | (y - self.scroll.prevY) / self.vp.lapHeight | ||
89 | |||
90 | local maxLaps = math.min(self.vp.maxLaps, #self.lapTimes) | ||
91 | if self.scroll.cursorPos < 0 then | ||
92 | self.scroll.cursorPos = 0 | ||
93 | elseif self.scroll.cursorPos >= maxLaps then | ||
94 | self.scroll.cursorPos = maxLaps | ||
95 | end | ||
96 | end | ||
97 | |||
98 | self.scroll.prevY = y | ||
99 | end | ||
100 | |||
101 | return true | ||
102 | else | ||
103 | return false | ||
104 | end | ||
105 | end | ||
106 | |||
107 | function LapsView:incTimer() | ||
108 | if self.timer.counting then | ||
109 | self.timer.current = self.timer.prevTotal + rb.current_tick() | ||
110 | - self.timer.startAt | ||
111 | else | ||
112 | self.timer.current = self.timer.prevTotal | ||
113 | end | ||
114 | end | ||
115 | |||
116 | function LapsView:startTimer() | ||
117 | self.timer.startAt = rb.current_tick() | ||
118 | self.timer.currentLap = self.timer.prevTotal | ||
119 | self.timer.counting = true | ||
120 | end | ||
121 | |||
122 | function LapsView:stopTimer() | ||
123 | self.timer.prevTotal = self.timer.prevTotal + rb.current_tick() | ||
124 | - self.timer.startAt | ||
125 | self.timer.counting = false | ||
126 | end | ||
127 | |||
128 | function LapsView:newLap() | ||
129 | table.insert(self.lapTimes, self.timer.current) | ||
130 | end | ||
131 | |||
132 | function LapsView:resetTimer() | ||
133 | self.lapTimes = {} | ||
134 | self.timer.counting = false | ||
135 | self.timer.current, self.timer.prevTotal, self.timer.startAt = 0, 0, 0 | ||
136 | self.scroll.cursorPos = 0 | ||
137 | end | ||
138 | |||
139 | function LapsView:saveState() | ||
140 | local fd = assert(io.open(STOPWATCH_FILE, "w")) | ||
141 | |||
142 | for _, v in ipairs({"current", "startAt", "prevTotal", "counting"}) do | ||
143 | assert(fd:write(tostring(self.timer[v]) .. "\n")) | ||
144 | end | ||
145 | for _, v in ipairs(self.lapTimes) do | ||
146 | assert(fd:write(tostring(v) .. "\n")) | ||
147 | end | ||
148 | |||
149 | fd:close() | ||
150 | end | ||
151 | |||
152 | function LapsView:loadState() | ||
153 | local fd = io.open(STOPWATCH_FILE, "r") | ||
154 | if fd == nil then return end | ||
155 | |||
156 | for _, v in ipairs({"current", "startAt", "prevTotal"}) do | ||
157 | self.timer[v] = tonumber(fd:read("*line")) | ||
158 | end | ||
159 | self.timer.counting = toboolean(fd:read("*line")) | ||
160 | |||
161 | local line = fd:read("*line") | ||
162 | while line do | ||
163 | table.insert(self.lapTimes, tonumber(line)) | ||
164 | line = fd:read("*line") | ||
165 | end | ||
166 | |||
167 | fd:close() | ||
168 | end | ||
169 | |||
170 | local Button = { | ||
171 | x = 0, | ||
172 | y = 0, | ||
173 | width = 80, | ||
174 | height = 50, | ||
175 | label = "" | ||
176 | } | ||
177 | |||
178 | function Button:new(o) | ||
179 | local o = o or {} | ||
180 | |||
181 | if o.label then | ||
182 | local _, w, h = rb.font_getstringsize(o.label, LapsView.vp.font) | ||
183 | o.width = 5 * w / 4 | ||
184 | o.height = 3 * h / 2 | ||
185 | end | ||
186 | |||
187 | setmetatable(o, self) | ||
188 | self.__index = self | ||
189 | return o | ||
190 | end | ||
191 | |||
192 | function Button:draw() | ||
193 | local _, w, h = rb.font_getstringsize(self.label, LapsView.vp.font) | ||
194 | local x, y = (2 * self.x + self.width - w) / 2, (2 * self.y + self.height - h) / 2 | ||
195 | |||
196 | rb.lcd_drawrect(self.x, self.y, self.width, self.height) | ||
197 | rb.lcd_putsxy(x, y, self.label) | ||
198 | end | ||
199 | |||
200 | function Button:isPressed(x, y) | ||
201 | return x > self.x and x < self.x + self.width and | ||
202 | y > self.y and y < self.y + self.height | ||
203 | end | ||
204 | |||
205 | -- Helper function | ||
206 | function ticksToString(laps, lap) | ||
207 | local ticks = type(laps) == "table" and laps[lap] or laps | ||
208 | lap = lap or 0 | ||
209 | |||
210 | local hours = ticks / (rb.HZ * 3600) | ||
211 | ticks = ticks - (rb.HZ * hours * 3600) | ||
212 | local minutes = ticks / (rb.HZ * 60) | ||
213 | ticks = ticks - (rb.HZ * minutes * 60) | ||
214 | local seconds = ticks / rb.HZ | ||
215 | ticks = ticks - (rb.HZ * seconds) | ||
216 | local cs = ticks | ||
217 | |||
218 | if (lap == 0) then | ||
219 | return string.format("%2d:%02d:%02d.%02d", hours, minutes, seconds, cs) | ||
220 | else | ||
221 | if (lap > 1) then | ||
222 | local last_ticks = laps[lap] - laps[lap-1] | ||
223 | local last_hours = last_ticks / (rb.HZ * 3600) | ||
224 | last_ticks = last_ticks - (rb.HZ * last_hours * 3600) | ||
225 | local last_minutes = last_ticks / (rb.HZ * 60) | ||
226 | last_ticks = last_ticks - (rb.HZ * last_minutes * 60) | ||
227 | local last_seconds = last_ticks / rb.HZ | ||
228 | last_ticks = last_ticks - (rb.HZ * last_seconds) | ||
229 | local last_cs = last_ticks | ||
230 | |||
231 | return string.format("%2d %2d:%02d:%02d.%02d [%2d:%02d:%02d.%02d]", | ||
232 | lap, hours, minutes, seconds, cs, last_hours, | ||
233 | last_minutes, last_seconds, last_cs) | ||
234 | else | ||
235 | return string.format("%2d %2d:%02d:%02d.%02d", lap, hours, minutes, seconds, cs) | ||
236 | end | ||
237 | end | ||
238 | end | ||
239 | |||
240 | -- Helper function | ||
241 | function toboolean(v) | ||
242 | return v == "true" | ||
243 | end | ||
244 | |||
245 | function arrangeButtons(btns) | ||
246 | local totalWidth, totalHeight, maxWidth, maxHeight, vp = 0, 0, 0, 0 | ||
247 | for _, btn in pairs(btns) do | ||
248 | totalWidth = totalWidth + btn.width | ||
249 | totalHeight = totalHeight + btn.height | ||
250 | maxHeight = math.max(maxHeight, btn.height) | ||
251 | maxWidth = math.max(maxWidth, btn.width) | ||
252 | end | ||
253 | |||
254 | if totalWidth <= rb.LCD_WIDTH then | ||
255 | local temp = 0 | ||
256 | for _, btn in pairs(btns) do | ||
257 | btn.y = rb.LCD_HEIGHT - maxHeight | ||
258 | btn.x = temp | ||
259 | |||
260 | temp = temp + btn.width | ||
261 | end | ||
262 | |||
263 | vp = { | ||
264 | x = 0, | ||
265 | y = 0, | ||
266 | width = rb.LCD_WIDTH, | ||
267 | height = rb.LCD_HEIGHT - maxHeight | ||
268 | } | ||
269 | elseif totalHeight <= rb.LCD_HEIGHT then | ||
270 | local temp = 0 | ||
271 | for _, btn in pairs(btns) do | ||
272 | btn.x = rb.LCD_WIDTH - maxWidth | ||
273 | btn.y = temp | ||
274 | |||
275 | temp = temp + btn.height | ||
276 | end | ||
277 | |||
278 | vp = { | ||
279 | x = 0, | ||
280 | y = 0, | ||
281 | width = rb.LCD_WIDTH - maxWidth, | ||
282 | height = rb.LCD_HEIGHT | ||
283 | } | ||
284 | else | ||
285 | error("Can't arrange the buttons according to your screen's resolution!") | ||
286 | end | ||
287 | |||
288 | for k, v in pairs(vp) do | ||
289 | LapsView.vp[k] = v | ||
290 | end | ||
291 | end | ||
292 | |||
293 | rb.touchscreen_set_mode(rb.TOUCHSCREEN_POINT) | ||
294 | |||
295 | LapsView:init() | ||
296 | |||
297 | local btns = { | ||
298 | Button:new({name = "startTimer", label = "Start"}), | ||
299 | Button:new({name = "stopTimer", label = "Stop"}), | ||
300 | Button:new({name = "newLap", label = "New Lap"}), | ||
301 | Button:new({name = "resetTimer", label = "Reset"}), | ||
302 | Button:new({name = "exitApp", label = "Quit"}) | ||
303 | } | ||
304 | |||
305 | arrangeButtons(btns) | ||
306 | |||
307 | for _, btn in pairs(btns) do | ||
308 | btn:draw() | ||
309 | end | ||
310 | |||
311 | repeat | ||
312 | LapsView:incTimer() | ||
313 | |||
314 | local action = rb.get_action(rb.contexts.CONTEXT_STD, 0) | ||
315 | |||
316 | if (action == rb.actions.ACTION_TOUCHSCREEN) then | ||
317 | local btn, x, y = rb.action_get_touchscreen_press() | ||
318 | |||
319 | if LapsView:checkForScroll(btn, x, y) then | ||
320 | -- Don't do anything | ||
321 | elseif btn == rb.buttons.BUTTON_REL then | ||
322 | for _, btn in pairs(btns) do | ||
323 | local name = btn.name | ||
324 | if (btn:isPressed(x, y)) then | ||
325 | if name == "exitApp" then | ||
326 | action = rb.actions.ACTION_STD_CANCEL | ||
327 | else | ||
328 | LapsView[name](LapsView) | ||
329 | end | ||
330 | end | ||
331 | end | ||
332 | end | ||
333 | end | ||
334 | |||
335 | LapsView:display() | ||
336 | rb.lcd_update() | ||
337 | rb.sleep(rb.HZ/50) | ||
338 | until action == rb.actions.ACTION_STD_CANCEL | ||
339 | |||
340 | LapsView:saveState() | ||
341 | |||