Scriptable Timer / Stopwatch

I really appreciate your time.

I will start with Objective-C then :slight_smile:

Hi kamil,

Technology is really great for teaching. After all, that is what the students grew up with. If you were very serious, you might want to get QuickTime Pro. I think that’s what they still call it.

Anyway, the scripting part is quite easy to learn I think, but if you want to jump into objective c then that might prove to be a little harder especially if you haven’t had any programming experience.

If that’s the case, then there’s this thing called AppleScript-ObjectiveC I think that was the name that you can use in Xcode. What it is is basically ObjectiveC made easy by combining AppleScript (a scripting language) with ObjectiveC (a lower level language). Note that a lower level language is harder as it gets closer to machine language (I think a bunch of zeroes and ones).

Anyway, If you start with a scripting language and then integrate that with a lower language then it might be easier. Of course, it depends on your learning style. Some People can just catch on to it and others cannot do that (me :D). But then everybody has their strong and weak points.

Just my thoughts.

gl,
kel

@StefanK

In your honor, here I present “Steffuhr” “ the Stopwatch app.
â–¸ the app name comes from the German word, “Stoppuhr,” which uncle Google claims it means stopwatch.

You can see the logo here.

I uploaded the Steffuhr app with the fixed logo here (file “Steffuhr Stopwatch.zip”).

Thank you so much for everything. I will be posting here about any updates I will do, or questions I will have.

appreciated, but the German word for stopwatch is actually “Stoppuhr”

Ohh yes, my bad. Uncle Google was right. Fixed it. Still, the app name comes from your name â–¸ STEFan+stoppUHR=Steffuhr (to make the “pp” consistent I doubled the “f”). I hope it doesn’t sound funny in German and sort of makes sense.

My 1st language isn’t English and I know that native English speakers don’t get making up or changing the names. Not the case where I come from (Poland), where people make up names all the time.

Hi kamil,

I rewrote the stopwatch with pause if you’re interested. Was on a little medication, because of a bad cold.

Anyway, here’s an example of stopwatch with pause using the reopen handler:

global run_time, is_running, start_date

-- initialize
on run
	set run_time to 0
	set is_running to false
	reopen
end run

-- reopen to toggle pause
on reopen
	set is_running to not is_running
	set cur_date to current date
	if is_running then -- set start date
		-- start or reset start date
		set start_date to cur_date
		say "running" without waiting until completion
	else -- pause (add to total time)
		set run_time to run_time + (cur_date - start_date)
		say "paused" without waiting until completion
	end if
end reopen

on quit
	-- display total run time without the pauses
	set cur_date to current date
	set run_time to run_time + (cur_date - start_date)
	try -- trap error bug if user cancel dialog
		display dialog "" & run_time & " secs"
	end try
	continue quit
end quit

You can run this with another AppleScript or Service using a keyboard shortcut or just rerun.

Edited: there’s still a bug if the user quits when it’s paused. Working on that and thinking of the best way to integrate this without an overall timer. I think I have one in the list somewhere, but can’t find it.

Edited: quick fix. Check if it was running or paused in the quit handler:

global run_time, is_running, start_date

-- initialize
on run
	set run_time to 0
	set is_running to false
	reopen
end run

-- reopen to toggle pause
on reopen
	set is_running to not is_running
	set cur_date to current date
	if is_running then -- set start date
		-- start or reset start date
		set start_date to cur_date
		say "running" without waiting until completion
	else -- pause (add to total time)
		set run_time to run_time + (cur_date - start_date)
		say "paused" without waiting until completion
	end if
end reopen

on quit
	-- display total run time without the pauses
	set cur_date to current date
	if is_running then
		set run_time to run_time + (cur_date - start_date)
	end if
	try -- trap error bug if user cancel dialog
		display dialog "" & run_time & " secs"
	end try
	continue quit
end quit

Still might need testing.

Edited: didn’t need to get the current date if it was paused:

global run_time, is_running, start_date

-- initialize
on run
	set run_time to 0
	set is_running to false
	reopen
end run

-- reopen to toggle pause
on reopen
	set is_running to not is_running
	set cur_date to current date
	if is_running then -- set start date
		-- start or reset start date
		set start_date to cur_date
		say "running" without waiting until completion
	else -- pause (add to total time)
		set run_time to run_time + (cur_date - start_date)
		say "paused" without waiting until completion
	end if
end reopen

on quit
	-- display total run time without the pauses
	if is_running then
		set cur_date to current date
		set run_time to run_time + (cur_date - start_date)
	end if
	try -- trap error bug if user cancel dialog
		display dialog "" & run_time & " secs"
	end try
	continue quit
end quit

gl,
kel

Hello Kel.

I liked your applet, but shouldn’t you have written that it must be exported as a “stay open applet”?

This is a different take on the problem of recording time. I use a script instead of an Applet. It works after the “finite state machine” principle, as it acts accordingly to which state it is currently in, much like your applet, but still not so, since it isn’t running at all times.

The script tracks the time in a property, so you can’t lock the script against modification I believe. You can start, pause and stop the “Stop Watch”.

I haven’t decided, if I should subtract the slack off the end time, or if I should leave a slack column.

How it cycles through the different states:

It starts off from the state “Stopped”, then it can only bekomme “Running”. From “Running”, it can become “Stopped” or “Paused”. From “Paused” it can become “Running” or “Stopped”. The code really doesn’t look very good, but it should work as expected.

property scriptTitle : "Stop Watch"
property state : "Stopped"
property t0 : 0
property elapsed : 0
property slack : 0
property observation : "Enter what to time:"
global clockIcon
# Copyright © 2015 McUsr, you may not post this as a work of your own on some webpage, or in a book.
-- http://macscripter.net/viewtopic.php?pid=179972#p179972
(*
 Now considers daylightsavings time regardless of locale
 This works as follows: if the time to gmt has increased, from when we started timing, then we must 
 subtract the difference, if the time has decreased, then we increase the time of the current lap
 likewise.
*)
on run
	set clockIcon to (path to library folder from system domain as text) & "CoreServices:CoreTypes.bundle:Contents:Resources:Clock.icns"
	if state = "Stopped" then
		tell application (path to frontmost application as text)
			display dialog "Start StopWatch" default answer observation with title scriptTitle buttons {"Cancel", "Start"} cancel button 1 default button 2 with icon file clockIcon
		end tell
		-- We never pass this point if the user hits "Cancel"
		set observation to text returned of result
		set {state, t0, t0_ToGMT} to {"Running", (current date), (time to GMT)}
		-- Changes the state to running so we don't enter this block before the script is stopped.
		display notification "Clocking: " & observation with title scriptTitle
	else if state = "Running" then
		
		tell application (path to frontmost application as text)
			display dialog "Pause  or Stop: " & observation with title scriptTitle buttons {"Cancel", "Pause", "Stop"} default button 2 with icon file clockIcon
		end tell
		
		set btn to button returned of result
		if btn is not "Cancel" then
			-- Recording "lap"
			set Te_ToGMT to (time to GMT)
			set elapsed to elapsed + ((current date) - t0) + (t0_ToGMT - Te_ToGMT)
			if btn = "Pause" then
				-- starting to "slack"
				set {state, slackStart, t0_ToGMT} to {"Paused", (current date), (time to GMT)}
				display notification "Paused after " & my formatTime(elapsed) & "." with title scriptTitle subtitle observation
			else if btn = "Stop" then
				set state to "Stopped"
				stopDialog(observation, elapsed, slack, scriptTitle)
			end if
		end if
		-- We can come back to the state "running" from the state "Paused", or when we have started afresh again from the state "Stopped"
	else if state is "Paused" then
		
		tell application (path to frontmost application as text)
			display dialog "Start or Stop: " & observation & "?" with title scriptTitle buttons {"Cancel", "Start", "Stop"} default button 2 with icon file clockIcon
		end tell
		
		set btn to button returned of result
		if btn is not "Cancel" then
			set Te_ToGMT to (time to GMT)
			tell (current date)
				set {slack, t0} to {slack + (it - slackStart) + (t0_ToGMT - Te_ToGMT), it}
			end tell
			if btn = "Start" then
				set state to "Running"
				set t0_ToGMT to (time to GMT)
				display notification "Clocking continued at " & t0's time string with title scriptTitle subtitle observation
			else if btn = "Stop" then
				stopDialog(observation, elapsed, slack, scriptTitle)
				set state to "Stopped"
				set observation to "Enter what to time:"
			end if
		end if
		-- we can only come back to this state from the state "Running", that is, if we don't stop the script from this "Paused" state.
	end if
	
end run

on stopDialog(observation, obsTime, slack, scriptTitle)
	set clockedTime to formatTime(obsTime)
	set slackedTime to formatTime(slack)
	set resultString to "Final time for:
" & observation & " was: " & clockedTime & "
Slack: " & slackedTime
	tell application (path to frontmost application as text)
		display dialog resultString with title scriptTitle buttons {"Clipboard", "Ok"} default button 2 with icon file clockIcon
	end tell
	if button returned of result is "Clipboard" then set the clipboard to resultString
	resetVars()
	-- we do wipe out the observation here
end stopDialog

on resetVars()
	global observation, t0, elapsed, slack
	set {observation, t0, elapsed, slack} to {"Enter what to time:", 0, 0, 0}
end resetVars
on formatTime(someSecs)
	if someSecs ≥ 3600 then -- we have to consider hours
		set tHours to someSecs div 3600
		if tHours < 10 then
			set tHours to "0" & tHours & ":"
		else
			set tHours to "" & tHours & ":"
		end if
		set someSecs to someSecs mod 3600
	else
		set tHours to ""
	end if
	set tMinutes to (text -2 thru -1 of ("0" & (someSecs div 60))) & ":"
	set someSecs to someSecs mod 60
	set tSecs to text -2 thru -1 of ("0" & someSecs)
	return tHours & tMinutes & tSecs
end formatTime

Hi McUsr,

Great idea about closing the app instead of having it go into stasis. I’m curious to see how you solved the startup time delay of the app. Need to look at your script.

Btw, I think I did write that it needs to be saved as stay open at the beginning. Will check it out.

Have a good day!
kel

Hi McUsr,

I read your post wrongly in thinking that you were quitting the app. Interesting, you’re switching between the display dialog and notifications.

Great idea with using Notification Center! Also, I didn’t think about the app that is timed being frontmost.

Getting ideas about using AppleScript ObjC and doing it completely scripting UserNotificationCenter. That way, we can skip using display dialog maybe. I haven’t tried it in Yosemite yet, but know that there are at least three places to click using AppleScript ObjC. This also eliminates the frontmost app problem.

Have a great day! :smiley:
kel

Hello.

I actually added one more notification, when you “restart” the Stopwatch after “pause”. I also removed a bug, (the Cancel button, canceled in Script Debugger without a cancel button property.). I have now added a proper “cancel button 1”.

I am fine with having dialogues for entering input, and confirming, like I have done it. The last dialog confirms that the timing is done.

I really have no need for the notification centre from objective c, there is a blogpost from 2011 or so here that shows you how to do it with ASOC in an applet.

Other than that, if you use regular notifications from an applet, then you should have a delay before quitting, if you send a notification right before you quit. I read that at macosxautomation.com under “Mavericks”.

Have a nice day, I’m signing off for today.

Hi McUsr when you get back,

On the side, found my old Xcode NotificationCenter script template if anybody is interested and it still works!

script AppDelegate
	property parent : class "NSObject"
    property myNotification : missing value
    property notificationCount: 0
	
	on applicationWillFinishLaunching_(aNotification)
        -- initialize
        return
	end applicationWillFinishLaunching_
    
    -- reopen handler
    on applicationShouldHandleReopen_hasVisibleWindows_(theApplication, aFlag)
        my applicationDidFinishLaunching_(missing value)
        return NO
    end applicationShouldHandleReopen_hasVisibleWindows_
    
    -- main
    on applicationDidFinishLaunching_(aNotification)
        -- set the delegate to script
        current application's NSUserNotificationCenter's defaultUserNotificationCenter's setDelegate_(me)
        -- get current date and target date
        set sentDate to current application's NSDate's |date|()
        set sentDateAsString to my formatDate_(sentDate,1,2)
        set numSeconds to 30
        set targetDate to current application's NSDate's dateWithTimeInterval_sinceDate_(numSeconds,sentDate)
        -- make user info NSDictionary
        set notificationCount to notificationCount + 1
        set nsDict to current application's NSDictionary's dictionaryWithObjectsAndKeys_(notificationCount, "notificationIndex", sentDateAsString, "sentDate", "value3", "key3", missing value)
        -- send notification to NSUserNotificationCenter
		my sendNotification_("MyNotifier",sentDateAsString,"It's time!","Restart","Stop",targetDate,"Boing",nsDict)
        say "Notification sent."
        return
	end applciationDidFinishLaunching_
    --
    
    -- format NSDate to string using styles
    -- 0 = none, 1 = short, 2 = med, 3 = long, 4 = full
    on formatDate_(aNSDate, aDateStyle, aTimeStyle)
        set myFormatter to current application's NSDateFormatter's alloc()'s init()
        myFormatter's setDateStyle_(aDateStyle)
        myFormatter's setTimeStyle_(aTimeStyle)
        set formattedDate to myFormatter's stringFromDate_(aNSDate)
        return formattedDate
    end formatDate_
    
	-- method for sending a notification
	on sendNotification_(aTitle, aSubtitle, aMessage, aActionButtonTitle, aOtherButtonTitle, aDeliveryDate, aSound, aDict)
        -- make the notification
		set myNotification to current application's NSUserNotification's alloc()'s init()
		set myNotification's title to aTitle
        set myNotification's subtitle to aSubtitle
		set myNotification's informativeText to aMessage
        set myNotification's actionButtonTitle to aActionButtonTitle
        set myNotification's otherButtonTitle to aOtherButtonTitle
        set myNotification's deliveryDate to aDeliveryDate
        set myNotification's soundName to aSound
        set myNotification's userInfo to aDict
        -- schedule the notification
        current application's NSUserNotificationCenter's defaultUserNotificationCenter's scheduleNotification_(myNotification)
        return
	end sendNotification_
    
    -- delegate instance methods
    -- force presentation for when application process is frontmost
    on userNotificationCenter_shouldPresentNotification_(aCenter, aNotification)
        return yes
    end userNotificationCenter_shouldPresentNotification_
    
    -- deliver
    on userNotificationCenter_didDeliverNotification_(aCenter, aNotification)
        say "Notification Delivered"
        return
    end userNotificationCenter_didDeliverNotification_

    -- user activation
    on userNotificationCenter_didActivateNotification_(aCenter, aNotification)
        say "Notification Activated"
        -- 0    none
        -- 1    contents clicked
        -- 2    action button clicked
        set userActivationType to (aNotification's activationType) as integer
        if userActivationType is 1 then
            say "contents clicked"
            my contentsClicked_(aNotification)
        else if userActivationType is 2 then
            say "action button clicked"
            my actionButtonClicked_(aNotification)
        else -- userActivationType is 0
            say "no user activation"
        end if
        return userActivationType
    end userNotificationCenter_didActivateNotification_
    
    --
    on contentsClicked_(aNotification)
        -- do something
        return
    end contentsClicked_
    
    -- gets user info and deletes delivered notification from Notification Center
    on actionButtonClicked_(aNotification)
        -- delete the notification and send a new one (or some other action)
        -- first get info on notification
        set theInfo to aNotification's userInfo
        set theValue to theInfo's valueForKey_("notificationIndex")
        say (theValue as string)
        -- delete notification from notification center
        current application's NSUserNotificationCenter's defaultUserNotificationCenter's removeDeliveredNotification_(aNotification)
        -- send a new notification
        my applicationDidFinishLaunching_(missing value)
        return
    end actionButtonClicked_
    --
    
    on applicationShouldTerminateAfterLastWindowClosed_()
        return true
    end applicationShouldTerminateAfterLastWindowClosed_
    
    -- quit
	on applicationShouldTerminate_(sender)
		-- Insert code here to do any housekeeping before your application quits 
		return current application's NSTerminateNow
	end applicationShouldTerminate_
	
end script

Anyway, there is no way for user text entry. Still thinking about that. The display dialog might be necessary. Still thinking about that.

I started out with something simple, but it has grown into a new project. :slight_smile: Anyone can take things as far as they want to take it.

Have a good day,
kel

ps. Thanks for the updates.

Hello kel.

I am not going out yet. What I think is, that instead of using an applet for timing, you can use a script, and thereby saving resources, if you are going to call an applet from a script anyway. The script approach comes short, if there is timing going on at the very moment daylight savings time is implemented/removed.
It is also harder to stop timing, if you put your computer to sleep. (But it is doable by another script, that sets the state and time properties adequately.)

The script approach should save resources, in that the script aren’t running except for when it is executed, the state machine is kind of adapaptable, so you could, just edit it, to do what you see fit in there, like creating a new record in FileMaker Pro, or fill in some cells in your favourite Spreadsheet, if the object of the stopwatch is to create time slips for instance. :slight_smile:

Have a good evening.

Hi McUsr,

The way you see all the places where bugs could occur is amazing! Never thought that the Timer could be running when daylight savings time has changed! There is so much to think about!

So what we need is to monitor the whole time thing (couldn’t think of a word for that :|). Need to look back at your old time scripts.

Edited: and btw, there is no problem with the computer sleeping as we can use caffeinate right.

Great minds,
kel

Well I think caffeinate just keeps the machine from sleeping, I was more thinking if you clocked some task that you could only do while at keyboard, then the timer should be paused when the computer fell asleep, and you should be notified during wakeup. I think displaysleep is the right utility for this, but I am happy with having to edit stuff at the moment, should a ‘sleep incident’ happen. :slight_smile: I didn’t bother to fix the issue with daylight savings, because it happens at a most unlikely time of day. But, before using this for shift-working, this is something that has to be considered.

Thanks for your template, templates are handy, you never know when they may be of use. :slight_smile:

Hello.

Just mentioning one negativism with regards to notification: Notifications are very limited in size, and can only consists of three lines, so, notifications are not fit for all kinds of messages, with respect to dynamic content.

Hi McUsr,

Yeah, the Notification window is limitted. What I get is 3 clicks with just the plain window, but there is another thing Apple has not added yet. I noticed that they added some stuff like the arrow where you have a drop down list. I’t’s not accessible through AppleScript yet, but interesting. You can see this in the notifications for updates where they ask to notify you later or whatever.

Also, I’ve been looking in the developer pages, but I think you can’t use the extra stuff in ASObjC. You need to do all that extra ObjC stuff. I could probably do it as from the teachings of Stefan, but it takes a long time for a beginner.

Anyway, life is a never ending puzzle.

gday,
kel

Hello kel.

I hope you find a solution to your problems with notifications. Personally, I really like them for short messages, but I can see the use. :slight_smile: Well, I had to play some more with the StopWatch, (and test), so now I’ve added a nicer icon, and it resets the text variable, among other things, when you stop the clock. I have also added some meagre comments, all in post #22. I really think it should be easy to adapt the script for journaling purposes, -write stuff to a csv file or something, so I’ll continue with this with FileMaker Pro. :slight_smile:

Oops:

I didn’t mean displaysleep, I meant sleepwatcher, which is a command line utility, if not the command line utility, to control what happens when your machine goes to sleep, wakes up, etc. It should be downloadable if you google it.

Hello.

So, I realized, that I didn’t have to dig deep into the locale database, to track when daylight savings times starts and ends, in order for making correct clockings at all times. :slight_smile:

All it takes, is to record the time to gmt, when the clocking starts, and take the time to gmt when the clocking ends, and then add time to gmt when the time starts - time to gmt when the timing ends, to the number of perceived seconds (difference in seconds between start and end), to make it all work correctly, all places all of the time.

I have updated the script in post #22, and think I am done with this for now.

Hi McUsr,

Yes, the Notification window is very limited with ASObjC. Another thing is you can’t update the text. Also, you can’t get a response when the user clicks the close button I think. Still checking if they made any changes with that button. The developer page was rewritten, so maybe they made changes.

You’re always on the ball with the updates in your apps. Still checking it out. Wish I had your energy.

Have a good day,
kel