Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Schwartz <bens@alum.mit.edu>2008-02-02 22:50:23 (GMT)
committer Benjamin Schwartz <bens@alum.mit.edu>2008-02-02 22:50:23 (GMT)
commit8ff827e0e9e7147e9cc2cc28b1a11d9dce17b098 (patch)
treecf62de583e5cd4150a5709f6cece18af3830154f
Initial import
-rw-r--r--MANIFEST4
-rw-r--r--activity.py152
-rw-r--r--activity/activity-stopwatch.svg83
-rw-r--r--activity/activity.info7
-rw-r--r--check.svg68
-rw-r--r--circle.svg73
-rw-r--r--setup.py21
-rw-r--r--stopwatch.py598
8 files changed, 1006 insertions, 0 deletions
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..93ea086
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,4 @@
+activity.py
+stopwatch.py
+check.svg
+circle.svg
diff --git a/activity.py b/activity.py
new file mode 100644
index 0000000..9789f30
--- /dev/null
+++ b/activity.py
@@ -0,0 +1,152 @@
+# Copyright 2007 Collabora Ltd.
+#
+# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""HelloMesh Activity: A case study for collaboration using Tubes."""
+
+import logging
+import telepathy
+
+from sugar.activity.activity import Activity, ActivityToolbox
+from sugar.presence import presenceservice
+
+from sugar.presence.tubeconn import TubeConnection
+
+import stopwatch
+import gobject
+
+import cPickle
+import gtk.gdk
+
+SERVICE = "org.laptop.StopWatch"
+
+class StopWatchActivity(Activity):
+ """StopWatch Activity as specified in activity.info"""
+ def __init__(self, handle):
+ """Set up the StopWatch activity."""
+ Activity.__init__(self, handle)
+ self._logger = logging.getLogger('stopwatch-activity')
+
+ gobject.threads_init()
+
+ # top toolbar with share and close buttons:
+ toolbox = ActivityToolbox(self)
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ self.model = stopwatch.Model()
+ self.controller = stopwatch.Controller(self.model)
+ self.gui = stopwatch.GUIView(self.model, self.controller)
+
+ self.set_canvas(self.gui.display)
+ self.show_all()
+
+ self.tubehandler = None # Shared session
+ self.initiating = False
+
+ # get the Presence Service
+ self.pservice = presenceservice.get_instance()
+ # Buddy object for you
+ owner = self.pservice.get_owner()
+ self.owner = owner
+
+ self.connect('shared', self._shared_cb)
+ self.connect('joined', self._joined_cb)
+
+ self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self.connect("visibility-notify-event", self._visible_cb)
+ self.connect("notify::active", self._active_cb)
+
+
+ def _shared_cb(self, activity):
+ self._logger.debug('My activity was shared')
+ self.initiating = True
+ self._sharing_setup()
+
+ self._logger.debug('This is my activity: making a tube...')
+ id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(
+ SERVICE, {})
+
+ def _sharing_setup(self):
+ if self._shared_activity is None:
+ self._logger.error('Failed to share or join activity')
+ return
+
+ self.conn = self._shared_activity.telepathy_conn
+ self.tubes_chan = self._shared_activity.telepathy_tubes_chan
+ self.text_chan = self._shared_activity.telepathy_text_chan
+
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube',
+ self._new_tube_cb)
+
+ def _list_tubes_reply_cb(self, tubes):
+ for tube_info in tubes:
+ self._new_tube_cb(*tube_info)
+
+ def _list_tubes_error_cb(self, e):
+ self._logger.error('ListTubes() failed: %s', e)
+
+ def _joined_cb(self, activity):
+ if not self._shared_activity:
+ return
+
+ self._logger.debug('Joined an existing shared activity')
+ self.initiating = False
+ self._sharing_setup()
+
+ self._logger.debug('This is not my activity: waiting for a tube...')
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=self._list_tubes_reply_cb,
+ error_handler=self._list_tubes_error_cb)
+
+ def _new_tube_cb(self, id, initiator, type, service, params, state):
+ self._logger.debug('New tube: ID=%d initator=%d type=%d service=%s '
+ 'params=%r state=%d', id, initiator, type, service,
+ params, state)
+ if (type == telepathy.TUBE_TYPE_DBUS and
+ service == SERVICE):
+ if state == telepathy.TUBE_STATE_LOCAL_PENDING:
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id)
+ tube_conn = TubeConnection(self.conn,
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
+ id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
+ self.tubehandler = stopwatch.TubeHandler(tube_conn, self.initiating,
+ self.model, self.controller)
+
+ def read_file(self, file_path):
+ f = open(file_path, 'r')
+ q = cPickle.load(f)
+ f.close()
+ self.model.reset(q)
+
+ def write_file(self, file_path):
+ q = self.model.get_all()
+ f = open(file_path, 'w')
+ cPickle.dump(q, f)
+ f.close()
+
+ def _active_cb(self, widget, event):
+ self._logger.debug("_active_cb")
+ if self.props.active:
+ self.gui.resume()
+ else:
+ self.gui.pause()
+
+ def _visible_cb(self, widget, event):
+ self._logger.debug("_visible_cb")
+ if event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED:
+ self.gui.pause()
+ else:
+ self.gui.resume()
diff --git a/activity/activity-stopwatch.svg b/activity/activity-stopwatch.svg
new file mode 100644
index 0000000..06c4830
--- /dev/null
+++ b/activity/activity-stopwatch.svg
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#9999FF">
+ <!ENTITY stroke_color "#5555FF">
+]>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ id="Icon"
+ width="45"
+ height="45"
+ viewBox="0 0 48.92 43.846"
+ overflow="visible"
+ enable-background="new 0 0 48.92 43.846"
+ xml:space="preserve"
+ sodipodi:version="0.32"
+ inkscape:version="0.45.1"
+ sodipodi:docname="activity-stopwatch.svg"
+ sodipodi:docbase="/home/bens/olpc3d/stopwatch/activity"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"><metadata
+ id="metadata2238"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs2236" /><sodipodi:namedview
+ inkscape:window-height="816"
+ inkscape:window-width="1440"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ inkscape:zoom="9.2824886"
+ inkscape:cx="23.598161"
+ inkscape:cy="22.892568"
+ inkscape:window-x="0"
+ inkscape:window-y="29"
+ inkscape:current-layer="Icon"
+ height="45px"
+ width="45px" />
+
+
+
+
+
+
+
+
+<path
+ sodipodi:type="arc"
+ style="fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:3.02743053;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path2186"
+ sodipodi:cx="21.976866"
+ sodipodi:cy="21.815269"
+ sodipodi:rx="19.606812"
+ sodipodi:ry="19.768406"
+ d="M 41.583677 21.815269 A 19.606812 19.768406 0 1 1 2.3700542,21.815269 A 19.606812 19.768406 0 1 1 41.583677 21.815269 z"
+ transform="matrix(0.9950145,0,0,0.9868809,2.0496201,0.5546025)" /><path
+ style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:&stroke_color;;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 8.6536187,4.8733103 L 5.7224121,8.1536565"
+ id="path2188"
+ sodipodi:nodetypes="cc" /><path
+ style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:&stroke_color;;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 23.700541,22.623243 L 37.640481,32.017563"
+ id="path4130"
+ sodipodi:nodetypes="cc" /><path
+ sodipodi:nodetypes="cc"
+ id="path2211"
+ d="M 39.919755,5.4588813 L 42.850961,8.7392275"
+ style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:&stroke_color;;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
+ style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:&stroke_color;;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 19.307658,-0.39682809 L 29.265715,-0.39567913"
+ id="path2213"
+ sodipodi:nodetypes="cc" /></svg>
diff --git a/activity/activity.info b/activity/activity.info
new file mode 100644
index 0000000..3844b8a
--- /dev/null
+++ b/activity/activity.info
@@ -0,0 +1,7 @@
+[Activity]
+name = StopWatch
+service_name = org.laptop.StopWatchActivity
+class = activity.StopWatchActivity
+icon = activity-stopwatch
+activity_version = 1
+show_launcher = yes
diff --git a/check.svg b/check.svg
new file mode 100644
index 0000000..97d29b1
--- /dev/null
+++ b/check.svg
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2211"
+ sodipodi:version="0.32"
+ inkscape:version="0.45.1"
+ width="29"
+ height="29"
+ version="1.0"
+ sodipodi:docbase="/home/bens/olpc3d/stopwatch"
+ sodipodi:docname="check.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape">
+ <metadata
+ id="metadata2216">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs2214" />
+ <sodipodi:namedview
+ inkscape:window-height="631"
+ inkscape:window-width="872"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ width="29px"
+ height="29px"
+ inkscape:zoom="12.996039"
+ inkscape:cx="4.6425105"
+ inkscape:cy="13.662969"
+ inkscape:window-x="5"
+ inkscape:window-y="79"
+ inkscape:current-layer="svg2211" />
+ <path
+ sodipodi:type="arc"
+ style="fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path2220"
+ sodipodi:cx="150.1626"
+ sodipodi:cy="6.6028275"
+ sodipodi:rx="10.306555"
+ sodipodi:ry="10.437018"
+ d="M 160.46915 6.6028275 A 10.306555 10.437018 0 1 1 139.85604,6.6028275 A 10.306555 10.437018 0 1 1 160.46915 6.6028275 z"
+ transform="matrix(1.3986429,0,0,1.3811596,-195.46347,5.3708786)" />
+ <path
+ style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3.60000014;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 6.5725621,15.041851 L 12.443397,20.912674 L 21.836711,9.9538065"
+ id="path3194"
+ sodipodi:nodetypes="ccc" />
+</svg>
diff --git a/circle.svg b/circle.svg
new file mode 100644
index 0000000..5e400cc
--- /dev/null
+++ b/circle.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2211"
+ sodipodi:version="0.32"
+ inkscape:version="0.45.1"
+ width="29"
+ height="29"
+ version="1.0"
+ sodipodi:docbase="/home/bens/olpc3d/stopwatch"
+ sodipodi:docname="circle.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape">
+ <metadata
+ id="metadata2216">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs2214" />
+ <sodipodi:namedview
+ inkscape:window-height="631"
+ inkscape:window-width="872"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ width="29px"
+ height="29px"
+ inkscape:zoom="18.379174"
+ inkscape:cx="11.09891"
+ inkscape:cy="10.470871"
+ inkscape:window-x="5"
+ inkscape:window-y="79"
+ inkscape:current-layer="svg2211" />
+ <path
+ sodipodi:type="arc"
+ style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path2220"
+ sodipodi:cx="150.1626"
+ sodipodi:cy="6.6028275"
+ sodipodi:rx="10.306555"
+ sodipodi:ry="10.437018"
+ d="M 160.46915 6.6028275 A 10.306555 10.437018 0 1 1 139.85604,6.6028275 A 10.306555 10.437018 0 1 1 160.46915 6.6028275 z"
+ transform="matrix(1.3986429,0,0,1.3811596,-195.48601,5.3934157)" />
+ <path
+ sodipodi:type="arc"
+ style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.4000001;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path5194"
+ sodipodi:cx="9.6304655"
+ sodipodi:cy="9.3357553"
+ sodipodi:rx="7.2908611"
+ sodipodi:ry="6.8555856"
+ d="M 16.921327 9.3357553 A 7.2908611 6.8555856 0 1 1 2.3396044,9.3357553 A 7.2908611 6.8555856 0 1 1 16.921327 9.3357553 z"
+ transform="matrix(1.2311847,0,0,1.3093554,2.632454,2.4726037)" />
+</svg>
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..39112ee
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2006, Red Hat, Inc.
+#
+# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from sugar.activity import bundlebuilder
+
+bundlebuilder.start('StopWatchActivity')
diff --git a/stopwatch.py b/stopwatch.py
new file mode 100644
index 0000000..4526653
--- /dev/null
+++ b/stopwatch.py
@@ -0,0 +1,598 @@
+# Copyright 2007 Benjamin M. Schwartz
+#
+# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import gtk.gdk
+import gobject
+import dbus
+import dbus.service
+import dbus.gobject_service
+import logging
+import time
+import thread
+import threading
+import cPickle
+import sets
+import bisect
+import locale
+import pango
+from gettext import gettext
+
+IFACE = "org.laptop.StopWatch"
+PATH = "/org/laptop/StopWatch"
+
+class TubeHandler(dbus.gobject_service.ExportedGObject):
+ def __init__(self, tube_conn, initiating, model, controller):
+ dbus.gobject_service.ExportedGObject.__init__(self, tube_conn, PATH)
+ self._logger = logging.getLogger('stopwatch.TubeHandler')
+ self.tube = tube_conn
+ self.is_initiator = initiating
+ self.model = model
+ self.controller = controller
+
+ self._know_offset = self.is_initiator
+ self._offset_lock = threading.Lock()
+ self._history_lock = threading.Lock()
+
+ self.tube.add_signal_receiver(self.tell_time, signal_name='What_time_is_it', dbus_interface=IFACE, sender_keyword='sender')
+ self.tube.add_signal_receiver(self.tell_history, signal_name='What_has_happened', dbus_interface=IFACE, sender_keyword='sender')
+ self.tube.add_signal_receiver(self.tell_names, signal_name='What_are_the_names', dbus_interface=IFACE, sender_keyword='sender')
+ self.tube.add_signal_receiver(self.receive_history, signal_name='event_broadcast', dbus_interface=IFACE, byte_arrays=True)
+ self.tube.add_signal_receiver(self.receive_name, signal_name='name_change_broadcast', dbus_interface=IFACE, sender_keyword='sender', utf8_strings=True)
+
+ self.controller.register_time_listener(self.event_listener)
+ self.controller.register_name_listener(self.name_listener)
+
+ if not self._know_offset:
+ self.ask_time()
+
+ if not self.is_initiator:
+ self.ask_history()
+ self.ask_names()
+
+ @dbus.service.signal(dbus_interface=IFACE, signature='d')
+ def What_time_is_it(self, asktime):
+ return
+
+ def ask_time(self):
+ self.What_time_is_it(time.time())
+
+ def tell_time(self, asktime, sender=None):
+ start_time = time.time()
+ if sender == self.tube.get_unique_name():
+ return
+ if self._know_offset:
+ remote = self.tube.get_object(sender, PATH)
+ offset = self.model.get_offset()
+ start_time += offset
+ remote.receive_time(asktime, start_time, time.time() + offset)
+
+ @dbus.service.method(dbus_interface=IFACE, in_signature='ddd', out_signature='')
+ def receive_time(self, asktime, start_time, finish_time):
+ rtime = time.time()
+ thread.start_new_thread(self._handle_incoming_time, (asktime, start_time, finish_time, rtime))
+
+ def _handle_incoming_time(self, ask, start, finish, receive):
+ self._offset_lock.acquire()
+ if not self._know_offset:
+ offset = ((start + finish)/2) - ((ask + receive)/2)
+ self.model.set_offset(offset)
+ self._know_offset = True
+ self._offset_lock.release()
+
+ @dbus.service.signal(dbus_interface=IFACE, signature='')
+ def What_has_happened(self):
+ return
+
+ def ask_history(self):
+ self.What_has_happened()
+
+ def tell_history(self, sender=None):
+ if sender == self.tube.get_unique_name():
+ return
+ remote = self.tube.get_object(sender, PATH)
+ h = self.model.get_history()
+ remote.receive_history(cPickle.dumps(h))
+
+ @dbus.service.method(dbus_interface=IFACE, in_signature='ay', out_signature='', byte_arrays=True, utf8_strings=True)
+ def receive_history(self, hist_string):
+ thread.start_new_thread(self._handle_incoming_history, (hist_string,))
+
+ def _handle_incoming_history(self, hist_string):
+ self._history_lock.acquire()
+ h = cPickle.loads(hist_string)
+ self.model.add_history(h)
+ self._history_lock.release()
+
+ @dbus.service.signal(dbus_interface=IFACE, signature='')
+ def What_are_the_names(self):
+ return
+
+ def ask_names(self):
+ self._logger.debug("ask_names")
+ self.What_are_the_names()
+
+ def tell_names(self, sender=None):
+ self._logger.debug("tell_names")
+ if sender == self.tube.get_unique_name():
+ return
+ remote = self.tube.get_object(sender, PATH)
+ n = self.model.get_all_names()
+ remote.receive_all_names(n)
+
+ @dbus.service.method(dbus_interface=IFACE, in_signature='(asad)', out_signature='', utf8_strings=True)
+ def receive_all_names(self, names_and_times):
+ self._logger.debug("receive_names")
+ thread.start_new_thread(self._handle_incoming_names, (names_and_times,))
+
+ def _handle_incoming_names(self, names):
+ self._logger.debug("_handle_incoming_names")
+ self.model.set_all_names(names)
+
+ @dbus.service.signal(dbus_interface=IFACE, signature='ay')
+ def event_broadcast(self, hist_string):
+ return
+
+ def event_listener(self, h):
+ self.event_broadcast(cPickle.dumps(h))
+
+ @dbus.service.signal(dbus_interface=IFACE, signature='isd')
+ def name_change_broadcast(self, i, name, t):
+ self._logger.debug("name_change_broadcast")
+ return
+
+ def name_listener(self, i, name, t):
+ self._logger.debug("name_listener")
+ self.name_change_broadcast(i, name, t)
+ return
+
+ def receive_name(self, i, name, t, sender=None):
+ self._logger.debug("receive_name " + name)
+ if sender != self.tube.get_unique_name():
+ self.model.set_name(i, name, t)
+
+class WatchEvent():
+ RUN_EVENT = 1
+ PAUSE_EVENT = 2
+ RESET_EVENT = 3
+ def __init__(self, event_time, event_type, watch_id):
+ self._event_time = event_time
+ self._event_type = event_type
+ self._watch_id = watch_id
+
+ def get_time(self):
+ return self._event_time
+
+ def get_type(self):
+ return self._event_type
+
+ def get_watch(self):
+ return self._watch_id
+
+ def _tuple(self):
+ return (self._event_time, self._event_type, self._watch_id)
+
+ def __cmp__(self, other):
+ return cmp(self._tuple(), other)
+
+ def __hash__(self):
+ return hash(self._tuple())
+
+
+class Model():
+ NUM_WATCHES = 10
+
+ STATE_PAUSED = 1
+ STATE_RUNNING = 2
+
+ def __init__(self):
+ self._logger = logging.getLogger('stopwatch.Model')
+ self._known_events = sets.Set()
+ self._history = [[] for i in xrange(Model.NUM_WATCHES)]
+ self._history_lock = threading.RLock()
+
+ self._offset = 0.0
+
+ self._init_state = [(Model.STATE_PAUSED, 0) for i in xrange(Model.NUM_WATCHES)]
+ self._state = [(Model.STATE_PAUSED, 0) for i in xrange(Model.NUM_WATCHES)]
+ self._names = [gettext("Stopwatch") + " " + locale.str(i+1) for i in xrange(Model.NUM_WATCHES)]
+ self._name_times = [float('-inf')] * Model.NUM_WATCHES
+ self._name_lock = threading.RLock()
+
+ self._time_listeners = [[] for i in xrange(Model.NUM_WATCHES)]
+ self._name_listeners = [[] for i in xrange(Model.NUM_WATCHES)]
+
+ def get_all(self):
+ return (self.get_all_names(), self._state, self._offset)
+
+ def reset(self, trio):
+ self._history_lock.acquire()
+ self.set_all_names(trio[0])
+ self._init_state = trio[1]
+ self._offset = trio[2]
+ self._history = [[] for i in xrange(Model.NUM_WATCHES)]
+ self._state = [() for i in xrange(Model.NUM_WATCHES)]
+ for i in xrange(Model.NUM_WATCHES):
+ self._update_state(i)
+ self._history_lock.release()
+
+ def get_offset(self):
+ return self._offset
+
+ def set_offset(self, x):
+ self._offset = x
+ for i in xrange(Model.NUM_WATCHES):
+ self._trigger(i)
+
+ def get_history(self):
+ return self._history
+
+ def get_name(self, i):
+ return self._names[i]
+
+ def set_name(self, i, name, t):
+ self._logger.debug("set_name" + str(i) + " " + name)
+ if self.set_name_silent(i, name, t):
+ self._name_trigger(i)
+
+ def set_name_silent(self, i, name, t):
+ self._logger.debug("set_name_silent" + str(i) + " " + name)
+ self._name_lock.acquire()
+ if self._name_times[i] <= t:
+ self._names[i] = str(name)
+ self._name_times[i] = float(t)
+ self._name_lock.release()
+ return True
+ else:
+ self._name_lock.release()
+ return False
+
+ def get_all_names(self):
+ return (self._names, self._name_times)
+
+ def set_all_names(self, n):
+ for i in xrange(Model.NUM_WATCHES):
+ self.set_name(i, n[0][i], n[1][i])
+
+ def add_history(self, h):
+ self._logger.debug("add_history")
+ assert len(h) == Model.NUM_WATCHES
+ self._history_lock.acquire()
+ for i in xrange(Model.NUM_WATCHES):
+ w = h[i]
+ changed = False
+ for ev in w:
+ if ev not in self._known_events:
+ self._known_events.add(ev)
+ bisect.insort(self._history[i], ev)
+ changed = True
+ if changed:
+ self._update_state(i)
+ self._history_lock.release()
+
+ def _update_state(self, i):
+ self._logger.debug("_update_state")
+ w = self._history[i]
+ L = len(w)
+ s = self._init_state[i][0]
+ timeval = self._init_state[i][1]
+ #state machine
+ for ev in w:
+ event_type = ev.get_type()
+ if s == Model.STATE_PAUSED:
+ if event_type == WatchEvent.RUN_EVENT:
+ s = Model.STATE_RUNNING
+ timeval = ev.get_time() - timeval
+ elif event_type == WatchEvent.RESET_EVENT:
+ timeval = 0
+ elif s == Model.STATE_RUNNING:
+ if event_type == WatchEvent.RESET_EVENT:
+ timeval = ev.get_time()
+ elif event_type == WatchEvent.PAUSE_EVENT:
+ s = Model.STATE_PAUSED
+ timeval = ev.get_time() - timeval
+
+ self._set_state(i, (s, timeval))
+
+ def _set_state(self, i, q):
+ self._logger.debug("_set_state")
+ if self._state[i] != q:
+ self._state[i] = q
+ self._trigger(i)
+
+ def _name_trigger(self, i):
+ self._logger.debug("_name_trigger")
+ for l in self._name_listeners[i]:
+ thread.start_new_thread(l, (self._names[i],))
+
+ def _trigger(self, i):
+ self._logger.debug("_trigger")
+ for l in self._time_listeners[i]:
+ thread.start_new_thread(l, (self._state[i],))
+
+ def register_time_listener(self, i, l):
+ self._logger.debug("register_time_listener " + str(i) + " " + str(l))
+ self._time_listeners[i].append(l)
+ self._logger.debug(str(self._time_listeners))
+
+ def register_name_listener(self, i, l):
+ self._logger.debug("register_name_listener " + str(i) + " " + str(l))
+ self._name_listeners[i].append(l)
+ self._logger.debug(str(self._name_listeners))
+
+class Controller():
+ def __init__(self, model):
+ self._logger = logging.getLogger('stopwatch.Controller')
+ self._model = model
+ self._time_listeners = [self._model.add_history]
+ self._name_listeners = [self._model.set_name_silent]
+
+ def register_time_listener(self, l):
+ self._time_listeners.append(l)
+
+ def register_name_listener(self, l):
+ self._name_listeners.append(l)
+
+ def _trigger(self, h):
+ self._logger.debug("_trigger")
+ for l in self._time_listeners:
+ thread.start_new_thread(l, (h,))
+
+ def _do_event(self, i, time, event_type):
+ ev = WatchEvent(time, event_type, i)
+ h = [[] for k in xrange(Model.NUM_WATCHES)]
+ h[i].append(ev)
+ self._trigger(h)
+
+ def run(self, i, time):
+ self._logger.debug("run "+ str(time))
+ self._do_event(i, time, WatchEvent.RUN_EVENT)
+
+ def pause(self, i, time):
+ self._logger.debug("pause "+ str(time))
+ self._do_event(i, time, WatchEvent.PAUSE_EVENT)
+
+ def reset(self, i, time):
+ self._logger.debug("reset "+ str(time))
+ self._do_event(i, time, WatchEvent.RESET_EVENT)
+
+ def set_name(self, i, name, t):
+ self._logger.debug("set_name "+ name)
+ for f in self._name_listeners:
+ thread.start_new_thread(f, (i, name, t))
+
+class OneWatchView():
+ def __init__(self, watch_id, model, controller):
+ self._logger = logging.getLogger('stopwatch.OneWatchView'+str(watch_id))
+ self._watch_id = watch_id
+ self._model = model
+ self._controller = controller
+
+ self._state = Model.STATE_PAUSED
+ self._timeval = 0
+
+ self._model.register_time_listener(self._watch_id, self.update_state)
+ self._offset = self._model.get_offset()
+
+ self._name = gtk.Entry()
+ self._name.set_text(self._model.get_name(self._watch_id))
+ self._name_changed_handler = self._name.connect('changed', self._name_cb)
+ self._model.register_name_listener(self._watch_id, self.update_name)
+ self._name_lock = threading.Lock()
+
+ check = gtk.Image()
+ check.set_from_file('check.svg')
+ self._run_button = gtk.ToggleButton(gettext("Start/Stop"))
+ self._run_button.set_image(check)
+ self._run_button.props.focus_on_click = False
+ self._run_handler = self._run_button.connect('clicked', self._run_cb)
+ self._run_button_lock = threading.Lock()
+
+ circle = gtk.Image()
+ circle.set_from_file('circle.svg')
+ self._reset_button = gtk.Button(gettext("Zero"))
+ self._reset_button.set_image(circle)
+ self._reset_button.props.focus_on_click = False
+ self._reset_button.connect('clicked', self._reset_cb)
+
+ timefont = pango.FontDescription()
+ timefont.set_family("monospace")
+ timefont.set_size(pango.SCALE*14)
+ self._time_label = gtk.Label(self._format(0))
+ self._time_label.modify_font(timefont)
+ self._time_label.set_single_line_mode(True)
+ self._time_label.set_selectable(True)
+ self._time_label.set_width_chars(10)
+ self._time_label.set_alignment(1,0.5) #justify right
+ self._time_label.set_padding(6,0)
+ eb = gtk.EventBox()
+ eb.add(self._time_label)
+ eb.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("white"))
+
+ self._should_update = threading.Event()
+ self._is_visible = threading.Event()
+ self._is_visible.set()
+ self._update_lock = threading.Lock()
+ self._label_lock = threading.Lock()
+
+ self.box = gtk.HBox()
+ self.box.pack_start(self._name, padding=6)
+ self.box.pack_start(self._run_button, expand=False)
+ self.box.pack_start(self._reset_button, expand=False)
+ self.box.pack_end(eb, expand=False, padding=6)
+
+ filler = gtk.VBox()
+ filler.pack_start(self.box, expand=True, fill=False)
+
+ self.backbox = gtk.EventBox()
+ self.backbox.add(filler)
+ self._black = gtk.gdk.color_parse("black")
+ self._gray = gtk.gdk.Color(256*192, 256*192, 256*192)
+
+ self.display = gtk.EventBox()
+ self.display.add(self.backbox)
+ #self.display.set_above_child(True)
+ self.display.props.can_focus = True
+ self.display.connect('focus-in-event', self._got_focus_cb)
+ self.display.connect('focus-out-event', self._lost_focus_cb)
+ self.display.add_events(gtk.gdk.ALL_EVENTS_MASK)
+ self.display.connect('key-press-event', self._keypress_cb)
+ #self.display.connect('key-release-event', self._keyrelease_cb)
+
+ thread.start_new_thread(self._start_running, ())
+
+ def update_state(self, q):
+ self._logger.debug("update_state: "+str(q))
+ self._update_lock.acquire()
+ self._logger.debug("acquired update_lock")
+ self._state = q[0]
+ self._offset = self._model.get_offset()
+ if self._state == Model.STATE_RUNNING:
+ self._timeval = q[1]
+ self._set_run_button_active(True)
+ self._should_update.set()
+ else:
+ self._set_run_button_active(False)
+ self._should_update.clear()
+ self._label_lock.acquire()
+ self._timeval = q[1]
+ ev = threading.Event()
+ gobject.idle_add(self._update_label, self._format(self._timeval), ev)
+ ev.wait()
+ self._label_lock.release()
+ self._update_lock.release()
+
+ def update_name(self, name):
+ self._name_lock.acquire()
+ self._name.set_editable(False)
+ self._name.handler_block(self._name_changed_handler)
+ ev = threading.Event()
+ gobject.idle_add(self._set_name, name, ev)
+ ev.wait()
+ self._name.handler_unblock(self._name_changed_handler)
+ self._name.set_editable(True)
+ self._name_lock.release()
+
+ def _set_name(self, name, ev):
+ self._name.set_text(name)
+ ev.set()
+ return False
+
+ def _format(self, t):
+ return locale.format('%.2f',t)
+
+ def _update_label(self, string, ev):
+ self._time_label.set_text(string)
+ ev.set()
+ return False
+
+ def _start_running(self):
+ self._logger.debug("_start_running")
+ ev = threading.Event()
+ while True:
+ self._should_update.wait()
+ self._is_visible.wait()
+ self._label_lock.acquire()
+ if self._should_update.isSet() and self._is_visible.isSet():
+ s = self._format(time.time() + self._offset - self._timeval)
+ ev.clear()
+ gobject.idle_add(self._update_label, s, ev)
+ ev.wait()
+ time.sleep(0.07)
+ self._label_lock.release()
+
+ def _run_cb(self, widget):
+ t = time.time()
+ self._logger.debug("run button pressed: " + str(t))
+ if self._run_button.get_active(): #button has _just_ been set active
+ self._controller.run(self._watch_id, self._offset + t)
+ else:
+ self._controller.pause(self._watch_id, self._offset + t)
+ return True
+
+ def _set_run_button_active(self, v):
+ self._run_button_lock.acquire()
+ self._run_button.handler_block(self._run_handler)
+ self._run_button.set_active(v)
+ self._run_button.handler_unblock(self._run_handler)
+ self._run_button_lock.release()
+
+ def _reset_cb(self, widget):
+ t = time.time()
+ self._logger.debug("reset button pressed: " + str(t))
+ self._controller.reset(self._watch_id, self._offset + t)
+ return True
+
+ def _name_cb(self, widget):
+ t = time.time()
+ self._controller.set_name(self._watch_id, widget.get_text(), self._offset + t)
+ return True
+
+ def pause(self):
+ self._logger.debug("pause")
+ self._is_visible.clear()
+
+ def resume(self):
+ self._logger.debug("resume")
+ self._is_visible.set()
+
+ def _got_focus_cb(self, widget, event):
+ self._logger.debug("got focus")
+ self.backbox.modify_bg(gtk.STATE_NORMAL, self._black)
+ self._name.modify_bg(gtk.STATE_NORMAL, self._black)
+ return True
+
+ def _lost_focus_cb(self, widget, event):
+ self._logger.debug("lost focus")
+ self.backbox.modify_bg(gtk.STATE_NORMAL, self._gray)
+ self._name.modify_bg(gtk.STATE_NORMAL, self._gray)
+ return True
+
+
+ # KP_End == check gamekey = 65436
+ # KP_Page_Down == X gamekey = 65435
+ # KP_Home == box gamekey = 65429
+ # KP_Page_Up == O gamekey = 65434
+ def _keypress_cb(self, widget, event):
+ self._logger.debug("key press: " + gtk.gdk.keyval_name(event.keyval)+ " " + str(event.keyval))
+ if event.keyval == 65436:
+ self._run_button.clicked()
+ elif event.keyval == 65434:
+ self._reset_button.clicked()
+ return False
+
+class GUIView():
+ def __init__(self, model, controller):
+ self._watches = [OneWatchView(i, model, controller) for i in xrange(Model.NUM_WATCHES)]
+ self.display = gtk.VBox()
+ for x in self._watches:
+ self.display.pack_start(x.display, expand=True, fill=True)
+
+ self._pause_lock = threading.Lock()
+
+ def pause(self):
+ self._pause_lock.acquire()
+ for w in self._watches:
+ w.pause()
+ self._pause_lock.release()
+
+ def resume(self):
+ self._pause_lock.acquire()
+ for w in self._watches:
+ w.resume()
+ self._pause_lock.release()
+
+