Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVincent Vinet <vince.vinet@gmail.com>2009-02-28 01:54:39 (GMT)
committer Vincent Vinet <vince.vinet@gmail.com>2009-02-28 01:54:39 (GMT)
commit7019bbb21cf345a25afe87c148e5651aef3dd5c2 (patch)
tree295550aaa76816c60266619a4f5747969a566fec
parent938cdfc307b359d3c4cc528acd5d412aae8b2a80 (diff)
-Replace the current event classes to use EVentFilters and
callbacks onto the Tutorial claa -Wow this is an awesome commit, seriously!
-rw-r--r--src/sugar/tutorius/Makefile.am4
-rw-r--r--src/sugar/tutorius/actions.py4
-rw-r--r--src/sugar/tutorius/core.py237
-rw-r--r--src/sugar/tutorius/filters.py162
-rw-r--r--src/sugar/tutorius/gtkutils.py166
5 files changed, 373 insertions, 200 deletions
diff --git a/src/sugar/tutorius/Makefile.am b/src/sugar/tutorius/Makefile.am
index 8389349..d6ce0f1 100644
--- a/src/sugar/tutorius/Makefile.am
+++ b/src/sugar/tutorius/Makefile.am
@@ -3,4 +3,6 @@ sugar_PYTHON = \
__init__.py \
core.py \
dialog.py \
- actions.py
+ actions.py \
+ gtkutils.py \
+ filters.py
diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py
index b48cb8b..58a4216 100644
--- a/src/sugar/tutorius/actions.py
+++ b/src/sugar/tutorius/actions.py
@@ -58,7 +58,7 @@ class DoOnceMixin(object):
class fails at __init__ ..... mixins... right....narwhals!
"""
def __init__(self ):
- super(DoOnceMixin, self).__init
+ super(DoOnceMixin, self).__init__()
self._called = False
self._need_undo = False
@@ -107,7 +107,7 @@ class OnceWrapper(object):
class DialogMessage(Action):
"""Show a dialog!"""
def __init__(self, message):
- super(DialogMessage, self).__init__(self)
+ super(DialogMessage, self).__init__()
self._message = message
self._dialog = None
diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py
index 62caee0..45eee3a 100644
--- a/src/sugar/tutorius/core.py
+++ b/src/sugar/tutorius/core.py
@@ -25,63 +25,14 @@ import gtk
import logging
from sugar.tutorius.dialog import TutoriusDialog
-
+from sugar.tutorius.gtkutils import find_widget
logger = logging.getLogger("tutorius")
-class Event:
- """Event descriptor class.
- This class is used to describe events that are expected to happen.
- """
-
- def __init__(self, object_name, event_name ):
- """Constructor for Event
- @param object_name str name of the object that will send the event
- @param event_name str name of the event
-
- Example:
- evt=Event("0.0.1.1.2", "clicked")
- """
- self.object_name = object_name
- self.event_name = event_name
-
- def test(self, sig, name):
- """Utility method for testing the equality between a signal and object
- names and their expected values.
- @param sig str signal name
- @param name str object name
- @return True if both match the expected values, False otherwise
- """
- if self.object_name == name and self.event_name == sig:
- return True
- return False
-
-
-
-
class Tutorial (object):
"""
Tutorial Class, used to run through the FSM.
"""
- EVENTS = [
- "focus",
- "button-press-event",
- "enter-notify-event",
- "leave-notify-event",
- "key-press-event",
- "text-selected",
- "clicked",
- ]
-
- IGNORED_WIDGETS = [
- "GtkVBox",
- "GtkHBox",
- "GtkAlignment",
- "GtkNotebook",
- "GtkButton",
- "GtkToolItem",
- "GtkToolbar",
- ]
def __init__(self, name, fsm):
"""Create an unattached tutorial
@@ -107,34 +58,17 @@ class Tutorial (object):
def detach(self):
"""Detach from the current activity"""
- self.disconnect_handlers()
- self.activity = None
-
- def handle_event(self, *args):
- """Default event handler for the Tutorial.
- Tests the received object and signal names onto each defined
- transition and changes to the next state if successful.
-
- The last parameter should be a two-tuple containing the
- (signal_name, object_name)
- """
- sig, objname = args[-1]
- logger.debug("EVENT %s ON %s" % (sig, objname) )
- for transition, next in self.state_machine[self.state]["Events"]:
- if transition.test(sig, objname):
- self.set_state(next)
+ #Remove handlers
+ for eventfilter in self.state_machine.get(self.state,{}).get("EventFilters",()):
+ eventfilter.remove_handlers()
-# @staticmethod
-# def logEvent(obj, *args):
-# logger.debug("%s" % str(args[-1]))
-
- def disconnect_handlers(self):
- """Disconnect all event handlers attached by self"""
- #Loop through handlers
- for obj, hid in self.handlers:
- obj.handler_disconnect(hid)
- self.handlers = []
+ #Undo actions
+ for act in self.state_machine.get(self.state,{}).get("Actions",()):
+ act.undo()
+ #FIXME There should be some amount of resetting done here...
+ self.activity = None
+
def set_state(self, name):
"""Switch to a new state"""
@@ -142,8 +76,10 @@ class Tutorial (object):
return
logger.debug("====NEW STATE: %s====" % name)
- #Remove handlers (TODO replace by EventFilter unregister)
- self.disconnect_handlers()
+ #Remove handlers
+ for eventfilter in self.state_machine.get(self.state,{}).get("EventFilters",()):
+ eventfilter.remove_handlers()
+
#Undo actions
for act in self.state_machine.get(self.state,{}).get("Actions",()):
act.undo()
@@ -151,131 +87,38 @@ class Tutorial (object):
#Switch to new state
self.state = name
newstate = self.state_machine.get(name)
- #Add handlers (TODO replace by EventFilter register)
- for event, unused in newstate["Events"]:
- self.register_signal(self.handle_event, \
- event.object_name, event.event_name)
-
+
+ #Register handlers for eventfilters
+ for eventfilter in newstate["EventFilters"]:
+ eventfilter.install_handlers(self._eventfilter_state_done,
+ activity=self.activity)
+
#Do actions
for act in newstate.get("Actions",()):
act.do()
- def register_signals(self, target, handler, prefix=None, max_depth=None):
- """
- Recursive function to register event handlers on an target
- and it's children. The event handler is called with an extra
- argument which is a two-tuple containing the signal name and
- the FQDN-style name of the target that triggered the event.
-
- This function registers all of the events listed in
- Tutorial.EVENTS and omits widgets with a name matching
- Tutorial.IGNORED_WIDGETS from the name hierarchy.
+ def _eventfilter_state_done(self, eventfilter):
+ """Callback handler for eventfilter to notify
+ when we must go to the next state."""
+ #XXX Tests should be run here normally
- Example arg tuple added:
- ("focus", "Activity.Toolbox.Bold")
- Side effects:
- -Handlers connected on the various targets
- -Handler ID's stored in self.handlers
+ #Swith to the next state pointed by the eventfilter
+ self.set_state(eventfilter.get_next_state())
- @param target the target to recurse on
- @param handler the handler function to connect
- @param prefix name prepended to the target name to form a chain
- @param max_depth maximum recursion depth, None for infinity
- """
- #Gtk Containers have a get_children() function
- if hasattr(target, "get_children") and \
- hasattr(target.get_children, "__call__"):
- for child in target.get_children():
- if max_depth is None or max_depth > 0:
- #Recurse with a prefix on all children
- pre = ".".join( \
- [p for p in (prefix, target.get_name()) \
- if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \
- )
- self.register_signals(child, handler, pre, max_depth-1)
- name = ".".join( \
- [p for p in (prefix, target.get_name()) \
- if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \
- )
- #register events on the target if a widget XXX necessary to check this?
- if isinstance(target, gtk.Widget):
- for sig in Tutorial.EVENTS:
- try:
- self.handlers.append( \
- (target, target.connect(sig, handler, (sig, name) )) \
- )
- except TypeError:
- continue
-
- def register_signals_numbered(self, \
- target, handler, prefix="0", max_depth=None):
- """
- Recursive function to register event handlers on an target
- and it's children. The event handler is called with an extra
- argument which is a two-tuple containing the signal name and
- the FQDN-style name of the target that triggered the event.
-
- This function registers all of the events listed in
- Tutorial.EVENTS
-
- Example arg tuple added:
- ("focus", "1.1.2")
- Side effects:
- -Handlers connected on the various targets
- -Handler ID's stored in self.handlers
-
- @param target the target to recurse on
- @param handler the handler function to connect
- @param prefix name prepended to the target name to form a chain
- @param max_depth maximum recursion depth, None for infinity
- """
- #Gtk Containers have a get_children() function
- if hasattr(target, "get_children") and \
- hasattr(target.get_children, "__call__"):
- children = target.get_children()
- for i in range(len(children)):
- child = children[i]
- if max_depth is None or max_depth > 0:
- #Recurse with a prefix on all children
- pre = ".".join( \
- [p for p in (prefix, str(i)) if not p is None]
- )
- if max_depth is None:
- dep = None
- else:
- dep = max_depth - 1
- self.register_signals_numbered(child, handler, pre, dep)
- #register events on the target if a widget XXX necessary to check this?
- if isinstance(target, gtk.Widget):
- for sig in Tutorial.EVENTS:
- try:
- self.handlers.append( \
- (target, target.connect(sig, handler, (sig, prefix) ))\
- )
- except TypeError:
- continue
-
- def register_signal(self, handler, obj_fqdn, signal_name):
- """Register a signal handler onto a specific widget
- @param handler function to attach as a handler
- @param obj_fqdn fqdn-style object name
- @param signal_name signal name to connect to
-
- Side effects:
- the object found and the handler id obtained by connect() are
- appended in self.handlers
- """
- path = obj_fqdn.split(".")
- #We select the first object and pop the first zero
- obj = self.activity
- path.pop(0)
-
- while len(path) > 0:
- obj = obj.get_children()[int(path.pop(0))]
-
- self.handlers.append( \
- (obj, obj.connect(signal_name, handler, (signal_name, obj_fqdn) ))\
- )
+# def register_signal(self, handler, obj_fqdn, signal_name):
+# """Register a signal handler onto a specific widget
+# @param handler function to attach as a handler
+# @param obj_fqdn fqdn-style object name
+# @param signal_name signal name to connect to
+#
+# Side effects:
+# the object found and the handler id obtained by connect() are
+# appended in self.handlers
+# """
+# obj = find_widget(self.activity, obj_fqdn)
+# self.handlers.append( \
+# (obj, obj.connect(signal_name, handler, (signal_name, obj_fqdn) ))\
+# )
class State:
"""This is a step in a tutorial. The state represents a collection of
diff --git a/src/sugar/tutorius/filters.py b/src/sugar/tutorius/filters.py
new file mode 100644
index 0000000..4c04cf6
--- /dev/null
+++ b/src/sugar/tutorius/filters.py
@@ -0,0 +1,162 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com>
+#
+# 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 gobject
+
+
+from sugar.tutorius.gtkutils import find_widget
+class EventFilter(object):
+ """
+ Base class for an event filter
+ """
+ def __init__(self, next_state):
+ """
+ Constructor.
+ @param next_state name of the next state
+ """
+ self._next_state = next_state
+ self._callback = None
+
+ def get_next_state(self):
+ """
+ Getter for the next state
+ """
+ return self._next_state
+
+ def install_handlers(self, callback, **kwargs):
+ """
+ install_handlers is called for eventfilters to setup all
+ necessary event handlers to be able to catch the desired
+ event.
+
+ @param callback the callback function that will be called
+ with the event filter as an argument when the event is catched
+ and validated.
+ @param **kwargs unused by this handler for now, allows subclasses
+ to receive information about the context when installing
+
+ Subclasses must call this super method to setup the callback if they
+ feel like cooperating
+ """
+ self._callback = callback
+
+ def remove_handlers(self):
+ """
+ remove_handlers is called when a state is done so that all
+ event filters can cleanup any handlers they have installed
+
+ This function will also clear the callback function so that any
+ leftover handler that is triggered will not be able to change the
+ application state.
+
+ subclasses must call this super method to cleanup the callback if they
+ collaborate and use this classe's do_callback()
+ """
+ self._callback = None
+
+ def do_callback(self, *args, **kwargs):
+ """
+ Default callback function that calls the event filter callback
+ with the event filter as only argument.
+ """
+ if self._callback:
+ self._callback(self)
+
+class TimerEvent(EventFilter):
+ """
+ TimerEvent is a special EventFilter that uses gobject
+ timeouts to trigger a state change after a specified amount
+ of time. It must be used inside a gobject main loop to work.
+ """
+ def __init__(self,next_state,timeout_s):
+ """Constructor.
+
+ @param next_state default EventFilter param, passed on to EventFilter
+ @param timeout_s timeout in seconds
+ """
+ super(TimerEvent,self).__init__(next_state)
+ self._timeout = timeout_s
+ self._handler_id = None
+
+ def install_handlers(self, callback, **kwargs):
+ """install_handlers creates the timer and starts it"""
+ super(TimerEvent,self).install_handlers(callback, **kwargs)
+ #Create the timer
+ self._handler_id = gobject.timeout_add_seconds(self._timeout, self._timeout_cb)
+
+ def remove_handlers(self):
+ """remove handler removes the timer"""
+ super(TimerEvent,self).remove_handlers()
+ if self._handler_id:
+ try:
+ #XXX What happens if this was already triggered?
+ #remove the timer
+ gobject.source_remove(self._handler_id)
+ except:
+ pass
+
+ def _timeout_cb(self):
+ """
+ _timeout_cb triggers the eventfilter callback.
+
+ It is necessary because gobject timers only stop if the callback they
+ trigger returns False
+ """
+ self.do_callback()
+ return False #Stops timeout
+
+class GtkWidgetEventFilter(EventFilter):
+ """
+ Basic Event filter for Gtk widget events
+ """
+ def __init__(self, next_state, object_id, event_name):
+ """Constructor
+ @param next_state default EventFilter param, passed on to EventFilter
+ @param object_id object fqdn-style identifier
+ @param event_name event to attach to
+ """
+ super(GtkWidgetEventFilter,self).__init__(next_state)
+ self._callback = None
+ self._object_id = object_id
+ self._event_name = event_name
+ self._widget = None
+ self._handler_id = None
+
+ def install_handlers(self, callback, **kwargs):
+ """install handlers
+ @param callback default EventFilter callback arg
+ @param activity keyword argument activity must be present to install
+ the event handler into the activity's widget hierarchy
+ """
+ super(GtkWidgetEventFilter, self).install_handlers(callback, **kwargs)
+ if not "activity" in kwargs:
+ raise TypeError("activity argument is Mandatory")
+
+ #find the widget and connect to its event
+ self._widget = find_widget(kwargs["activity"], self._object_id)
+ self._handler_id = self._widget.connect( \
+ self._event_name, self.do_callback )
+
+ def remove_handlers(self):
+ """remove handlers"""
+ super(GtkWidgetEventFilter, self).remove_handlers()
+ #if an event was connected, disconnect it
+ if self._handler_id:
+ self._widget.handler_disconnect(self._handler_id)
+ self._handler_id=None
+
+
diff --git a/src/sugar/tutorius/gtkutils.py b/src/sugar/tutorius/gtkutils.py
new file mode 100644
index 0000000..7196469
--- /dev/null
+++ b/src/sugar/tutorius/gtkutils.py
@@ -0,0 +1,166 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com>
+#
+# 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
+"""
+Utility classes and functions that are gtk related
+"""
+
+def find_widget(base, target_fqdn):
+ """Find a widget by digging into a parent widget's children tree
+ @param base the parent widget
+ @param target_fqdn fqdn-style target object name
+
+ @return widget found
+
+ The object should normally be the activity widget, as it is the root
+ widget for activities. The target_fqdn is a dot separated list of
+ indexes used in widget.get_children and should start with a 0 which is
+ the base widget itself,
+
+ Example Usage:
+ find_widget(activity,"0.0.0.1.0.0.2")
+ """
+ path = target_fqdn.split(".")
+ #We select the first object and pop the first zero
+ obj = base
+ path.pop(0)
+
+ while len(path) > 0:
+ obj = obj.get_children()[int(path.pop(0))]
+
+ return obj
+
+EVENTS = [
+ "focus",
+ "button-press-event",
+ "enter-notify-event",
+ "leave-notify-event",
+ "key-press-event",
+ "text-selected",
+ "clicked",
+]
+
+IGNORED_WIDGETS = [
+ "GtkVBox",
+ "GtkHBox",
+ "GtkAlignment",
+ "GtkNotebook",
+ "GtkButton",
+ "GtkToolItem",
+ "GtkToolbar",
+]
+
+def register_signals_numbered(target, handler, prefix="0", max_depth=None):
+ """
+ Recursive function to register event handlers on an target
+ and it's children. The event handler is called with an extra
+ argument which is a two-tuple containing the signal name and
+ the FQDN-style name of the target that triggered the event.
+
+ This function registers all of the events listed in
+ EVENTS
+
+ Example arg tuple added:
+ ("focus", "1.1.2")
+ Side effects:
+ -Handlers connected on the various targets
+
+ @param target the target to recurse on
+ @param handler the handler function to connect
+ @param prefix name prepended to the target name to form a chain
+ @param max_depth maximum recursion depth, None for infinity
+
+ @returns list of (object, handler_id)
+ """
+ ret = []
+ #Gtk Containers have a get_children() function
+ if hasattr(target, "get_children") and \
+ hasattr(target.get_children, "__call__"):
+ children = target.get_children()
+ for i in range(len(children)):
+ child = children[i]
+ if max_depth is None or max_depth > 0:
+ #Recurse with a prefix on all children
+ pre = ".".join( \
+ [p for p in (prefix, str(i)) if not p is None]
+ )
+ if max_depth is None:
+ dep = None
+ else:
+ dep = max_depth - 1
+ ret+=register_signals_numbered(child, handler, pre, dep)
+ #register events on the target if a widget XXX necessary to check this?
+ if isinstance(target, gtk.Widget):
+ for sig in Tutorial.EVENTS:
+ try:
+ ret.append( \
+ (target, target.connect(sig, handler, (sig, prefix) ))\
+ )
+ except TypeError:
+ pass
+
+ return ret
+
+def register_signals(self, target, handler, prefix=None, max_depth=None):
+ """
+ Recursive function to register event handlers on an target
+ and it's children. The event handler is called with an extra
+ argument which is a two-tuple containing the signal name and
+ the FQDN-style name of the target that triggered the event.
+
+ This function registers all of the events listed in
+ Tutorial.EVENTS and omits widgets with a name matching
+ Tutorial.IGNORED_WIDGETS from the name hierarchy.
+
+ Example arg tuple added:
+ ("focus", "Activity.Toolbox.Bold")
+ Side effects:
+ -Handlers connected on the various targets
+ -Handler ID's stored in self.handlers
+
+ @param target the target to recurse on
+ @param handler the handler function to connect
+ @param prefix name prepended to the target name to form a chain
+ @param max_depth maximum recursion depth, None for infinity
+ """
+ ret = []
+ #Gtk Containers have a get_children() function
+ if hasattr(target, "get_children") and \
+ hasattr(target.get_children, "__call__"):
+ for child in target.get_children():
+ if max_depth is None or max_depth > 0:
+ #Recurse with a prefix on all children
+ pre = ".".join( \
+ [p for p in (prefix, target.get_name()) \
+ if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \
+ )
+ ret += register_signals(child, handler, pre, max_depth-1)
+ name = ".".join( \
+ [p for p in (prefix, target.get_name()) \
+ if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \
+ )
+ #register events on the target if a widget XXX necessary to check this?
+ if isinstance(target, gtk.Widget):
+ for sig in Tutorial.EVENTS:
+ try:
+ ret.append( \
+ (target, target.connect(sig, handler, (sig, name) )) \
+ )
+ except TypeError:
+ pass
+
+ return ret
+