diff options
author | Benjamin 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) |
commit | 8ff827e0e9e7147e9cc2cc28b1a11d9dce17b098 (patch) | |
tree | cf62de583e5cd4150a5709f6cece18af3830154f |
Initial import
-rw-r--r-- | MANIFEST | 4 | ||||
-rw-r--r-- | activity.py | 152 | ||||
-rw-r--r-- | activity/activity-stopwatch.svg | 83 | ||||
-rw-r--r-- | activity/activity.info | 7 | ||||
-rw-r--r-- | check.svg | 68 | ||||
-rw-r--r-- | circle.svg | 73 | ||||
-rw-r--r-- | setup.py | 21 | ||||
-rw-r--r-- | stopwatch.py | 598 |
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() + + |