timergauge.lua
--- Animated countdown timer, extends <a href="https://www.mudlet.org/geyser/files/geyser/GeyserGauge.html">Geyser.Gauge</a> -- @classmod TimerGauge -- @author Damian Monogue <demonnic@gmail.com> -- @copyright 2020 Damian Monogue -- @license MIT, see LICENSE.lua local TimerGauge = { name = "TimerGaugeClass", active = true, showTime = true, prefix = "", timeFormat = "S.t", suffix = "", updateTime = "10", autoHide = true, autoShow = true, manageContainer = false, } function TimerGauge:setStyleSheet(cssFront, cssBack, cssText) cssFront = cssFront or self.cssFront cssBack = cssBack or self.cssBack cssBack = cssBack or self.cssFront .. "background-color: black;" cssText = cssText or self.cssText if cssFront then self.front:setStyleSheet(cssFront) end if cssBack then self.back:setStyleSheet(cssBack) end if cssText then self.text:setStyleSheet(cssText) end -- self.gauge:setStyleSheet(cssFront, cssBack, cssText) self.cssFront = cssFront self.cssBack = cssBack self.cssText = cssText end --- Shows the TimerGauge. If the manageContainer property is true, then will add it back to its container function TimerGauge:show2() if self.manageContainer and self.savedContainer then self.savedContainer:add(self) self.savedContainer = nil end self:show() end --- Hides the TimerGauge. If manageContainer property is true, then it will remove it from its container and if the container is an HBox or VBox it will initiate size/position management function TimerGauge:hide2() if self.manageContainer and self.container.name ~= Geyser.name then self.savedContainer = self.container Geyser:add(self) self.savedContainer:remove(self) if self.savedContainer.type == "VBox" or self.savedContainer.type == "HBox" then if self.savedContainer.organize then self.savedContainer:organize() else self.savedContainer:reposition() end end end self:hide() end --- Starts the timergauge. Works whether the timer is stopped or not. Does not start a timer which is already at 0 -- @tparam[opt] boolean show override the autoShow property. True will always show, false will never show. -- @usage myTimerGauge:start() --starts the timer, will show or not based on autoShow property -- myTimerGauge:start(false) --starts the timer, will not change hidden status, regardless of autoShow property -- myTimerGauge:start(true) --starts the timer, will show it regardless of autoShow property function TimerGauge:start(show) if show == nil then show = self.autoShow end self.active = true if self.timer then killTimer(self.timer) self.timer = nil end startStopWatch(self.stopWatchName) self:update() self.timer = tempTimer(self.updateTime / 1000, function() self:update() end, true) if show then self:show2() end end --- Stops the timergauge. Works whether the timer is started or not. -- @tparam[opt] boolean hide override the autoHide property. True will always hide, false will never hide. -- @usage myTimerGauge:stop() --stops the timer, will hide or not based on autoHide property -- myTimerGauge:stop(false) --stops the timer, will not change hidden status, regardless of autoHide property -- myTimerGauge:stop(true) --stops the timer, will hide it regardless of autoHide property function TimerGauge:stop(hide) if hide == nil then hide = self.autoHide end self.active = false if self.timer then killTimer(self.timer) self.timer = nil end stopStopWatch(self.stopWatchName) if hide then self:hide2() end end --- Alias for stop. -- @tparam[opt] boolean hide override the autoHide property. True will always hide, false will never hide. function TimerGauge:pause(hide) self:stop(hide) end --- Resets the time on the timergauge to its original value. Does not alter the running state of the timer function TimerGauge:reset() resetStopWatch(self.stopWatchName) adjustStopWatch(self.stopWatchName, self.time * -1) self:update() end --- Resets and starts the timergauge. -- @tparam[opt] boolean show override the autoShow property. true will always show, false will never show -- @usage myTimerGauge:restart() --restarts the timer, will show or not based on autoShow property -- myTimerGauge:restart(false) --restarts the timer, will not change hidden status, regardless of autoShow property -- myTimerGauge:restart(true) --restarts the timer, will show it regardless of autoShow property function TimerGauge:restart(show) self:reset() self:start(show) end --- Get the amount of time remaining on the timer, in seconds -- @tparam string format Format string for how to return the time. If not provided defaults to self.timeFormat(which defaults to "S.t").<br> -- If "" is passed will return "" as the time. See below table for formatting codes<br> -- <table class="tg"> -- <tr> -- <th>format code</th> -- <th>what it is replaced with</th> -- </tr> -- <tr> -- <td class="tg-1">S</td> -- <td class="tg-1">Time left in seconds, unbroken down. Does not include milliseconds.<br> -- IE a timer with 2 minutes left it would replace S with 120 -- </td> -- </tr> -- <tr> -- <td class="tg-2">dd</td> -- <td class="tg-2">Days, with 1 leading 0 (0, 01, 02-...)</td> -- </tr> -- <tr> -- <td class="tg-1">d</td> -- <td class="tg-1">Days, with no leading 0 (1,2,3-...)</td> -- </tr> -- <tr> -- <td class="tg-2">hh</td> -- <td class="tg-2">hours, with leading 0 (00-24)</td> -- </tr> -- <tr> -- <td class="tg-1">h</td> -- <td class="tg-1">hours, without leading 0 (0-24)</td> -- </tr> -- <tr> -- <td class="tg-2">MM</td> -- <td class="tg-2">minutes, with a leading 0 (00-59)</td> -- </tr> -- <tr> -- <td class="tg-1">M</td> -- <td class="tg-1">minutes, no leading 0 (0-59)</td> -- </tr> -- <tr> -- <td class="tg-2">ss</td> -- <td class="tg-2">seconds, with leading 0 (00-59)</td> -- </tr> -- <tr> -- <td class="tg-1">s</td> -- <td class="tg-1">seconds, no leading 0 (0-59)</td> -- </tr> -- <tr> -- <td class="tg-2">t</td> -- <td class="tg-2">tenths of a second<br> -- timer with 12.345 seconds left, t would<br> -- br replaced by 3. -- </td> -- </tr> -- <tr> -- <td class="tg-1">mm</td> -- <td class="tg-1">milliseconds with leadings 0s (000-999)</td> -- </tr> -- <tr> -- <td class="tg-2">m</td> -- <td class="tg-2">milliseconds with no leading 0s (0-999)</td> -- </tr> -- </table><br> -- @usage myTimerGauge:getTime() --returns the time using myTimerGauge.format -- myTimerGauge:getTime("hh:MM:ss") --returns the time as hours, minutes, and seconds, with leading 0s (01:23:04) -- myTimerGauge:getTime("S.mm") --returns the time as the total number of seconds, including milliseconds (114.004) function TimerGauge:getTime(format) format = format or self.timeFormat local time = getStopWatchTime(self.stopWatchName) local timerTable = getStopWatchBrokenDownTime(self.stopWatchName) if time > 0 then self:stop(self.autoHide) resetStopWatch(self.stopWatchName) time = getStopWatchTime(self.stopWatchName) timerTable = getStopWatchBrokenDownTime(self.stopWatchName) self.active = false end if format == "" then return format end local totalSeconds = string.split(math.abs(time), "%.")[1] local tenths = string.sub(string.format("%03d", timerTable.milliSeconds), 1, 1) format = format:gsub("S", totalSeconds) format = format:gsub("t", tenths) format = format:gsub("mm", string.format("%03d", timerTable.milliSeconds)) format = format:gsub("m", timerTable.milliSeconds) format = format:gsub("MM", string.format("%02d", timerTable.minutes)) format = format:gsub("M", timerTable.minutes) format = format:gsub("dd", string.format("%02d", timerTable.days)) format = format:gsub("d", timerTable.days) format = format:gsub("ss", string.format("%02d", timerTable.seconds)) format = format:gsub("s", timerTable.seconds) format = format:gsub("hh", string.format("%02d", timerTable.hours)) format = format:gsub("h", timerTable.hours) return format end -- Execute the timer's hook, if there is one. Internal function function TimerGauge:executeHook() local hook = self.hook if not hook then return end local hooktype = type(hook) if hooktype == "string" then local f, e = loadstring("return " .. hook) if not f then f, e = loadstring(hook) end if not f then debugc(string.format("TimerGauge encountered an error while executing the hook for TimerGauge with name: %s error: %s", self.name, tostring(e))) return end hook = f end hooktype = type(hook) if hooktype ~= "function" then debugc(string.format( "TimerGauge with name: %s was given a hook which is neither a function nor a string which can be made into one. Provided type was %s", self.name, hooktype)) return end local worked, err = pcall(hook) if not worked then debugc(string.format("TimerGauge named %s encountered the following error while executing its hook: %s", self.name, err)) end end --- Sets the timer's remaining time to 0, stops it, and executes the hook if one exists. -- @tparam[opt] boolean skipHook use true to have it set the timer to 0 and stop, but not execute the hook. -- @usage myTimerGauge:finish() --executes the hook if it has one -- myTimerGauge:finish(false) --will not execute the hook function TimerGauge:finish(skipHook) resetStopWatch(self.stopWatchName) self:update(skipHook) end -- Internal function, no ldoc -- Updates the gauge based on time remaining. -- @tparam[opt] boolean skipHook use true if you do not want to execute the hook if the timer is at 0. function TimerGauge:update(skipHook) local time = self.showTime and self:getTime(self.timeFormat) or "" local current = tonumber(self:getTime("S.mm")) local suffix = self.suffix or "" local prefix = self.prefix or "" local text = string.format("%s%s%s", prefix, time, suffix) self:setValue(current, self.time, text) if current == 0 then if self.timer then killTimer(self.timer) self.timer = nil end if not skipHook then self:executeHook() end end end --- Sets the amount of time the timer will run for. Make sure to call :reset() or :restart() -- if you want to cause the timer to run for that amount of time. If you set it to a time lower -- than the time left on the timer currently, it will reset the current time, otherwise it is left alone -- @tparam number time how long in seconds the timer should run for -- @usage myTimerGauge:setTime(50) -- sets myTimerGauge's max time to 50. function TimerGauge:setTime(time) local timetype = type(time) if timetype ~= "number" then local err = string.format("TimerGauge:setTime(time): time as number expected, got %s", timetype) debugc(err) return nil, err end time = math.abs(time) if time == 0 then local err = "TimerGauge:setTime(time): you cannot pass in 0 as the max time for the timer" debugc(err) return nil, err end local currentTime = tonumber(self:getTime("S.t")) self.time = time if time < currentTime then self:reset() else self:update(currentTime == 0) end end --- Changes the time between gauge updates. -- @tparam number updateTime amount of time in milliseconds between gauge updates. Must be a positive number. function TimerGauge:setUpdateTime(updateTime) local updateTimeType = type(updateTime) assert(updateTimeType == "number", string.format("TimerGauge:setUpdateTime(updateTime): name: %s updateTime as number expected, got %s", self.name, updateTimeType)) assert(updateTime > 0, string.format("TimerGauge:setUpdateTime(updateTime): name: %s updateTime must be a positive number. You gave %d", self.name, updateTime)) self.updateTime = updateTime if self.timer then killTimer(self.timer) self.timer = nil end if self.active then self.timer = tempTimer(updateTime / 1000, function() self:update() end, true) end end TimerGauge.parent = Geyser.Gauge setmetatable(TimerGauge, Geyser.Gauge) --- Creates a new TimerGauge instance. -- @tparam table cons a table of options (or constraints) for how the TimerGauge will behave. Valid options include: -- <br> -- <table class="tg"> -- <tr> -- <th>name</th> -- <th>description</th> -- <th>default</th> -- </tr> -- <tr> -- <td class="tg-1">time</td> -- <td class="tg-1">how long the timer should run for</td> -- <td class="tg-1"></td> -- </tr> -- <tr> -- <td class="tg-2">active</td> -- <td class="tg-2">whether the timer should run or not</td> -- <td class="tg-2">true</td> -- </tr> -- <tr> -- <td class="tg-1">showTime</td> -- <td class="tg-1">should we show the time remaining on the gauge?</td> -- <td class="tg-1">true</td> -- </tr> -- <tr> -- <td class="tg-2">prefix</td> -- <td class="tg-2">text you want shown before the time.</td> -- <td class="tg-2">""</td> -- </tr> -- <tr> -- <td class="tg-1">suffix</td> -- <td class="tg-1">text you want shown after the time.</td> -- <td class="tg-1">""</td> -- </tr> -- <tr> -- <td class="tg-2">timerCaption</td> -- <td class="tg-2">Alias for suffix. Deprecated and may be remove in the future</td> -- <td class="tg-2"/> -- </tr> -- <tr> -- <td class="tg-1">updateTime</td> -- <td class="tg-1">number of milliseconds between gauge updates.</td> -- <td class="tg-1">10</td> -- </tr> -- <tr> -- <td class="tg-2">autoHide</td> -- <td class="tg-2">should the timer :hide() itself when it runs out/you stop it?</td> -- <td class="tg-2">true</td> -- </tr> -- <tr> -- <td class="tg-1">autoShow</td> -- <td class="tg-1">should the timer :show() itself when you start it?</td> -- <td class="tg-1">true</td> -- </tr> -- <tr> -- <td class="tg-2">manageContainer</td> -- <td class="tg-2">should the timer remove itself from its container when you call <br>:hide() and add itself back when you call :show()?</td> -- <td class="tg-2">false</td> -- </tr> -- <tr> -- <td class="tg-1">timeFormat</td> -- <td class="tg-1">how should the time be displayed/returned if you call :getTime()? <br>See table below for more information</td> -- <td class="tg-1">"S.t"</td> -- </tr> -- </table> -- <br>Table of time format options -- <table class="tg"> -- <tr> -- <th>format code</th> -- <th>what it is replaced with</th> -- </tr> -- <tr> -- <td class="tg-1">S</td> -- <td class="tg-1">Time left in seconds, unbroken down. Does not include milliseconds.<br> -- IE a timer with 2 minutes left it would replace S with 120 -- </td> -- </tr> -- <tr> -- <td class="tg-2">dd</td> -- <td class="tg-2">Days, with 1 leading 0 (0, 01, 02-...)</td> -- </tr> -- <tr> -- <td class="tg-1">d</td> -- <td class="tg-1">Days, with no leading 0 (1,2,3-...)</td> -- </tr> -- <tr> -- <td class="tg-2">hh</td> -- <td class="tg-2">hours, with leading 0 (00-24)</td> -- </tr> -- <tr> -- <td class="tg-1">h</td> -- <td class="tg-1">hours, without leading 0 (0-24)</td> -- </tr> -- <tr> -- <td class="tg-2">MM</td> -- <td class="tg-2">minutes, with a leading 0 (00-59)</td> -- </tr> -- <tr> -- <td class="tg-1">M</td> -- <td class="tg-1">minutes, no leading 0 (0-59)</td> -- </tr> -- <tr> -- <td class="tg-2">ss</td> -- <td class="tg-2">seconds, with leading 0 (00-59)</td> -- </tr> -- <tr> -- <td class="tg-1">s</td> -- <td class="tg-1">seconds, no leading 0 (0-59)</td> -- </tr> -- <tr> -- <td class="tg-2">t</td> -- <td class="tg-2">tenths of a second<br> -- timer with 12.345 seconds left, t would<br> -- br replaced by 3. -- </td> -- </tr> -- <tr> -- <td class="tg-1">mm</td> -- <td class="tg-1">milliseconds with leadings 0s (000-999)</td> -- </tr> -- <tr> -- <td class="tg-2">m</td> -- <td class="tg-2">milliseconds with no leading 0s (0-999)</td> -- </tr> -- </table><br> -- @param parent The Geyser parent for this TimerGauge -- @usage -- local TimerGauge = require("MDK.timergauge") -- myTimerGauge = TimerGauge:new({ -- name = "testGauge", -- x = 100, -- y = 100, -- height = 40, -- width = 200, -- time = 10 -- }) function TimerGauge:new(cons, parent) -- type checking and error handling local consType = type(cons) if consType ~= "table" then local err = string.format("TimerGauge:new(options, parent): options must be provided as a table, received: %s", consType) debugc(err) return nil, err end local timetype = type(cons.time) local time = tonumber(cons.time) if not time then local err = string.format( "TimerGauge:new(options, parent): options table must include a time entry, which must be a number. We received: %s which is type: %s", cons.time or tostring(cons.time), timetype) debugc(err) return nil, err end cons.time = math.abs(time) if cons.time == 0 then local err = "TimerGauge:new(options, parent): time entry in options table must be non-0" debugc(err) return nil, err end if cons.timerCaption and (not cons.suffix) then cons.suffix = cons.timerCaption end cons.type = cons.type or "timergauge" -- call parent constructor local me = self.parent:new(cons, parent) -- add TimerGauge as the metatable/index setmetatable(me, self) self.__index = self -- apply any styling requested if me.cssFront then if not me.cssBack then me.cssBack = me.cssFront .. "background-color: black;" end me:setStyleSheet(me.cssFront, me.cssBack, me.cssText) end -- create and reset the driving stopwatch me.stopWatchName = me.name .. "_timergauge" createStopWatch(me.stopWatchName) me:reset() -- start it up? if me.active then me:start() end me:update() return me end return TimerGauge