rockbox/apps/plugins/stopwatch.lua

334 lines
9.2 KiB
Lua

--[[
__________ __ ___.
Open \______ \ ____ ____ | | _\_ |__ _______ ___
Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
\/ \/ \/ \/ \/
$Id$
Port of Stopwatch to Lua for touchscreen targets.
Original copyright: Copyright (C) 2004 Mike Holden
Copyright (C) 2009 by Maurus Cuelenaere
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
KIND, either express or implied.
]]--
require "actions"
require "buttons"
STOPWATCH_FILE = rb.PLUGIN_APPS_DATA_DIR .. "/stopwatch.dat"
local LapsView = {
lapTimes = {},
timer = {
counting = false,
prevTotal = 0,
startAt = 0,
current = 0
},
vp = {
x = 80,
y = 0,
width = rb.LCD_WIDTH - 80,
height = rb.LCD_HEIGHT,
font = rb.FONT_UI,
fg_pattern = rb.lcd_get_foreground()
},
scroll = {
prevY = 0,
cursorPos = 0
}
}
function LapsView:init()
local _, _, h = rb.font_getstringsize("", self.vp.font)
self.vp.maxLaps = self.vp.height / h
self.vp.lapHeight = h
self:loadState()
end
function LapsView:display()
rb.set_viewport(self.vp)
rb.clear_viewport()
local nrOfLaps = math.min(self.vp.maxLaps, #self.lapTimes)
rb.lcd_puts_scroll(0, 0, ticksToString(self.timer.current))
for i=1, nrOfLaps do
local idx = #self.lapTimes - self.scroll.cursorPos - i + 1
if self.lapTimes[idx] ~= nil then
rb.lcd_puts_scroll(0, i, ticksToString(self.lapTimes, idx))
end
end
rb.set_viewport(nil)
end
function LapsView:checkForScroll(btn, x, y)
if x > self.vp.x and x < self.vp.x + self.vp.width and
y > self.vp.y and y < self.vp.y + self.vp.height then
if bit.band(btn, rb.buttons.BUTTON_REL) == rb.buttons.BUTTON_REL then
self.scroll.prevY = 0
else
if #self.lapTimes > self.vp.maxLaps and self.scroll.prevY ~= 0 then
self.scroll.cursorPos = self.scroll.cursorPos -
(y - self.scroll.prevY) / self.vp.lapHeight
local maxLaps = math.min(self.vp.maxLaps, #self.lapTimes)
if self.scroll.cursorPos < 0 then
self.scroll.cursorPos = 0
elseif self.scroll.cursorPos >= maxLaps then
self.scroll.cursorPos = maxLaps
end
end
self.scroll.prevY = y
end
return true
else
return false
end
end
function LapsView:incTimer()
if self.timer.counting then
self.timer.current = self.timer.prevTotal + rb.current_tick()
- self.timer.startAt
else
self.timer.current = self.timer.prevTotal
end
end
function LapsView:startTimer()
self.timer.startAt = rb.current_tick()
self.timer.currentLap = self.timer.prevTotal
self.timer.counting = true
end
function LapsView:stopTimer()
self.timer.prevTotal = self.timer.prevTotal + rb.current_tick()
- self.timer.startAt
self.timer.counting = false
end
function LapsView:newLap()
table.insert(self.lapTimes, self.timer.current)
end
function LapsView:resetTimer()
self.lapTimes = {}
self.timer.counting = false
self.timer.current, self.timer.prevTotal, self.timer.startAt = 0, 0, 0
self.scroll.cursorPos = 0
end
function LapsView:saveState()
local fd = assert(io.open(STOPWATCH_FILE, "w"))
for _, v in ipairs({"current", "startAt", "prevTotal", "counting"}) do
assert(fd:write(tostring(self.timer[v]) .. "\n"))
end
for _, v in ipairs(self.lapTimes) do
assert(fd:write(tostring(v) .. "\n"))
end
fd:close()
end
function LapsView:loadState()
local fd = io.open(STOPWATCH_FILE, "r")
if fd == nil then return end
for _, v in ipairs({"current", "startAt", "prevTotal"}) do
self.timer[v] = tonumber(fd:read("*line"))
end
self.timer.counting = toboolean(fd:read("*line"))
local line = fd:read("*line")
while line do
table.insert(self.lapTimes, tonumber(line))
line = fd:read("*line")
end
fd:close()
end
local Button = {
x = 0,
y = 0,
width = 80,
height = 50,
label = ""
}
function Button:new(o)
local o = o or {}
if o.label then
local _, w, h = rb.font_getstringsize(o.label, LapsView.vp.font)
o.width = math.max(5 * w / 4,o.width)
o.height = 3 * h / 2
end
setmetatable(o, self)
self.__index = self
return o
end
function Button:draw()
local _, w, h = rb.font_getstringsize(self.label, LapsView.vp.font)
local x, y = (2 * self.x + self.width - w) / 2, (2 * self.y + self.height - h) / 2
rb.lcd_drawrect(self.x, self.y, self.width, self.height)
rb.lcd_putsxy(x, y, self.label)
end
function Button:isPressed(x, y)
return x > self.x and x < self.x + self.width and
y > self.y and y < self.y + self.height
end
-- Helper function
function ticksToString(laps, lap)
local ticks = type(laps) == "table" and laps[lap] or laps
lap = lap or 0
local hours = ticks / (rb.HZ * 3600)
ticks = ticks - (rb.HZ * hours * 3600)
local minutes = ticks / (rb.HZ * 60)
ticks = ticks - (rb.HZ * minutes * 60)
local seconds = ticks / rb.HZ
ticks = ticks - (rb.HZ * seconds)
local cs = ticks
if (lap == 0) then
return string.format("%2d:%02d:%02d.%02d", hours, minutes, seconds, cs)
else
if (lap > 1) then
local last_ticks = laps[lap] - laps[lap-1]
local last_hours = last_ticks / (rb.HZ * 3600)
last_ticks = last_ticks - (rb.HZ * last_hours * 3600)
local last_minutes = last_ticks / (rb.HZ * 60)
last_ticks = last_ticks - (rb.HZ * last_minutes * 60)
local last_seconds = last_ticks / rb.HZ
last_ticks = last_ticks - (rb.HZ * last_seconds)
local last_cs = last_ticks
return string.format("%2d %2d:%02d:%02d.%02d [%2d:%02d:%02d.%02d]",
lap, hours, minutes, seconds, cs, last_hours,
last_minutes, last_seconds, last_cs)
else
return string.format("%2d %2d:%02d:%02d.%02d", lap, hours, minutes, seconds, cs)
end
end
end
-- Helper function
function toboolean(v)
return v == "true"
end
function arrangeButtons(btns)
local totalWidth, totalHeight, maxWidth, maxHeight, vp = 0, 0, 0, 0
local width, row = 0, 0
local items, num_rows
for i, btn in pairs(btns) do
maxHeight = math.max(maxHeight, btn.height)
totalWidth = totalWidth + btn.width
items = i
end
for _, btn in pairs(btns) do
btn.height = maxHeight
end
num_rows = totalWidth / rb.LCD_WIDTH
for _, btn in pairs(btns) do
btn.x = width
btn.y = rb.LCD_HEIGHT - ((num_rows - row) * maxHeight)
width = width + btn.width
if (width > rb.LCD_WIDTH - 5) then -- 5 is rounding margin
width = 0
row = row+1
end
end
vp = {
x = 0,
y = 0,
width = rb.LCD_WIDTH,
height = rb.LCD_HEIGHT - num_rows*maxHeight - 2
}
for k, v in pairs(vp) do
LapsView.vp[k] = v
end
end
rb.touchscreen_mode(rb.TOUCHSCREEN_POINT)
LapsView:init()
local third = rb.LCD_WIDTH/3
local btns = {
Button:new({name = "startTimer", label = "Start", width = third}),
Button:new({name = "stopTimer", label = "Stop", width = third}),
Button:new({name = "resetTimer", label = "Reset", width = rb.LCD_WIDTH-third*2}), -- correct rounding error
Button:new({name = "newLap", label = "New Lap", width = third*2}),
Button:new({name = "exitApp", label = "Quit", width = rb.LCD_WIDTH-third*2}) -- correct rounding error
}
arrangeButtons(btns)
for _, btn in pairs(btns) do
btn:draw()
end
repeat
LapsView:incTimer()
local action = rb.get_action(rb.contexts.CONTEXT_STD, 0)
if (action == rb.actions.ACTION_TOUCHSCREEN) then
local btn, x, y = rb.action_get_touchscreen_press()
if LapsView:checkForScroll(btn, x, y) then
-- Don't do anything
elseif btn == rb.buttons.BUTTON_REL then
for _, btn in pairs(btns) do
local name = btn.name
if (btn:isPressed(x, y)) then
if name == "exitApp" then
action = rb.actions.ACTION_STD_CANCEL
else
LapsView[name](LapsView)
end
end
end
end
end
LapsView:display()
rb.lcd_update()
rb.sleep(rb.HZ/50)
until action == rb.actions.ACTION_STD_CANCEL
LapsView:saveState()