--[[ __________ __ ___. 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_set_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()