#!/usr/bin/env python title = "Stopwatch" version = "0.1.0" copyright = "Copyright (C) 2006 George F. Rice" website = "http://drgeorge.org/stopwatch/" authors = ["George F. Rice"] logo = "icon.png" license = "gpl.txt" """Stopwatch, a multi-lap timing application for GTK+ compatible devices Copyright (C) 2006 George F. Rice 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 program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ from Stopwatch import stopwatch # a model of a multi-lap time stopwatch # Maemo is true if executing on a Nokia 770 or other hildon-based device try: import hildon maemo = True except: maemo = False import gobject import gtk import pango import os.path import string import webbrowser class gtkStopwatch: """ Implement a stopwatch GUI for generic GTK+ systems """ def __init__(self): # Still using item_factory until Nokia 770 UIManager works self.menu_items = ( ( "/_File", None, None, 0, "" ), ( "/File/_New", "N", self.fileNew, 0, None ), ( "/File/_Open", "O", self.fileOpen, 0, None ), ( "/File/Open _Template", "O", self.fileOpenTemplate, 0, None ), ( "/File/_Save", "S", self.fileSave, 0, None ), ( "/File/Save _As", "S", self.fileSaveAs, 0, None ), ( "/File/sep1", None, None, 0, "" ), ( "/File/_Quit", "Q", self.fileExit, 0, None ), ( "/_Edit", None, None, 0, "" ), ( "/Edit/Cu_t", "X", self.editCut, 0, None ), ( "/Edit/_Copy", "C", self.editCopy, 0, None ), ( "/Edit/Copy _Time", "C", self.editCopyTime, 0, None ), ( "/Edit/_Paste", "V", self.editPaste, 0, None ), ( "/_View", None, None, 0, "" ), ( "/View/Refresh _Fast", None, self.viewFast, 0, None ), ( "/View/Refresh _Slow", None, self.viewSlow, 0, None ), ( "/View/Refresh _Off", None, self.viewOff, 0, None ), ( "/_Tools", None, None, 0, "" ), ( "/Tools/_Start", "F8", self.toolsStart, 0, None ), ( "/Tools/S_top", "F9", self.toolsStop, 0, None ), ( "/Tools/_Reset", "R", self.toolsReset, 0, None ), ( "/_Help", None, None, 0, "" ), ( "/_Help/Manual", "F1", self.helpManual, 0, None ), ( "/_Help/About...", None, self.helpAbout, 0, None ), ) # Set the important instance attributes. self.gtimer = None # timer object used to drive the stopwatch updates self.refreshRate = 20 # frequency by which the display is updated (msec) self.filename = None # holds the currently open filename self.dirty = False # True of self.sw doesn't match self.filename self.sw = stopwatch() # model of a stopwatch, q.v. # Create the main application window if maemo: self.app = hildon.App() self.appview = hildon.AppView("(Untitled)") else: self.app = gtk.Window() self.app.set_title(title) if maemo: self.app.set_two_part_title(True) self.app.set_appview(self.appview) self.appview.connect("destroy", self.fileExit) else: self.app.connect("delete_event", self.fileExit) # Create the main layout manager (a vertical box) self._vbox = gtk.VBox(False, 0) if maemo: gtk.Container.add(self.appview, self._vbox) else: self.app.add(self._vbox) # Create the menu bar if maemo: main_menu = hildon.AppView.get_menu(self.appview) self.menus = {} self.menuitems ={} for label, key, callback, x, type in self.menu_items: # Delete '_' if present x = label.find('_') if x != -1: label = label[0:x] + label[x+1:] # Disassemble the path x = label.split('/') if len(x) == 2: # submenu self.menuitems[x[1]] = gtk.MenuItem(x[1]) self.menus[x[1]] = gtk.Menu() gtk.Menu.append(main_menu, self.menuitems[x[1]]) gtk.MenuItem.set_submenu(self.menuitems[x[1]], self.menus[x[1]]) elif len(x) == 3 and callback: # item self.menuitems[x[2]] = gtk.MenuItem(x[2]) self.menuitems[x[2]].connect("activate", callback) gtk.Menu.append(self.menus[x[1]], self.menuitems[x[2]]) gtk.Widget.show_all(main_menu) else: self._menu = self.getMenu(self.app) self._vbox.pack_start(self._menu, False, True, 0) # Create top row - start/stop and reset buttons, and stopwatch display # Pango is used to create the giant 50 point stopwatch display self._pango = pango.AttrList() self._pango.insert(pango.AttrSize(50000,0,12)) self._time = gtk.Label("00:00:00.00") self._time.set_alignment(1,0) self._time.set_attributes(self._pango) self._bStart = gtk.Button("Start") self._bReset = gtk.Button("Reset") self._bStart.connect("clicked", self.toolsToggle, None) self._bReset.connect("clicked", self.toolsReset, None) self._hbox = gtk.HBox(False, 0) self._vbox.pack_start(self._hbox, False, False, 0) self._hbox.pack_start(self._bStart, True, False, 0) self._hbox.pack_start(self._time, True, False, 0) self._hbox.pack_start(self._bReset, True, False, 0) # Create a table layout for the lap timers self._frame = gtk.Frame("Lap Timers") self._frame.set_label_align(0.5,0.0) self._vbox.pack_start(self._frame, False, False, 0) self._table = gtk.Table(6, 5, True) self._frame.add(self._table) self.lapLabel = {} # given lap timer display widget, return default lap timer label self.lapTimers = [] # list of the lap timer widgets (name / value pairs) # Create the lap timers. for y in range(9): for x in [0,2,4]: lapDefaultName = "Lap %d" % (y + [1, 10, 19][x/2], ) self.sw.getLapTime(lapDefaultName) # creates the lap timer in the model # The label is a text view, so that it can be easily renamed by the user. _lLapName = gtk.TextView() _lLapName.get_buffer().set_text(lapDefaultName) _lLapName.set_wrap_mode(gtk.WRAP_NONE) _lLapName.set_justification(gtk.JUSTIFY_RIGHT) _lLapName.set_pixels_above_lines(5) _lLapName.set_accepts_tab(False) self._table.attach(_lLapName, x,x+1,y,y+1) _lLapName.show() # The lap time is a button; clicking the button records the lap time. _bLapValue = gtk.Button("") self._table.attach(_bLapValue, x+1,x+2,y,y+1) _bLapValue.show() _bLapValue.connect("clicked", self.recordLap, None) self.lapLabel[_bLapValue] = lapDefaultName self.lapTimers.append((_lLapName, _bLapValue, lapDefaultName)) # Pack the applications elements and show them. if not maemo: self._menu.show() self._time.show() self._bStart.show() self._bReset.show() self._hbox.show() self._frame.show() self._table.show() self._vbox.show() self.app.show() def getMenu(self, window): """ Return the menu for display. ItemFactory is used to support the Nokia 770 (I think?). """ aGroup = gtk.AccelGroup() self.item_factory = gtk.ItemFactory(gtk.MenuBar, "
", aGroup) self.item_factory.create_items(self.menu_items) window.add_accel_group(aGroup) return self.item_factory.get_widget("
") def setFilename(self, filename): """ Setter for the current filename. The title bar is updated with the current filename, or just the app title if no filename. """ self.filename = filename if self.filename: p, f = os.path.split(filename) self.app.set_title("%s - %s" % (title, f)) else: self.app.set_title(title) def refreshTime(self): """ Update the time on the display. This is normalled called from a gobject timer, but can be called manually when needed. """ self._time.set_text(self.sw.getFormattedTime()) return True def recordLap(self, widget, data=None): """ Record and display a lap time. """ lapName = self.lapLabel[widget] self.sw.stopLapTimer(lapName) widget.set_label(self.sw.getFormattedTime(lapName)) self.dirty = True return True def fileNew(self, widget=None, data=None): """ Discard all current data, and create a new stopwatch. """ self.dirty = False self.setFilename(None) self.toolsReset() self.sw = stopwatch() # Reset the labels and clear any lap times previously recorded for _lLapName, _bLapValue, defaultLapName in self.lapTimers: _bLapValue.set_label("") _lLapName.get_buffer().set_text(defaultLapName) self.sw.getLapTime(defaultLapName) # creates the lap timer in the model def fileOpen(self, widget=None, data=None): """ Create a new stopwatch, with times / timers loaded from a filename requested from the user. If the filename is successfully opened, self.filename will be set even if the file is not fully read. """ self.open() def fileOpenTemplate(self, widget=None, data=None): """ Create a new stopwatch with timer at zero and no lap times recorded, but with the lap timer labels loaded from a filename requested from user. """ self.open(None, True) def open(self, filename = None, template = False): """ A utility method to load the specified file into a new stopwatch model and update the user interface. If filename is omitted, one is requested from the user. If template is true, only the lap timer labels are loaded from the file, the timer value and lap timer values are zeroed. """ if not filename: # Ask the user for a filename to open dialog = gtk.FileChooserDialog( "Open Stopwatch", None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) filter = gtk.FileFilter() filter.set_name("All Files") filter.add_pattern("*") dialog.add_filter(filter) filter = gtk.FileFilter() filter.set_name("Stopwatches") filter.add_pattern("*.csv") dialog.add_filter(filter) # If one is provided, try to load it. The file format is documented # in the manual. response = dialog.run() if response == gtk.RESPONSE_OK: filename = dialog.get_filename() dialog.destroy() if filename: i = 0 try: f = open(filename, "r") v = f.readline().strip() if v != title: raise ValueError, "Not a stopwatch file" fmajor, fminor, fpatch = f.readline().strip().split(".") vmajor, vminor, vpatch = version.split(".") if fmajor != vmajor: raise ValueError, "File created with version %s (this is version $s)\nSee website for help." % (fmajor, vmajor) self.setFilename(filename) self.sw = stopwatch() i = 3 if template: # ignore current timer values for j in range(3): f.readline() else: # not a template, so load timer values v = f.readline().strip() if v == "running": self.sw.running = True elif v == "stopped": self.sw.running = False else: raise ValueError, "Expecting 'running' or 'stopped' but read %s" % v i = 4 self.sw.startTime = float(f.readline()) self.sw.elapsedTime = float(f.readline()) self.setRefreshRate(self.refreshRate) # Start visible timer for _lLapName, _bLapValue, lapDefaultName in self.lapTimers: i += 1 n = f.readline() if n: lapName, lapValue = n.strip().split(",") else: lapName, lapValue = lapDefaultName, 0.0 self.sw.lap[lapDefaultName] = float(lapValue) _lLapName.get_buffer().set_text(lapName) if float(lapValue) and not template: _bLapValue.set_label(self.sw.getFormattedTime(lapDefaultName)) else: _bLapValue.set_label("") except Exception, inst: if i: m = "Error while reading line %d of file (partially read):" % (i, ) else: m = "Unable to open file (existing stopwatch preserved):" dialog = gtk.MessageDialog(self.app, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, "%s\n%s\n%s" % (m, filename, inst)) dialog.run() dialog.destroy() # Regardless of what happens, dump the file handle try: f.close() except: pass return True def fileSave(self, widget=None, data=None): """ If self.filename is not None, current data is written to it. Otherwise, a new filename is requested from the user and used to write the stopwatch data. True is returned if data is successfully saved, False otherwise. """ return self.save(self.filename) def fileSaveAs(self, widget=None, data=None): """ Request a new filename from the user, and write the stopwatch data to it. True is returned if data is successfully saved, False otherwise. """ return self.save(None) def save(self, filename): """ A utility method to write stopwatch data to a file. If filename is None, get a filename from the user; if user cancels, return False. If filename is not None or is selected by the user, then write the stopwatch data to it in CSV format as described in the manual. If successful, self.filename will be set to filename and True returned. If not successful, an error dialog is displayed, self.filename is unmodified and False is returned. """ if filename == None: dialog = gtk.FileChooserDialog( "Save Stopwatch", None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) filter = gtk.FileFilter() filter.set_name("Stopwatches") filter.add_pattern("*.csv") dialog.add_filter(filter) response = dialog.run() filename = None if response == gtk.RESPONSE_OK: filename = dialog.get_filename() dialog.destroy() if not filename: return False result = False try: i = 0 f = open(filename, "w") i = 1 f.write(title + "\n"); i += 1 f.write(version + "\n"); i += 1 if self.sw.running: f.write("running\n"); i += 1 else: f.write("stopped\n"); i += 1 f.write("%3f\n" % (self.sw.startTime, )) f.write("%3f\n" % (self.sw.elapsedTime, )) for _lLapName, _bLapValue, lapDefaultName in self.lapTimers: i += 1 b = _lLapName.get_buffer() l = b.get_text(b.get_start_iter(),b.get_end_iter()) f.write(l + "," + str(self.sw.getLapTime(lapDefaultName)) + "\n") self.setFilename(filename) result = True self.dirty = False except Exception, inst: if i: m = "Error while writing line %d of file (partially written):" % (i, ) else: m = "Unable to open file for writing:" dialog = gtk.MessageDialog(self.app, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, "%s\n%s\n%s" % (m, filename, inst)) dialog.run() dialog.destroy() try: f.close() except: pass return result def fileExit(self, widget=None, data=None): """ Terminate this application. If data is dirty, offer user chance to save data or to cancel this operation. """ if self.dirty: dialog = gtk.MessageDialog(self.app, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, "Save before closing?") dialog.add_button("Cancel", gtk.RESPONSE_CANCEL) dialog.set_default_response(gtk.RESPONSE_CANCEL) response = dialog.run() dialog.destroy() if response == gtk.RESPONSE_YES: if self.fileSave(): gtk.main_quit() elif response == gtk.RESPONSE_NO: gtk.main_quit() else: gtk.main_quit() return True def editCut(self, widget=None, data=None): """ Cut the selected lap timer label to the default clipboard. """ w = self.app.get_focus() if isinstance(w, gtk.TextView): w.get_buffer().cut_clipboard(gtk.clipboard_get() , w.get_editable()) return True def editCopy(self, widget=None, data=None): """ Copy the selected lap timer label or lap time to the default clipboard. """ w = self.app.get_focus() if isinstance(w, gtk.TextView): w.get_buffer().copy_clipboard(gtk.clipboard_get()) elif isinstance(w, gtk.Button): gtk.clipboard_get().set_text(w.get_label()) return True def editCopyTime(self, widget=None, data=None): """ Copy the main timer value to the default clipboard. """ gtk.clipboard_get().set_text(self._time.get_text()) return True def editPaste(self, widget=None, data=None): """ Paste the contents of the default clipboard to the selected lap timer label. """ w = self.app.get_focus() if isinstance(w, gtk.TextView): w.get_buffer().paste_clipboard(gtk.clipboard_get(), None, w.get_editable()) return True def setRefreshRate(self, rate): """ Sets rate at which stopwatch time is updated. If stopwatch is not running, display will still be updated once. """ if self.sw.running: if self.refreshRate: try: gobject.source_remove(self.gtimer) except: pass if rate: self.gtimer = gobject.timeout_add(rate, self.refreshTime) else: self.refreshTime() self.refreshRate = rate def viewFast(self, widget=None, data=None): """ Set the refresh rate to about what the human eye can discern. It looks cool, but isn't really necessary and burns extra cycles. """ self.setRefreshRate(20) return True def viewSlow(self, widget=None, data=None): """ Set the refresh rate to tenths of a second, a good compromise between impressive updating and CPU utilization. """ self.setRefreshRate(100) return True def viewOff(self, widget=None, data=None): """ Disable updating of the timer display. The timer continues to run internally, and is refreshed each time this is called. """ self.setRefreshRate(0) self.refreshTime() return True def toolsToggle(self, widget, data=None): """ If the stopwatch is running, stop it. Otherwise, (re)start it. """ if self.sw.running: self.toolsStop(widget) else: self.toolsStart(widget) def toolsStart(self, widget=None, data=None): """ If not running, start the stopwatch. """ self.dirty = True self.sw.start() if self.refreshRate: self.gtimer = gobject.timeout_add(self.refreshRate, self.refreshTime) self._bStart.set_label("Stop") def toolsStop(self, widget=None, data=None): """ If running, stop the stopwatch. """ if self.sw.running: try: gobject.source_remove(self.gtimer) except: pass self.dirty = True self.sw.stop() self._time.set_text(self.sw.getFormattedTime()) self._bStart.set_label("Start") def toolsReset(self, widget=None, data=None): """ Reset the stopwatch, preserving the lap names. To also clear lap timer names, use fileNew instead. """ self.dirty = True self.toolsStop(widget) self.sw.elapsedTime = 0.0 self.refreshTime() for _lLapName, _bLapValue, lapDefaultName in self.lapTimers: _bLapValue.set_label("") def helpManual(self, widget=None, data=None): """ Show the manual in a local web browser, or (failing that) a text editor. """ try: webbrowser.open("manual.htm") return True except: pass try: webbrowser.open("manual.txt") return True except: return True def helpAbout(self, widget=None, data=None): """ Show the Help->About dialog box. """ dialog = gtk.AboutDialog() dialog.set_name(title) dialog.set_version(version) dialog.set_copyright(copyright) dialog.set_website(website) try: dialog.set_logo(gtk.gdk.pixbuf_new_from_file(logo)) except: pass try: f = open(license) l = string.join(f.readlines(), '\n') dialog.set_license(l) except: pass response = dialog.run() dialog.destroy() def main(self): """ The main program is called to start the user interface. """ gtk.main() if __name__ == "__main__": gtksw = gtkStopwatch() gtksw.main()