From 231a3d87b9c5e2c6469269b93e7ca819f4f32f87 Mon Sep 17 00:00:00 2001 From: mike Date: Sat, 28 Feb 2009 02:08:43 +0000 Subject: Merge branch 'tutorial_toolkit' of ssh://mike@bobthebuilder.mine.nu:8080/home/git into tutorial_toolkit --- 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 +# +# 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 +# +# 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 + -- cgit v0.9.1