It is currently May 27th, 2020, 4:01 am

Dynamic Stopwatch with Lap Support and a Scrollable Lap List

Post your work-in-progress and completed skins to share and discuss.
User avatar
raiguard
Posts: 660
Joined: June 25th, 2015, 7:02 pm
Location: The Sky, USA

Dynamic Stopwatch with Lap Support and a Scrollable Lap List

Post by raiguard »

Stopwatch.gif
Hello all! I have been toiling away on a stopwatch for the last few weeks, and I finally feel like it's ready to be shared! This stopwatch is different from others that I've seen in that it uses a LUA script to manage all of the timing and meter displays. I designed the script to be as easy to use and expandable as possible.

Here is a short overview of how to use the script:

To use the stopwatch, certain measures and meters need to update ten times a second. By default, Rainmeter skins update once per second. To override this, insert the following into the [Rainmeter] section of your skin:

Code: Select all

[Rainmeter]
; To get certain meters to update ten times a second, set the update value in this section to 100 milliseconds.
Update=100
; This prevents meters other than those we want from updating excessively. Meters that we wish to update ten times a second will need 'UpdateDivider=1' set.
DefaultUpdateDivider=10
The script manages all of the logic behind the stopwatch, so the only two measures needed are the script measure and a time measure:

Code: Select all

[MeasureStopwatchScript]
Measure=Script
ScriptFile=#@#Scripts\Stopwatch.lua
; Defaults to 1 if unspecified. Setting this to 0 will return the times as MM:SS.T instead of HH:MM:SS.T
ShowHours=0
; Number of rows in the lap list
LapListHeight=8
; Defines the name of the time measure the stopwatch will use. Defaults to 'MeasureTime' if unspecified.
TimeMeasure=MeasureStopwatchTime
; Update ten times a second
UpdateDivider=1
; Enabling this measure is what starts the stopwatch
Disabled=1

[MeasureStopwatchTime]
Measure=Time
; Creates the tenths place out of a whole number
AverageSize=10
; Update ten times a second
UpdateDivider=1
All stopwatch-related meters use Inline LUA commands to get information from the Stopwatch script. Thus, all stopwatch-related meters need DynamicVariables set in order to properly retrieve the values from the section variable.

Next, we have the main stopwatch display and lap time display:

Code: Select all

[MeterStopwatchMainDisplay]
Meter=String
MeterStyle=StyleString | StyleStringCenterAlign
FontSize=20
FontColor=#colorStopwatchMain#
Y=#contentMargin#
; Gets the current stopwatch time from the script
Text=[&MeasureStopwatchScript:GetTime()]
DynamicVariables=1
; Update ten times a second
UpdateDivider=1

[MeterStopwatchLapDisplay]
Meter=String
MeterStyle=StyleString | StyleStringCenterAlign
FontSize=15
Y=-3R
; Gets the current lap time from the script
Text=[&MeasureStopwatchScript:GetLapTime()]
DynamicVariables=1
; Update ten times a second
UpdateDivider=1
These are pretty straightforward. To get the stopwatch time, simply use Inline LUA to get the return value of the GetTime() function. The lap display simply uses the GetLapTime() function instead. Make certain these meters update ten times a second, otherwise your stopwatch won't be of much use!

Next up, we have the scrolling detection. The scrolling works by using MouseScrollActions on an invisible image meter that covers the entire lap list. I have un-hidden the meter in the image below:
2018-06-23 22_13_23-D__Settings_Caleb_Rainmeter_Skins_Stopwatch_Stopwatch.ini.png
Implementing this is fairly straightforward as well, thanks to the functions I put into the Stopwatch script. All you need to do is call the LapScrollUp() or LapScrollDown() functions when the corresponding scroll action is detected:

Code: Select all

[MeterStopwatchLapListScroll]
Meter=Image
; Set alpha value to > 0 if you wish to see the region where scrolling can occur
SolidColor=255,255,255,0
X=#contentMargin#
Y=#rowSpacing#R
W=#contentWidth#
; Set to the height of the lap list. Can either be a hard-coded value, or use the Y value of the bottom of the list as shown here.
H=([MeterStopwatchLap8Label:Y] + [MeterStopwatchLap8Label:H]) - [MeterStopwatchLapListScroll:Y]
DynamicVariables=1
; Tells the script to scroll the lap list down. Will automatically stop when the bottom of the list is reached.
MouseScrollDownAction=[!CommandMeasure MeasureStopwatchScript "LapScrollDown()"]
; Tells the script to scroll the lap list up. Will automatically stop when the top of the list is reached.
MouseScrollUpAction=[!CommandMeasure MeasureStopwatchScript "LapScrollUp()"]
; Prevents the mouse cursor from becoming a hand (a.k.a "click me!")
MouseActionCursor=0
Next, the lap list itself:

Code: Select all

[MeterStopwatchLap1Label]
Meter=String
MeterStyle=StyleString | StyleStringStopwatchLapLabel
Y=r
; Gets the lap number of the lap at the top of the list
Text=[&MeasureStopwatchScript:GetLap(1)]

[MeterStopwatchLap1LapTime]
Meter=String
MeterStyle=StyleString | StyleStringStopwatchLapValue | StyleStringCenterAlign
; Gets the lap time of the lap at the top of the list
Text=[&MeasureStopwatchScript:GetLap(1, 'lap')]

[MeterStopwatchLap1Total]
Meter=String
MeterStyle=StyleString | StyleStringStopwatchLapValue
; Gets the stopwatch time when the lap at the top of the list was created
Text=[&MeasureStopwatchScript:GetLap(1, 'total')]
Once again, very straightforward thanks to the script!

The number argument in the GetLap() function DOES NOT refer to the lap number itself - it refers to the lap position in the list. For example, this first set is the top of the list, so it is lap 1. The next row down is the second row, hence lap 2. And so on.

Just including a number argument in GetLap() returns the lap number of the lap at the top of the list. Adding 'lap' or 'total' to the arguments returns the lap time or the total stopwatch time at the time the lap was made, respectively. Simply repeat this set of meters, incrementing the number by 1, for as long as you would like the list to be, and set the LapListHeight option on the script measure accordingly.

Last but not least, we have the controls! Here is the code with explanations baked in:

Code: Select all

[MeterStopwatchStartButton]
Meter=String
MeterStyle=StyleString | StyleStringStopwatchButton
FontColor=#colorStopwatchStart#
FontSize=25
X=(#contentMargin# + 6)
Y=(#contentMargin# + 10)
Text=[\x45]
; Tell the other meters that the stopwatch is running, set the current time as the time the stopwatch will measure from, and enable the script measure so the stopwatch will start counting.
LeftMouseUpAction=[!SetVariable stopwatchStatus 1][!UpdateMeterGroup StopwatchMeters][!CommandMeasure MeasureStopwatchScript "deltaTime = [MeasureStopwatchTime:]"][!EnableMeasure MeasureStopwatchScript]
; Show only when the stopwatch is reset (optional)
Hidden=(#stopwatchStatus# <> 0)
ToolTipText=Start

[MeterStopwatchUnpauseButton]
Meter=String
MeterStyle=StyleString | StyleStringStopwatchButton | MeterStopwatchStartButton
Y=r
; Tell the other meters that the stopwatch is running, command the script to unpause the stopwatch.
LeftMouseUpAction=[!SetVariable stopwatchStatus 1][!UpdateMeterGroup StopwatchMeters][!CommandMeasure MeasureStopwatchScript "paused = 0"]
; Show only when the stopwatch is paused (optional)
Hidden=(#stopwatchStatus# <> 2)
ToolTipText=Resume

[MeterStopwatchLapButton]
Meter=String
MeterStyle=StyleString | StyleStringStopwatchButton
FontColor=#colorStopwatchLapButton#
X=5r
Y=5r
FontSize=16
Text=[\x7d]
; Command the script to create a lap using the current stopwatch and lap times
LeftMouseUpAction=[!CommandMeasure MeasureStopwatchScript "Lap()"]
; Show only when the stopwatch is running (optional)
Hidden=(#stopwatchStatus# <> 1)
ToolTipText=Lap

[MeterStopwatchPauseButton]
Meter=String
MeterStyle=StyleString | StyleStringStopwatchButton
FontColor=#colorStopwatchPauseButton#
FontSize=25
X=(#contentMarginRight# - 4)
Y=(#contentMargin# + 10)
StringAlign=Right
Text=[\x60]
; Tell the other meters that the stopwatch is paused, command the measure to pause the stopwatch.
LeftMouseUpAction=[!SetVariable stopwatchStatus 2][!UpdateMeterGroup StopwatchMeters][!CommandMeasure MeasureStopwatchScript "paused = 1"]
; Show only when the stopwatch is running (optional)
Hidden=(#stopwatchStatus# <> 1)
ToolTipText=Pause

[MeterStopwatchResetButton]
Meter=String
MeterStyle=StyleString | StyleStringStopwatchButton
FontColor=#colorStopwatchResetButton#
FontSize=13
StringAlign=Right
X=-8r
Y=8r
Text=[\xe02a]
; Tell the other meters that the stopwatch is reset, disable the script measure to stop it from counting, command the script to reset all values to zero, update stopwatch meters.
LeftMouseUpAction=[!SetVariable stopwatchStatus 0][!DisableMeasure MeasureStopwatchScript][!CommandMeasure MeasureStopwatchScript "Reset()"][!UpdateMeterGroup StopwatchMeters]
; Show only when the stopwatch is paused or reset (optional)
Hidden=(#stopwatchStatus# = 1)
ToolTipText=Reset
The only required things in these meters are the MouseActions, and the code contains explanations of what you need to do. Keep in mind that you must execute these bangs in the order they are written here, or things might break! I hold no responsibility for computers that spontaneously combust because you forgot to disable the stopwatch script!

For anyone interested, here is the contents of the script in its entirety:

Code: Select all

-- ----------------------------------------
-- Stopwatch.lua
-- v1.0.0
-- raiguard
-- ----------------------------------------

measureTime = 0
realTime = 0
deltaTime = 0
elapsedTime = 0

lapDeltaTime = 0
lapTime = 0
lapCount = 0
lapScroll = 0
laps = {}
lapListHeight = 0

paused = 0

debug = false

function Initialize()

	measureTime = SKIN:GetMeasure(SELF:GetOption('TimeMeasure', 'MeasureTime'))
	lapListHeight = tonumber(SELF:GetOption('LapListHeight', 5))
	showHours = tonumber(SELF:GetOption('ShowHours', 1))
	Reset()

end

function Update() --> Updates the stopwatch time and lap time ten times a second

	realTime = measureTime:GetValue()
	if paused == 1 then deltaTime = realTime - elapsedTime
		else elapsedTime = (realTime - deltaTime) end

end

function Reset() --> Resets all stopwatch statistics to their starting point

	realTime = 0
	deltaTime = 0
	elapsedTime = 0

	lapDeltaTime = 0
	lapTime = 0
	lapCount = 0
	lapScroll = 0
	laps = {}

	paused = 0

end

function GetTime() return FormatTimeString(elapsedTime) end --> Returns the current stopwatch time. Usage: Text=[&MeasureStopwatchScript:GetTime()]

function GetLapTime() return FormatTimeString(elapsedTime - lapDeltaTime) end --> Returns the current stopwatch lap time. Usage: Text=[&MeasureStopwatchScript:GetLapTime()]

function GetLap(lap, value) --> Returns the lap number, lap time, or stopwatch time for a specific lap.

	if lapCount <= lap - 1 then return '-'
	elseif value then return laps[lapScroll - (lap - 1)][value]
		else return lapScroll - (lap - 1) end

	-- USAGE:
	-- Text=[&MeasureStopwatchScript:GetLap(1)] --> Returns the lap number of the highest lap on the list
	-- Text=[&MeasureStopwatchScript:GetLap(1, 'lap')] --> Returns the lap's lap time
	-- Text=[&MeasureStopwatchScript:GetLap(1, 'total')] --> Returns the total stopwatch time when that lap was made

end

function Lap() --> Takes the current stopwatch time and creates a new lap from it

	if lapScroll == lapCount then lapScroll = lapScroll + 1 end
	lapCount = lapCount + 1
	table.insert(laps, lapCount, { lap = GetLapTime(), total = GetTime() })
	lapDeltaTime = elapsedTime
	LogHelper('Lap ' .. lapCount .. ' = ' .. laps[lapCount]['total'], 'Debug')
	SKIN:Bang('!UpdateMeterGroup', 'LapMeters')
	SKIN:Bang('!Redraw')

end

function LapScrollUp() --> Scrolls the lap list up. Will automatically stop if the top of the list is reached.

	if lapScroll < lapCount then
		lapScroll = lapScroll + 1
		SKIN:Bang('!UpdateMeterGroup', 'LapMeters')
		SKIN:Bang('!Redraw')
	end
end

function LapScrollDown() --> Scrolls the lap list down. Will automatically stop if the bottom of the list is reached.

	if lapScroll > lapListHeight then
		lapScroll = lapScroll - 1
		SKIN:Bang('!UpdateMeterGroup', 'LapMeters')
		SKIN:Bang('!Redraw')
	end
end

function FormatTimeString(time) --> Converts a raw timestamp value into a human-readable format.

	local hours = tostring(math.floor((time / 3600) % 24)):gsub('(.+)', '0%1'):gsub('^%d(%d%d)$', '%1')
	local minutes = tostring(math.floor((time / 60) % 60)):gsub('(.+)', '0%1'):gsub('^%d(%d%d)$', '%1')
	local seconds = tostring(math.floor(time % 60)):gsub('(.+)', '0%1'):gsub('^%d(%d%d)$', '%1')
	local tenths = round((time * 10) % 10)
	if tenths == 10 then tenths = 0 end

	if showHours == 1 then return hours .. ':' .. minutes .. ':' .. seconds .. '.' .. tenths
		else return minutes .. ':' .. seconds .. '.' .. tenths end

end

function round(x) --> Rounds...
  if x%2 ~= 0.5 then
    return math.floor(x+0.5)
  end
  return x-0.5
end

-- function to make logging messages less cluttered
function LogHelper(message, type)

  if type == nil then type = 'Debug' end

  if debug == true then
    SKIN:Bang("!Log", message, type)
  elseif type ~= 'Debug' then
  	SKIN:Bang("!Log", message, type)
	end

end
And... if my calculations are correct, that's it! Let me know what you think, if there are any features I should add, or if you find any bugs!
You do not have the required permissions to view the files attached to this post.
”We are pretty sure that r2922 resolves the regression in resolution caused by a reversion to a revision.” - jsmorley, 2017
User avatar
limitless
Posts: 53
Joined: January 8th, 2017, 2:31 am
Location: Charlotte, NC

Re: Dynamic Stopwatch with Lap Support and a Scrollable Lap List

Post by limitless »

Very well done! I am going to try to implement this into my new Plex Media Server Skin when I get a chance. :thumbup:
Image