diff options
Diffstat (limited to 'sugar-toolkit/src/sugar/tutorius')
25 files changed, 2657 insertions, 0 deletions
diff --git a/sugar-toolkit/src/sugar/tutorius/Makefile.am b/sugar-toolkit/src/sugar/tutorius/Makefile.am new file mode 100644 index 0000000..6fd32c7 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/Makefile.am @@ -0,0 +1,10 @@ +sugardir = $(pythondir)/sugar/tutorius +sugar_PYTHON = \ + __init__.py \ + core.py \ + dialog.py \ + actions.py \ + gtkutils.py \ + filters.py \ + services.py \ + overlayer.py diff --git a/sugar-toolkit/src/sugar/tutorius/Makefile.in b/sugar-toolkit/src/sugar/tutorius/Makefile.in new file mode 100644 index 0000000..b6f8607 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/Makefile.in @@ -0,0 +1,437 @@ +# Makefile.in generated by automake 1.10.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, +# 2003, 2004, 2005, 2006, 2007, 2008 Free Software Foundation, Inc. +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ +VPATH = @srcdir@ +pkgdatadir = $(datadir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +subdir = src/sugar/tutorius +DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in \ + $(sugar_PYTHON) +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4/gnome-compiler-flags.m4 \ + $(top_srcdir)/m4/intltool.m4 $(top_srcdir)/m4/libtool.m4 \ + $(top_srcdir)/m4/ltoptions.m4 $(top_srcdir)/m4/ltsugar.m4 \ + $(top_srcdir)/m4/ltversion.m4 $(top_srcdir)/m4/lt~obsolete.m4 \ + $(top_srcdir)/m4/python.m4 $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(install_sh) -d +CONFIG_CLEAN_FILES = +SOURCES = +DIST_SOURCES = +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = `echo $$p | sed -e 's|^.*/||'`; +am__installdirs = "$(DESTDIR)$(sugardir)" +sugarPYTHON_INSTALL = $(INSTALL_DATA) +py_compile = $(top_srcdir)/py-compile +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +ALL_LINGUAS = @ALL_LINGUAS@ +AMTAR = @AMTAR@ +AR = @AR@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +CATALOGS = @CATALOGS@ +CATOBJEXT = @CATOBJEXT@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DATADIRNAME = @DATADIRNAME@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +EXT_CFLAGS = @EXT_CFLAGS@ +EXT_LIBS = @EXT_LIBS@ +FGREP = @FGREP@ +GETTEXT_PACKAGE = @GETTEXT_PACKAGE@ +GLIB_GENMARSHAL = @GLIB_GENMARSHAL@ +GMOFILES = @GMOFILES@ +GMSGFMT = @GMSGFMT@ +GREP = @GREP@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +INSTOBJEXT = @INSTOBJEXT@ +INTLLIBS = @INTLLIBS@ +INTLTOOL_CAVES_RULE = @INTLTOOL_CAVES_RULE@ +INTLTOOL_DESKTOP_RULE = @INTLTOOL_DESKTOP_RULE@ +INTLTOOL_DIRECTORY_RULE = @INTLTOOL_DIRECTORY_RULE@ +INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@ +INTLTOOL_KBD_RULE = @INTLTOOL_KBD_RULE@ +INTLTOOL_KEYS_RULE = @INTLTOOL_KEYS_RULE@ +INTLTOOL_MERGE = @INTLTOOL_MERGE@ +INTLTOOL_OAF_RULE = @INTLTOOL_OAF_RULE@ +INTLTOOL_PERL = @INTLTOOL_PERL@ +INTLTOOL_POLICY_RULE = @INTLTOOL_POLICY_RULE@ +INTLTOOL_PONG_RULE = @INTLTOOL_PONG_RULE@ +INTLTOOL_PROP_RULE = @INTLTOOL_PROP_RULE@ +INTLTOOL_SCHEMAS_RULE = @INTLTOOL_SCHEMAS_RULE@ +INTLTOOL_SERVER_RULE = @INTLTOOL_SERVER_RULE@ +INTLTOOL_SERVICE_RULE = @INTLTOOL_SERVICE_RULE@ +INTLTOOL_SHEET_RULE = @INTLTOOL_SHEET_RULE@ +INTLTOOL_SOUNDLIST_RULE = @INTLTOOL_SOUNDLIST_RULE@ +INTLTOOL_THEME_RULE = @INTLTOOL_THEME_RULE@ +INTLTOOL_UI_RULE = @INTLTOOL_UI_RULE@ +INTLTOOL_UPDATE = @INTLTOOL_UPDATE@ +INTLTOOL_XAM_RULE = @INTLTOOL_XAM_RULE@ +INTLTOOL_XML_NOMERGE_RULE = @INTLTOOL_XML_NOMERGE_RULE@ +INTLTOOL_XML_RULE = @INTLTOOL_XML_RULE@ +LD = @LD@ +LDFLAGS = @LDFLAGS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LIBTOOL = @LIBTOOL@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LTLIBOBJS = @LTLIBOBJS@ +MAKEINFO = @MAKEINFO@ +MKDIR_P = @MKDIR_P@ +MKINSTALLDIRS = @MKINSTALLDIRS@ +MSGFMT = @MSGFMT@ +MSGFMT_OPTS = @MSGFMT_OPTS@ +MSGMERGE = @MSGMERGE@ +NM = @NM@ +NMEDIT = @NMEDIT@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +POFILES = @POFILES@ +POSUB = @POSUB@ +PO_IN_DATADIR_FALSE = @PO_IN_DATADIR_FALSE@ +PO_IN_DATADIR_TRUE = @PO_IN_DATADIR_TRUE@ +PYGTK_CODEGEN = @PYGTK_CODEGEN@ +PYGTK_DEFSDIR = @PYGTK_DEFSDIR@ +PYTHON = @PYTHON@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_INCLUDES = @PYTHON_INCLUDES@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_VERSION = @PYTHON_VERSION@ +RANLIB = @RANLIB@ +SED = @SED@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +STRIP = @STRIP@ +USE_NLS = @USE_NLS@ +VERSION = @VERSION@ +WARN_CFLAGS = @WARN_CFLAGS@ +XGETTEXT = @XGETTEXT@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +lt_ECHO = @lt_ECHO@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +sugardir = $(pythondir)/sugar/tutorius +sugar_PYTHON = \ + __init__.py \ + core.py \ + dialog.py \ + actions.py \ + gtkutils.py \ + filters.py \ + services.py \ + overlayer.py + +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh \ + && exit 0; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/sugar/tutorius/Makefile'; \ + cd $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/sugar/tutorius/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +install-sugarPYTHON: $(sugar_PYTHON) + @$(NORMAL_INSTALL) + test -z "$(sugardir)" || $(MKDIR_P) "$(DESTDIR)$(sugardir)" + @list='$(sugar_PYTHON)'; dlist=''; for p in $$list; do\ + if test -f "$$p"; then b=; else b="$(srcdir)/"; fi; \ + if test -f $$b$$p; then \ + f=$(am__strip_dir) \ + dlist="$$dlist $$f"; \ + echo " $(sugarPYTHON_INSTALL) '$$b$$p' '$(DESTDIR)$(sugardir)/$$f'"; \ + $(sugarPYTHON_INSTALL) "$$b$$p" "$(DESTDIR)$(sugardir)/$$f"; \ + else :; fi; \ + done; \ + if test -n "$$dlist"; then \ + if test -z "$(DESTDIR)"; then \ + PYTHON=$(PYTHON) $(py_compile) --basedir "$(sugardir)" $$dlist; \ + else \ + PYTHON=$(PYTHON) $(py_compile) --destdir "$(DESTDIR)" --basedir "$(sugardir)" $$dlist; \ + fi; \ + else :; fi + +uninstall-sugarPYTHON: + @$(NORMAL_UNINSTALL) + @list='$(sugar_PYTHON)'; dlist=''; for p in $$list; do\ + f=$(am__strip_dir) \ + rm -f "$(DESTDIR)$(sugardir)/$$f"; \ + rm -f "$(DESTDIR)$(sugardir)/$${f}c"; \ + rm -f "$(DESTDIR)$(sugardir)/$${f}o"; \ + done +tags: TAGS +TAGS: + +ctags: CTAGS +CTAGS: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -pR $(srcdir)/$$file $(distdir)$$dir || exit 1; \ + fi; \ + cp -pR $$d/$$file $(distdir)$$dir || exit 1; \ + else \ + test -f $(distdir)/$$file \ + || cp -p $$d/$$file $(distdir)/$$file \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile +installdirs: + for dir in "$(DESTDIR)$(sugardir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + `test -z '$(STRIP)' || \ + echo "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'"` install +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic clean-libtool mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +info: info-am + +info-am: + +install-data-am: install-sugarPYTHON + +install-dvi: install-dvi-am + +install-exec-am: + +install-html: install-html-am + +install-info: install-info-am + +install-man: + +install-pdf: install-pdf-am + +install-ps: install-ps-am + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-sugarPYTHON + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-generic clean-libtool \ + distclean distclean-generic distclean-libtool distdir dvi \ + dvi-am html html-am info info-am install install-am \ + install-data install-data-am install-dvi install-dvi-am \ + install-exec install-exec-am install-html install-html-am \ + install-info install-info-am install-man install-pdf \ + install-pdf-am install-ps install-ps-am install-strip \ + install-sugarPYTHON installcheck installcheck-am installdirs \ + maintainer-clean maintainer-clean-generic mostlyclean \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + uninstall uninstall-am uninstall-sugarPYTHON + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/sugar-toolkit/src/sugar/tutorius/__init__.py b/sugar-toolkit/src/sugar/tutorius/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/__init__.py diff --git a/sugar-toolkit/src/sugar/tutorius/actions.py b/sugar-toolkit/src/sugar/tutorius/actions.py new file mode 100644 index 0000000..da8219e --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/actions.py @@ -0,0 +1,152 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 +""" +This module defines Actions that can be done and undone on a state +""" + +from sugar.tutorius import gtkutils +from dialog import TutoriusDialog +from sugar.tutorius.services import ObjectStore +import overlayer + + +class Action(object): + """Base class for Actions""" + def __init__(self): + object.__init__(self) + + def do(self, **kwargs): + """ + Perform the action + """ + raise NotImplementedError("Not implemented") + + def undo(self): + """ + Revert anything the action has changed + """ + pass #Should raise NotImplemented? + + +class OnceWrapper(object): + """ + Wraps a class to perform an action once only + + This ConcreteActions's do() method will only be called on the first do() + and the undo() will be callable after do() has been called + """ + def __init__(self, action): + self._action = action + self._called = False + self._need_undo = False + + def do(self): + """ + Do the action only on the first time + """ + if not self._called: + self._called = True + self._action.do() + self._need_undo = True + + def undo(self): + """ + Undo the action if it's been done + """ + if self._need_undo: + self._action.undo() + self._need_undo = False + +class DialogMessage(Action): + """ + Shows a dialog with a given text, at the given position on the screen. + + @param message A string to display to the user + @param pos A list of the form [x, y] + """ + def __init__(self, message, pos=[0,0]): + super(DialogMessage, self).__init__() + self._message = message + self.position = pos + self._dialog = None + + def do(self): + """ + Show the dialog + """ + self._dialog = TutoriusDialog(self._message) + self._dialog.set_button_clicked_cb(self._dialog.close_self) + self._dialog.set_modal(False) + self._dialog.move(self.position[0], self.position[1]) + self._dialog.show() + + def undo(self): + """ + Destroy the dialog + """ + if self._dialog: + self._dialog.destroy() + self._dialog = None + + +class BubbleMessage(Action): + """ + Shows a dialog with a given text, at the given position on the screen. + + @param message A string to display to the user + @param pos A list of the form [x, y] + @param speaker treeish representation of the speaking widget + """ + def __init__(self, message, pos=[0,0], speaker=None, tailpos=None): + Action.__init__(self) + self._message = message + self.position = pos + + self.overlay = None + self._bubble = None + self._speaker = None + self._tailpos = tailpos + + + def do(self): + """ + Show the dialog + """ + # get or inject overlayer + self.overlay = ObjectStore().activity._overlayer + # FIXME: subwindows, are left to overlap this. This behaviour is + # undesirable. subwindows (i.e. child of top level windows) should be + # handled either by rendering over them, or by finding different way to + # draw the overlay. + + if not self._bubble: + x, y = self.position + # TODO: tails are relative to tailpos. They should be relative to + # the speaking widget. Same of the bubble position. + self._bubble = overlayer.TextBubble(text=self._message, + tailpos=self._tailpos) + self._bubble.show() + self.overlay.put(self._bubble, x, y) + self.overlay.queue_draw() + + def undo(self): + """ + Destroy the dialog + """ + if self._bubble: + self._bubble.destroy() + self._bubble = None + diff --git a/sugar-toolkit/src/sugar/tutorius/calc.py b/sugar-toolkit/src/sugar/tutorius/calc.py new file mode 100644 index 0000000..56ceba6 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/calc.py @@ -0,0 +1,130 @@ +import logging + +import sys, os +import pygtk +pygtk.require20() +import gtk + +import overlayer + + +#import rpdb2 +#rpdb2.start_embedded_debugger("foo", True, True) + +class TootOriole(): + def hello(self, widget, data=None): + logging.info('Hello World') + + def buttonclicked(self, button, data=None): + evt = button.child.get_text() + if evt not in ("+", "-", "*", "/", "=", "."): + if self.newNum: + self.reg1 = 0 + self.newNum = False + if not self.decimal: + self.reg1 = self.reg1*10+int(evt) + else: + self.reg1 += self.decimal*int(evt) + self.decimal /= 10 + else: + if (evt == "."): + self.decimal = 0.1 + return + + if evt == "=": + evt = None + + self.reg2 = { + "+": lambda a, b: a+b, + "-": lambda a, b: a-b, + "*": lambda a, b: a*b, + "/": lambda a, b: a/b, + None: lambda a, b: b + }[self.lastOp](self.reg2, self.reg1) + self.reg1 = self.reg2 + self.newNum = True + self.decimal = 0 + self.lastOp = evt + self.label.set_label(str(self.reg1)) + + def __init__(self): + self.reg1 = 0 + self.reg2 = 0 + self.lastOp = None + self.decimal = 0 + self.newNum = False + + print "running activity init" + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.connect("delete_event", self.delete_event) + self.window.connect("destroy", self.destroy) + print "activity running" + + # Creates the Toolbox. It contains the Activity Toolbar, which is the + # bar that appears on every Sugar window and contains essential + # functionalities, such as the 'Collaborate' and 'Close' buttons. + #toolbox = activity.ActivityToolbox(self) + #self.set_toolbox(toolbox) + #toolbox.show() + + self.vbox = gtk.VBox(homogeneous=False, spacing=20) + + #textbox + self.label = gtk.Label("0") + self.label.show() + self.vbox.pack_start(self.label, False, True, 10) + + table = gtk.Table(rows=4, columns=4, homogeneous=True) + table.show() + self.vbox.pack_start(child=table, expand=True, fill=True, \ + padding=10) + buttonlist = [0, "+", "-", "x", 7, 8, 9, "/", 4, 5, 6, "=", \ + 1, 2, 3, "."] + + for i in range(4): + for j in range(4): + button = gtk.Button("") + button.child.set_markup("<big><b>%s</b></big>" \ + % str(buttonlist.pop(0))) + table.attach(button, j, j+1, i, i+1, gtk.EXPAND|gtk.FILL, + gtk.EXPAND|gtk.FILL, 15, 15) + button.connect("clicked", self.buttonclicked, None) + button.show() + + # Set the button to be our canvas. The canvas is the main section of + # every Sugar Window. It fills all the area below the toolbox. + + + # The final step is to display this newly created widget. + self.vbox.show() + self.window.add(self.vbox) + + self.window.show() + + # proto overlap + # ==================================================================== + # =================================== clip here ====================== + # Create overlay base where all overlayed widgets will reside + overlayBase = overlayer.Overlayer() + overlayBase.inject(self.vbox) + bubble = overlayer.TextBubble(text="Hello,\nI'm a comma.Use me to\nrepresent decimal numbers.", speaker=button) + overlayBase.put(bubble, 40, 50) + # we do not eject the overlay, but it could be done when overlay is not + # needed anymore + # =================================== end clip ======================= + # ==================================================================== + + + def delete_event(self, widget, event, data=None): + print "quitting..." + return False + + def destroy(self, widget, data=None): + gtk.main_quit() + + + + +if __name__ == "__main__": + t = TootOriole() + gtk.main() diff --git a/sugar-toolkit/src/sugar/tutorius/core.py b/sugar-toolkit/src/sugar/tutorius/core.py new file mode 100644 index 0000000..f817ba9 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/core.py @@ -0,0 +1,334 @@ +# 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 +""" +Core + +This module contains the core classes for tutorius + +""" + +import gtk +import logging + +from sugar.tutorius.dialog import TutoriusDialog +from sugar.tutorius.gtkutils import find_widget + +logger = logging.getLogger("tutorius") + +class Tutorial (object): + """ + Tutorial Class, used to run through the FSM. + """ + + def __init__(self, name, fsm): + """ + Creates an unattached tutorial. + """ + object.__init__(self) + self.name = name + + self.state_machine = fsm + self.state_machine.set_tutorial(self) + + self.state = None + + self.handlers = [] + self.activity = None + #Rest of initialisation happens when attached + + def attach(self, activity): + """ + Attach to a running activity + + @param activity the activity to attach to + """ + #For now, absolutely detach if a previous one! + if self.activity: + self.detach() + self.activity = activity + self.state_machine.set_state("INIT") + + def detach(self): + """ + Detach from the current activity + """ + + # Uninstall the whole FSM + self.state_machine.teardown() + + #FIXME There should be some amount of resetting done here... + self.activity = None + + + def set_state(self, name): + """ + Switch to a new state + """ + logger.debug("====NEW STATE: %s====" % name) + + self.state_machine.set_state(name) + + + # Currently unused -- equivalent function is in each state + 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 + + #Swith to the next state pointed by the eventfilter + self.set_state(eventfilter.get_next_state()) + +class State: + """ + This is a step in a tutorial. The state represents a collection of actions + to undertake when entering the state, and a series of event filters + with associated actions that point to a possible next state. + """ + + def __init__(self, name, action_list=[], event_filter_list=[], tutorial=None): + """ + Initializes the content of the state, like loading the actions + that are required and building the correct tests. + + @param action_list The list of actions to execute when entering this + state + @param event_filter_list A list of tuples of the form + (event_filter, next_state_name), that explains the outgoing links for + this state + @param tutorial The higher level container of the state + """ + self._actions = action_list + + # Unused for now + #self.tests = [] + + self._event_filters = event_filter_list + + self.tutorial = tutorial + + def set_tutorial(self, tutorial): + """ + Associates this state with a tutorial. A tutorial must be set prior + to executing anything in the state. The reason for this is that the + states need to have access to the activity (via the tutorial) in order + to properly register their callbacks on the activities' widgets. + + @param tutorial The tutorial that this state runs under. + """ + if self.tutorial == None : + self.tutorial = tutorial + else: + raise RuntimeWarning(\ + "The state %s was already associated with a tutorial." % self.name) + + def setup(self): + """ + Install the state itself, by first registering the event filters + and then triggering the actions. + """ + for eventfilter in self._event_filters: + eventfilter.install_handlers(self._event_filter_state_done_cb, + activity=self.tutorial.activity) + + for action in self._actions: + action.do() + + def teardown(self): + """ + Uninstall all the event filters that were active in this state. + Also undo every action that was installed for this state. This means + removing dialogs that were displayed, removing highlights, etc... + """ + # Remove the handlers for the all of the state's event filters + for event_filter in self._event_filters: + event_filter.remove_handlers() + + # Undo all the actions related to this state + for action in self._actions: + action.undo() + + def _event_filter_state_done_cb(self, event_filter): + """ + Callback for event filters. This function needs to inform the + tutorial that the state is over and tell it what is the next state. + + @param event_filter The event filter that was called + """ + # Run the tests here, if need be + + # Warn the higher level that we wish to change state + self.tutorial.set_state(event_filter.get_next_state()) + + # Unused for now +## def verify(self): +## """Run the internal tests to see if one of them passes. If it does, +## then do the associated processing to go in the next state.""" +## for test in self.tests: +## if test.verify() == True: +## actions = test.get_actions() +## for act in actions: +## act.do() +## # Now that we execute the actions related to a test, we might +## # want to undo them right after --- should we use a callback or +## # a timer? + +class FiniteStateMachine(State): + """ + This is a collection of states, with a start state and an end callback. + It is used to simplify the development of the various tutorials by + encapsulating a collection of states that represent a given learning + process. + + For now, we will consider that there can only be states + inserted in the FSM, and that there are no nested FSM inside. + """ + + def __init__(self, name, tutorial=None, state_dict={}, start_state_name="INIT", action_list=[]): + """ + The constructor for a FSM. Pass in the start state and the setup + actions that need to be taken when the FSM itself start (which may be + different from what is done in the first state of the machine). + + @param name A short descriptive name for this FSM + @param tutorial The tutorial that will execute this FSM. If None is + attached on creation, then one must absolutely be attached before + executing the FSM with set_tutorial(). + @param state_dict A dictionary containing the state names as keys and + the state themselves as entries. + @param start_state_name The name of the starting state, if different + from "INIT" + @param action_list The actions to undertake when initializing the FSM + """ + State.__init__(self, name) + + self.name = name + self.tutorial = tutorial + + # Dictionnary of states contained in the FSM + self._states = state_dict + + # Remember the initial state - we might want to reset + # or rewind the FSM at a later moment + self.start_state = state_dict[start_state_name] + self.current_state = self.start_state + + # Register the actions for the FSM - They will be processed at the + # FSM level, meaning that when the FSM will start, it will first + # execute those actions. When the FSM closes, it will tear down the + # inner actions of the state, then close its own actions + self.actions = action_list + + # Flag to mention that the FSM was initialized + self._fsm_setup_done = False + # Flag that must be raised when the FSM is to be teared down + self._fsm_teardown_done = False + # Flag used to declare that the FSM has reached an end state + self._fsm_has_finished = False + + def set_tutorial(self, tutorial): + """ + This associates the FSM to the given tutorial. It MUST be associated + either in the constructor or with this function prior to executing the + FSM. + + @param tutorial The tutorial that will execute this FSM. + """ + # If there was no tutorial associated + if self.tutorial == None: + # Associate it with this FSM and all the underlying states + self.tutorial = tutorial + for state in self._states.itervalues(): + state.set_tutorial(tutorial) + else: + raise RuntimeWarning(\ + "The FSM %s is already associated with a tutorial."%self.name\ + ) + + def setup(self): + """ + This function initializes the FSM the first time it is called. + Then, every time it is called, it initializes the current state. + """ + # Are we associated with a tutorial? + if self.tutorial == None: + raise UnboundLocalError("No tutorial was associated with FSM %s" % self.name) + + # If we never initialized the FSM itself, then we need to run all the + # actions associated with the FSM. + if self._fsm_setup_done == False: + # Flag the FSM level setup as done + self._fsm_setup_done = True + # Execute all the FSM level actions + for action in self.actions: + action.do() + + # Then, we need to run the setup of the current state + self.current_state.setup() + + def set_state(self, new_state_name): + """ + This functions changes the current state of the finite state machine. + + @param new_state The identifier of the state we need to go to + """ + # TODO : Since we assume no nested FSMs, we don't set state on the + # inner States / FSMs +## # Pass in the name to the internal state - it might be a FSM and +## # this name will apply to it +## self.current_state.set_state(new_state_name) + + # Make sure the given state is owned by the FSM + if not self._states.has_key(new_state_name): + # If we did not recognize the name, then we do not possess any + # state by that name - we must ignore this state change request as + # it will be done elsewhere in the hierarchy (or it's just bogus). + return + + new_state = self._states[new_state_name] + + # Undo the actions of the old state + self.teardown() + + # Insert the new state + self.current_state = new_state + + # Call the initial actions in the new state + self.setup() + + + def teardown(self): + """ + Revert any changes done by setup() + """ + # Teardown the current state + self.current_state.teardown() + + # If we just finished the whole FSM, we need to also call the teardown + # on the FSM level actions + if self._fsm_has_finished == True: + # Flag the FSM teardown as not needed anymore + self._fsm_teardown_done = True + # Undo all the FSM level actions here + for action in self.actions: + action.undo() + + #Unused for now +## def verify(self): +## """Verify if the current state passes its tests""" +## return self.current_state.verify() diff --git a/sugar-toolkit/src/sugar/tutorius/dialog.py b/sugar-toolkit/src/sugar/tutorius/dialog.py new file mode 100644 index 0000000..be51a0e --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/dialog.py @@ -0,0 +1,59 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 +""" +The Dialog module provides means of interacting with the user +through the use of Dialogs. +""" +import gtk + +class TutoriusDialog(gtk.Dialog): + """ + TutoriusDialog is a simple wrapper around gtk.Dialog. + + It allows creating and showing a dialog and connecting the response and + button click events to callbacks. + """ + def __init__(self, label="Hint", button_clicked_cb=None, response_cb=None): + """ + Constructor. + + @param label text to be shown on the dialog + @param button_clicked_cb callback for the button click + @param response_cb callback for the dialog response + """ + gtk.Dialog.__init__(self) + + self._button = gtk.Button(label) + + self.add_action_widget(self._button, 1) + + if not button_clicked_cb == None: + self._button.connect("clicked", button_clicked_cb) + + self._button.show() + + if not response_cb == None: + self.connect("response", response_cb) + + self.set_decorated(False) + + def set_button_clicked_cb(self, funct): + """Setter for the button_clicked callback""" + self._button.connect("clicked", funct) + + def close_self(self, arg=None): + """Close the dialog""" + self.destroy() diff --git a/sugar-toolkit/src/sugar/tutorius/dialog.pyc b/sugar-toolkit/src/sugar/tutorius/dialog.pyc Binary files differnew file mode 100644 index 0000000..e7e02b9 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/dialog.pyc diff --git a/sugar-toolkit/src/sugar/tutorius/dragbox.py b/sugar-toolkit/src/sugar/tutorius/dragbox.py new file mode 100644 index 0000000..d84e4ee --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/dragbox.py @@ -0,0 +1,120 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 +""" +This module defines the DragBox class, representing a gtk.Layout like container +which supports a "children dragging mode". +""" + +import gtk + +class DragBox(gtk.EventBox): + """ + A DragBox represents a gtk container whose child object can be arbitrarily + placed, like for a gtk.Layout. However DragBox implements a child "dragging" + mode which allows the user to freely move immediate children of the DragBox. + + This can be toggled by the dragMode property (default to False) + + Default behavior is to keep children at minimal size, as returned by + a call to gtk.Widget.get_size(). + + """ + def __init__(self): + gtk.EventBox.__init__(self) + + self.set_above_child(False) + self.connect("motion-notify-event", self._on_mouse_evt) + self.connect("button-press-event", self._on_mouse_evt) + self.connect("button-release-event", self._on_mouse_evt) + + self._drag_mode = False + self._dragging = None # the object being dragged, or None + self._last_mouse_pos = None # last known mouse position (win. relative) + + self._layout = gtk.Layout() + self.add(self._layout) + + def attach(self, widget, xPos=0, yPos=0, size=None, resizable=False): + """ + Adds the child to the EventBox hierarchy. + widget is expected to be a displayable gtk.Widget and will respond + to the dragMode by being dragable. + """ + # TODO handle child and set size + self._layout.put(child_widget=widget, x=xPos, y=yPos) + + def show(self): + gtk.EventBox.show(self) + self._layout.show() + + def hide(self): + gtk.EventBox.hide(self) + self._layout.hide() + + def _on_mouse_evt(self, widget, event, data=None): + """ + Callback registered on mouse events. + _on_mouse_evt does the actual child selection and moving + """ + if not self._drag_mode: + return + + x, y = event.get_coords() + + if self._dragging is not None and event.type is gtk.gdk.MOTION_NOTIFY: + child = self._dragging.get_allocation() + self._layout.move(self._dragging, + int(child.x+x-self._last_mouse_pos[0]), + int(child.y+y-self._last_mouse_pos[1])) + self._last_mouse_pos = event.get_coords() + # FIXME scale layout or constraint dragging to DragBox + + elif self._dragging is None and event.type is gtk.gdk.BUTTON_PRESS: + # iterate in revese as last drawn child is over the others + for child in reversed(self._layout.get_children()): + rect = child.get_allocation() + if x >= rect.x and x < rect.x+rect.width \ + and y >= rect.y and y < rect.y+rect.height: + self._dragging = child + # Common interface paradigm is that selecting an item + # goes to top of stack it so we do just that. + self._layout.remove(child) + self._layout.put(child, rect.x, rect.y) + break + self._last_mouse_pos = event.get_coords() + # TODO add conditional restack child + + elif self._dragging is not None \ + and event.type is gtk.gdk.BUTTON_RELEASE: + self._dragging = None + + def _set_drag_mode(self, value): + if (not self._drag_mode) and value: + self.set_above_child(True) + elif self._drag_mode and (not value): + self.set_above_child(False) + + self._drag_mode = value + + def _get_drag_mode(self): + return self._drag_mode + + dragMode = property(fset=_set_drag_mode, fget=_get_drag_mode, + doc="Defines whether widgets in DragBox can be mouse dragged.") + + # TODO set child properties (list_child_properties) + + diff --git a/sugar-toolkit/src/sugar/tutorius/dragbox.pyc b/sugar-toolkit/src/sugar/tutorius/dragbox.pyc Binary files differnew file mode 100644 index 0000000..568f3e3 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/dragbox.pyc diff --git a/sugar-toolkit/src/sugar/tutorius/filters.py b/sugar-toolkit/src/sugar/tutorius/filters.py new file mode 100644 index 0000000..4c04cf6 --- /dev/null +++ b/sugar-toolkit/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/sugar-toolkit/src/sugar/tutorius/gtkutils.py b/sugar-toolkit/src/sugar/tutorius/gtkutils.py new file mode 100644 index 0000000..073a7f3 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/gtkutils.py @@ -0,0 +1,169 @@ +# 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: + try: + obj = obj.get_children()[int(path.pop(0))] + except: + break + + 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 + diff --git a/sugar-toolkit/src/sugar/tutorius/overlayer.py b/sugar-toolkit/src/sugar/tutorius/overlayer.py new file mode 100644 index 0000000..c08ed4c --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/overlayer.py @@ -0,0 +1,328 @@ +""" +This guy manages drawing of overlayed widgets. The class responsible for drawing +management (Overlayer) and overlayable widgets are defined here. +""" +# Copyright (C) 2009, Tutorius.org +# +# 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 +import gtk +import cairo +import pangocairo + +# This is the CanvasDrawable protocol. Any widget wishing to be drawn on the +# overlay must implement it. See TextBubble for a sample implementation. +#class CanvasDrawable(object): +# """Defines the CanvasDrawable protocol""" +# no_expose = None +# def draw_with_context(self, context): +# """ +# Draws the cairo widget with the passed cairo context. +# This will be called if the widget is child of an overlayer. +# """ +# pass + +class Overlayer(gtk.Layout): + """ + This guy manages drawing of overlayed widgets. Those can be standard GTK + widgets or special "cairoDrawable" widgets which support the defined + interface (see the put method). + + @param overlayed widget to be overlayed. Will be resized to full size. + """ + def __init__(self, overlayed=None): + gtk.Layout.__init__(self) + + self._overlayed = overlayed + if overlayed: + self.put(overlayed, 0, 0) + + self.__realizer = self.connect("expose-event", self.__init_realized) + self.connect("size-allocate", self.__size_allocate) + self.show() + + self.__render_handle = None + + def put(self, child, x, y): + """ + Adds a child widget to be overlayed. This can be, overlay widgets or + normal GTK widgets (though normal widgets will alwas appear under + cairo widgets due to the rendering chain). + + @param child the child to add + @param x the horizontal coordinate for positionning + @param y the vertical coordinate for positionning + """ + if hasattr(child, "draw_with_context"): + # if the widget has the CanvasDrawable protocol, use it. + child.no_expose = True + gtk.Layout.put(self, child, x, y) + + + def __init_realized(self, widget, event): + """ + Initializer to set once widget is realized. + Since an expose event is signaled only to realized widgets, we set this + callback for the first expose run. It should also be called after + beign reparented to ensure the window used for drawing is set up. + """ + assert hasattr(self.window, "set_composited"), \ + "compositing not supported or widget not realized." + self.disconnect(self.__realizer) + del self.__realizer + + self.parent.set_app_paintable(True) + + # the parent is composited, so we can access gtk's rendered buffer + # and overlay over. If we don't composite, we won't be able to read + # pixels and background will be black. + self.window.set_composited(True) + self.__render_handle = self.parent.connect_after("expose-event", \ + self.__expose_overlay) + + def __expose_overlay(self, widget, event): + """expose event handler to draw the thing.""" + #get our child (in this case, the event box) + child = widget.get_child() + + #create a cairo context to draw to the window + ctx = widget.window.cairo_create() + + #the source data is the (composited) event box + ctx.set_source_pixmap(child.window, + child.allocation.x, + child.allocation.y) + + #draw no more than our expose event intersects our child + region = gtk.gdk.region_rectangle(child.allocation) + rect = gtk.gdk.region_rectangle(event.area) + region.intersect(rect) + ctx.region (region) + ctx.clip() + + ctx.set_operator(cairo.OPERATOR_OVER) + # has to be blended and a 1.0 alpha would not make it blend + ctx.paint_with_alpha(0.99) + + #draw overlay + for drawn_child in self.get_children(): + if hasattr(drawn_child, "draw_with_context"): + drawn_child.draw_with_context(ctx) + + + def __size_allocate(self, widget, allocation): + """ + Set size allocation (actual gtk widget size) and propagate it to + overlayed child + """ + self.allocation = allocation + # One may wonder why using size_request instead of size_allocate; + # Since widget is laid out in a Layout box, the Layout will honor the + # requested size. Using size_allocate could make a nasty nested loop in + # some cases. + self._overlayed.set_size_request(allocation.width, allocation.height) + + +class TextBubble(gtk.Widget): + """ + A CanvasDrawableWidget drawing a round textbox and a tail pointing + to a specified widget. + """ + def __init__(self, text, speaker=None, tailpos=None): + """ + Creates a new cairo rendered text bubble. + + @param text the text to render in the bubble + @param speaker the widget to compute the tail position from + @param tailpos (optional) position relative to the bubble to use as + the tail position, if no speaker + """ + gtk.Widget.__init__(self) + + # FIXME: ensure previous call does not interfere with widget stacking, + # as using a gtk.Layout and stacking widgets may reveal a screwed up + # order with the cairo widget on top. + self.__label = None + self.__text_dimentions = None + + self.label = text + self.speaker = speaker + self.tailpos = tailpos + self.line_width = 5 + + self.__exposer = self.connect("expose-event", self.__on_expose) + + def draw_with_context(self, context): + """ + Draw using the passed cairo context instead of creating a new cairo + context. This eases blending between multiple cairo-rendered + widgets. + """ + context.translate(self.allocation.x, self.allocation.y) + width = self.allocation.width + height = self.allocation.height + xradius = width/2 + yradius = height/2 + width -= self.line_width + height -= self.line_width + + # bubble border + context.move_to(self.line_width, yradius) + context.curve_to(self.line_width, self.line_width, + self.line_width, self.line_width, xradius, self.line_width) + context.curve_to(width, self.line_width, + width, self.line_width, width, yradius) + context.curve_to(width, height, width, height, xradius, height) + context.curve_to(self.line_width, height, + self.line_width, height, self.line_width, yradius) + context.set_line_width(self.line_width) + context.set_source_rgb(0.0, 0.0, 0.0) + context.stroke() + + # TODO fetch speaker coordinates + + # draw bubble tail + if self.tailpos: + context.move_to(xradius-40, yradius) + context.line_to(self.tailpos[0], self.tailpos[1]) + context.line_to(xradius+40, yradius) + context.set_line_width(self.line_width) + context.set_source_rgb(0.0, 0.0, 0.0) + context.stroke_preserve() + context.set_source_rgb(1.0, 1.0, 0.0) + context.fill() + + # bubble painting. Redrawing the inside after the tail will combine + # both shapes. + # TODO: we could probably generate the shape at initialization to + # lighten computations. + context.move_to(self.line_width, yradius) + context.curve_to(self.line_width, self.line_width, + self.line_width, self.line_width, xradius, self.line_width) + context.curve_to(width, self.line_width, + width, self.line_width, width, yradius) + context.curve_to(width, height, width, height, xradius, height) + context.curve_to(self.line_width, height, + self.line_width, height, self.line_width, yradius) + context.set_source_rgb(1.0, 1.0, 0.0) + context.fill() + + # text + # FIXME create text layout when setting text or in realize method + context.set_source_rgb(0.0, 0.0, 0.0) + pangoctx = pangocairo.CairoContext(context) + text_layout = pangoctx.create_layout() + text_layout.set_text(self.__label) + pangoctx.move_to( + int((self.allocation.width-self.__text_dimentions[0])/2), + int((self.allocation.height-self.__text_dimentions[1])/2)) + pangoctx.show_layout(text_layout) + + # work done. Be kind to next cairo widgets and reset matrix. + context.identity_matrix() + + def do_realize(self): + """ Setup gdk window creation. """ + self.set_flags(gtk.REALIZED | gtk.NO_WINDOW) + # TODO: cleanup window creation code as lot here probably isn't + # necessary. + # See http://www.learningpython.com/2006/07/25/writing-a-custom-widget-using-pygtk/ + # as the following was taken there. + self.window = self.get_parent_window() + if not isinstance(self.parent, Overlayer): + self.unset_flags(gtk.NO_WINDOW) + self.window = gtk.gdk.Window( + self.get_parent_window(), + width=self.allocation.width, + height=self.allocation.height, + window_type=gtk.gdk.WINDOW_CHILD, + wclass=gtk.gdk.INPUT_OUTPUT, + event_mask=self.get_events()|gtk.gdk.EXPOSURE_MASK) + + # Associate the gdk.Window with ourselves, Gtk+ needs a reference + # between the widget and the gdk window + self.window.set_user_data(self) + + # Attach the style to the gdk.Window, a style contains colors and + # GC contextes used for drawing + self.style.attach(self.window) + + # The default color of the background should be what + # the style (theme engine) tells us. + self.style.set_background(self.window, gtk.STATE_NORMAL) + self.window.move_resize(*self.allocation) + + def __on_expose(self, widget, event): + """Redraw event callback.""" + ctx = self.window.cairo_create() + + self.draw_with_context(ctx) + + return True + + def _set_label(self, value): + """Sets the label and flags the widget to be redrawn.""" + self.__label = value + # FIXME hack to calculate size. necessary because may not have been + # realized. We create a fake surface to use builtin math. This should + # probably be done at realization and/or on text setter. + surf = cairo.SVGSurface("/dev/null", 0, 0) + ctx = cairo.Context(surf) + pangoctx = pangocairo.CairoContext(ctx) + text_layout = pangoctx.create_layout() + text_layout.set_text(value) + self.__text_dimentions = text_layout.get_pixel_size() + del text_layout, pangoctx, ctx, surf + + def do_size_request(self, requisition): + """Fill requisition with size occupied by the widget.""" + width, height = self.__text_dimentions + + # FIXME bogus values follows. will need to replace them with + # padding relative to font size and line border size + requisition.width = int(width+30) + requisition.height = int(height+40) + + def do_size_allocate(self, allocation): + """Save zone allocated to the widget.""" + self.allocation = allocation + + def _get_label(self): + """Getter method for the label property""" + return self.__label + + def _set_no_expose(self, value): + """setter for no_expose property""" + if self.__exposer and value: + self.disconnect(self.__exposer) + self.__exposer = None + elif (not self.__exposer) and (not value): + self.__exposer = self.connect("expose-event", self.__on_expose) + + def _get_no_expose(self): + """getter for no_expose property""" + return not self.__exposer + + no_expose = property(fset=_set_no_expose, fget=_get_no_expose, + doc="Whether the widget should handle exposition events or not.") + + label = property(fget=_get_label, fset=_set_label, + doc="Text label which is to be painted on the top of the widget") + +gobject.type_register(TextBubble) + + +# vim:set ts=4 sts=4 sw=4 et: diff --git a/sugar-toolkit/src/sugar/tutorius/overlayer.pyc b/sugar-toolkit/src/sugar/tutorius/overlayer.pyc Binary files differnew file mode 100644 index 0000000..627ba9d --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/overlayer.pyc diff --git a/sugar-toolkit/src/sugar/tutorius/services.py b/sugar-toolkit/src/sugar/tutorius/services.py new file mode 100644 index 0000000..467eca0 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/services.py @@ -0,0 +1,68 @@ +# 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 +""" +Services + +This module supplies services to be used by States, FSMs, Actions and Filters. + +Services provided are: +-Access to the running activity +-Access to the running tutorial +""" + + +class ObjectStore(object): + #Begin Singleton code + instance=None + def __new__(cls): + if not ObjectStore.instance: + ObjectStore.instance = ObjectStore.__ObjectStore() + + return ObjectStore.instance + + #End Singleton code + class __ObjectStore(object): + """ + The Object Store is a singleton class that allows access to + the current runnign activity and tutorial. + """ + def __init__(self): + self._activity = None + self._tutorial = None + #self._fsm_path = [] + + def set_activity(self, activity): + """Setter for activity""" + self._activity = activity + + def get_activity(self): + """Getter for activity""" + return self._activity + + activity = property(fset=set_activity,fget=get_activity,doc="activity") + + def set_tutorial(self, tutorial): + """Setter for tutorial""" + self._tutorial = tutorial + + def get_tutorial(self): + """Getter for tutorial""" + return self._tutorial + + tutorial = property(fset=set_tutorial,fget=get_tutorial,doc="tutorial") + + __doc__ = __ObjectStore.__doc__ diff --git a/sugar-toolkit/src/sugar/tutorius/tests/.coverage b/sugar-toolkit/src/sugar/tutorius/tests/.coverage new file mode 100644 index 0000000..eb89cb2 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/tests/.coverage @@ -0,0 +1 @@ +{0
\ No newline at end of file diff --git a/sugar-toolkit/src/sugar/tutorius/tests/coretests.py b/sugar-toolkit/src/sugar/tutorius/tests/coretests.py new file mode 100644 index 0000000..ed5a7c0 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/tests/coretests.py @@ -0,0 +1,197 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Michael Janelle-Montcalm <michael.jmontcalm@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 +""" +Core Tests + +This module contains all the tests that pertain to the usage of the Tutorius +Core. This means that the Event Filters, the Finite State Machine and all the +related elements and interfaces are tested here. + +""" + +import unittest + +import logging +from sugar.tutorius.actions import Action, OnceWrapper +from sugar.tutorius.core import * +from sugar.tutorius.filters import * + +# Helper classes to help testing +class SimpleTutorial(Tutorial): + """ + Fake tutorial + """ + def __init__(self, start_name="INIT"): + #Tutorial.__init__(self, "Simple Tutorial", None) + self.current_state_name = start_name + self.activity = "TODO : This should be an activity" + + def set_state(self, name): + self.current_state_name = name + +class TrueWhileActiveAction(Action): + """ + This action's active member is set to True after a do and to False after + an undo. + + Used to verify that a State correctly triggers the do and undo actions. + """ + def __init__(self): + self.active = False + + def do(self): + self.active = True + + def undo(self): + self.active = False + + +class CountAction(Action): + """ + This action counts how many times it's do and undo methods get called + """ + def __init__(self): + self.do_count = 0 + self.undo_count = 0 + + def do(self): + self.do_count += 1 + + def undo(self): + self.undo_count += 1 + +class TriggerEventFilter(EventFilter): + """ + This event filter can be triggered by simply calling its execute function. + + Used to fake events and see the effect on the FSM. + """ + def __init__(self, next_state): + EventFilter.__init__(self, next_state) + self.toggle_on_callback = False + + def install_handlers(self, callback, **kwargs): + """ + Forsakes the incoming callback function and just set the inner one. + """ + self._callback = self._inner_cb + + def _inner_cb(self, event_filter): + self.toggle_on_callback = not self.toggle_on_callback + +class OnceWrapperTests(unittest.TestCase): + def test_onceaction_toggle(self): + """ + Validate that the OnceWrapper wrapper works properly using the + CountAction + """ + act = CountAction() + wrap = OnceWrapper(act) + + assert act.do_count == 0, "do() should not have been called in __init__()" + assert act.undo_count == 0, "undo() should not have been called in __init__()" + + wrap.undo() + + assert act.undo_count == 0, "undo() should not be called if do() has not been called" + + wrap.do() + assert act.do_count == 1, "do() should have been called once" + + wrap.do() + assert act.do_count == 1, "do() should have been called only once" + + wrap.undo() + assert act.undo_count == 1, "undo() should have been called once" + + wrap.undo() + assert act.undo_count == 1, "undo() should have been called only once" + + +# State testing class +class StateTest(unittest.TestCase): + """ + This class has to test the State interface as well as the expected + functionality. + """ + + def test_action_toggle(self): + """ + Validate that the actions are properly done on setup and undone on + teardown. + + Pretty awesome. + """ + act = TrueWhileActiveAction() + + state = State("action_test", action_list=[act]) + + assert act.active == False, "Action is not initialized properly" + + state.setup() + + assert act.active == True, "Action was not triggered properly" + + state.teardown() + + assert act.active == False, "Action was not undone properly" + + def test_event_filter(self): + """ + Tests the fact that the event filters are correctly installed on setup + and uninstalled on teardown. + """ + event_filter = TriggerEventFilter("second_state") + + state = State("event_test", event_filter_list=[event_filter]) + state.set_tutorial(SimpleTutorial()) + + assert event_filter.toggle_on_callback == False, "Wrong init of event_filter" + assert event_filter._callback == None, "Event filter has a registered callback before installing handlers" + + state.setup() + + assert event_filter._callback != None, "Event filter did not register callback!" + + # 'Trigger' the event - This is more like a EventFilter test. + event_filter.do_callback() + + assert event_filter.toggle_on_callback == True, "Event filter did not execute callback" + + state.teardown() + + assert event_filter._callback == None, "Event filter did not remove callback properly" + + def test_warning_set_tutorial_twice(self): + """ + Calls set_tutorial twice and expects a warning on the second. + """ + state = State("start_state") + tut = SimpleTutorial("First") + tut2 = SimpleTutorial("Second") + + state.set_tutorial(tut) + + try: + state.set_tutorial(tut2) + assert False, "No RuntimeWarning was raised on second set_tutorial" + except : + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/sugar-toolkit/src/sugar/tutorius/tests/coretests.pyc b/sugar-toolkit/src/sugar/tutorius/tests/coretests.pyc Binary files differnew file mode 100644 index 0000000..5adf79e --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/tests/coretests.pyc diff --git a/sugar-toolkit/src/sugar/tutorius/tests/overlaytests.py b/sugar-toolkit/src/sugar/tutorius/tests/overlaytests.py new file mode 100644 index 0000000..b5fd209 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/tests/overlaytests.py @@ -0,0 +1,115 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Simon Poirier <simpoir@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 +""" +GUI Tests + +This module contains all the tests that pertain to the usage of the Tutorius +overlay mechanism used to display objects on top of the application. +""" + +import unittest + +import logging +import gtk, gobject +from sugar.tutorius.actions import Action, BubbleMessage +import sugar.tutorius.overlayer as overlayer + +class CanvasDrawable(object): + def __init__(self): + self._no_expose = False + self.exposition_count = 0 + def _set_no_expose(self, value): + self._no_expose = value + def draw_with_context(self, context): + self.exposition_count += 1 + no_expose = property(fset=_set_no_expose) + + +class OverlayerTest(unittest.TestCase): + def test_cairodrawable_iface(self): + """ + Quickly validates that all our cairo widgets have a minimal interface + implemented. + """ + drawables = [overlayer.TextBubble] + for widget in drawables: + for attr in filter(lambda s:s[0]!='_', dir(CanvasDrawable)): + assert hasattr(widget, attr), \ + "%s not implementing CanvasDrawable iface"%widget.__name__ + + + def test_drawn(self): + """ + Ensures a cairo widget draw method is called at least once in + a real gui app. + """ + win = gtk.Window(type=gtk.WINDOW_TOPLEVEL) + + btn = gtk.Button() + btn.show() + overlay = overlayer.Overlayer(btn) + win.add(overlay) + # let's also try to draw substitute button label + lbl = overlayer.TextBubble("test!") + assert lbl.label == 'test!', \ + "label property mismatch" + btn.show() + lbl.show() + btn.add(lbl) + + lbl.no_expose = True + assert lbl.no_expose, "wrong no_expose evaluation" + lbl.no_expose = False + assert not lbl.no_expose, "wrong no_expose evaluation" + + + widget = overlayer.TextBubble("testing msg!", tailpos=(10,-20)) + widget.exposition_count = 0 + # override draw method + def counter(ctx, self=widget): + self.exposition_count += 1 + self.real_exposer(ctx) + widget.real_exposer = widget.draw_with_context + widget.draw_with_context = counter + # centering allows to test the blending with the label + overlay.put(widget, 50, 50) + widget.show() + assert widget.no_expose, \ + "Overlay should overide exposition handling of widget" + assert not lbl.no_expose, \ + "Non-overlayed cairo should expose as usual" + + # force widget realization + # the child is flagged to be redrawn, the overlay should redraw too. + win.set_default_size(100, 100) + win.show() + + while gtk.events_pending(): + gtk.main_iteration(block=False) + # visual validation: there should be 2 visible bubbles, one as label, + # one as overlay + import time + time.sleep(1) + # as x11 events are asynchronous, wait a bit before assuming it went + # wrong. + while gtk.events_pending(): + gtk.main_iteration(block=False) + assert widget.exposition_count>0, "overlay widget should expose" + + +if __name__ == "__main__": + unittest.main() diff --git a/sugar-toolkit/src/sugar/tutorius/tests/overlaytests.pyc b/sugar-toolkit/src/sugar/tutorius/tests/overlaytests.pyc Binary files differnew file mode 100644 index 0000000..8d7d533 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/tests/overlaytests.pyc diff --git a/sugar-toolkit/src/sugar/tutorius/tests/run-tests.py b/sugar-toolkit/src/sugar/tutorius/tests/run-tests.py new file mode 100755 index 0000000..74efd64 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/tests/run-tests.py @@ -0,0 +1,12 @@ +#!/usr/bin/python +# This is a dumb script to run tests on the sugar-jhbuild installed files +# The path added is the default path for the jhbuild build + +import os, sys +sys.path.insert(0, + os.path.abspath("../../../../../../install/lib/python2.5/site-packages/") +) +import unittest +from coretests import * + +unittest.main() diff --git a/sugar-toolkit/src/sugar/tutorius/testwin.py b/sugar-toolkit/src/sugar/tutorius/testwin.py new file mode 100644 index 0000000..ef92b7f --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/testwin.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# Copyright (C) 2009, Tutorius.org +# +# 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 dragbox +import textbubble + +box = None + +def _destroy(widget, data=None): + gtk.main_quit() + +def _delete_event(widget, event, data=None): + print "quitting" + return False + +def blublu(widget, data=""): + print data + +def _drag_toggle(widget, data=None): + global box + box.dragMode = not box.dragMode + + +def addBtn(widget, data, bubble=0, btns=[0]): + if bubble == 1: + bt = textbubble.TextBubble("Bubble(%d)"%btns[0]) + else: + bt = gtk.Button("Bubble(%d)"%btns[0]) + ##bt.set_size_request(60,40) + bt.show() + data.attach(bt) + btns[0] += 1 + +def main(): + global box + win = gtk.Window(type=gtk.WINDOW_TOPLEVEL) + win.connect("delete_event", _delete_event) + win.connect("destroy", _destroy) + + win.set_default_size(800,600) + + vbox = gtk.VBox() + vbox.show() + win.add(vbox) + + check = gtk.CheckButton(label="dragMode") + check.connect("toggled", _drag_toggle) + check.show() + vbox.pack_start(check, expand=False) + + btnadd = gtk.Button("Add Bubble") + btnadd.show() + vbox.pack_start(btnadd, expand=False) + btnadd2 = gtk.Button("Add Button") + btnadd2.show() + vbox.pack_start(btnadd2, expand=False) + +## bubble = textbubble.TextBubble("Bubbles!") +## bubble.show() +## bubble.set_size_request(40,40) +## vbox.pack_start(bubble, expand=False) + + box = dragbox.DragBox() + box.set_border_width(10) + box.show() + vbox.pack_start(box, expand=True, fill=True) + + btnadd.connect("clicked", addBtn, box, 1) + btnadd2.connect("clicked", addBtn, box) + + win.show() + gtk.main() + + +if __name__ == "__main__": + main() + diff --git a/sugar-toolkit/src/sugar/tutorius/textbubble.py b/sugar-toolkit/src/sugar/tutorius/textbubble.py new file mode 100644 index 0000000..e09b298 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/textbubble.py @@ -0,0 +1,109 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 +""" +This module represents TextBubble widget. Also, it aims to be a short example +of drawing with Cairo. +""" + +import gtk +from math import pi as M_PI +import cairo + +# FIXME set as subclass of gtk.Widget, not EventBox +class TextBubble(gtk.EventBox): + def __init__(self, label): + gtk.EventBox.__init__(self) + + ##self.set_app_paintable(True) # else may be blank + # FIXME ensure previous call does not interfere with widget stacking + self.label = label + self.lineWidth = 5 + + self.connect("expose-event", self._on_expose) + + def __draw_with_cairo__(self, context): + """ + + """ + pass + + def _on_expose(self, widget, event): + """Redraw event callback.""" + # TODO + ctx = self.window.cairo_create() + + # set drawing region. Useless since this widget has its own window. + ##region = gtk.gdk.region_rectangle(self.allocation) + ##region.intersect(gtk.gdk.region_rectangle(event.area)) + ##ctx.region(region) + ##ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) + ##ctx.clip() + + ##import pdb; pdb.set_trace() + ##ctx.set_operator(cairo.OPERATOR_CLEAR) + ##ctx.paint() + ##ctx.set_operator(cairo.OPERATOR_OVER) + + width = self.allocation.width + height = self.allocation.height + xradius = width/2 + yradius = height/2 + width -= self.lineWidth + height -= self.lineWidth + ctx.move_to(self.lineWidth, yradius) + ctx.curve_to(self.lineWidth, self.lineWidth, + self.lineWidth, self.lineWidth, xradius, self.lineWidth) + ctx.curve_to(width, self.lineWidth, + width, self.lineWidth, width, yradius) + ctx.curve_to(width, height, width, height, xradius, height) + ctx.curve_to(self.lineWidth, height, + self.lineWidth, height, self.lineWidth, yradius) + ctx.set_source_rgb(1.0, 1.0, 1.0) + ctx.fill_preserve() + ctx.set_line_width(self.lineWidth) + ctx.set_source_rgb(0.0, 0.0, 0.0) + ctx.stroke() + + _, _, textWidth, textHeight, _, _ = ctx.text_extents(self._label) + ctx.move_to(int((self.allocation.width-textWidth)/2), + int((self.allocation.height+textHeight)/2)) + ctx.text_path(self._label) + ctx.fill() + + return True + + + def _set_label(self, value): + """Sets the label and flags the widget to be redrawn.""" + self._label = value + # FIXME hack to calculate size. necessary because may not have been + # realized + surf = cairo.SVGSurface("/dev/null", 0, 0) + ctx = cairo.Context(surf) + _, _, width, height, _, _ = ctx.text_extents(self._label) + del ctx, surf + + # FIXME bogus values follows + self.set_size_request(int(width+20), int(height+40)) + # TODO test changing a realized label + + def _get_label(self): + """Getter method for the label property""" + return self._label + + label = property(fget=_get_label, fset=_set_label,\ + doc="Text label which is to be painted on the top of the widget") + diff --git a/sugar-toolkit/src/sugar/tutorius/textbubble.pyc b/sugar-toolkit/src/sugar/tutorius/textbubble.pyc Binary files differnew file mode 100644 index 0000000..c16cb0f --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/textbubble.pyc diff --git a/sugar-toolkit/src/sugar/tutorius/tutorial.py b/sugar-toolkit/src/sugar/tutorius/tutorial.py new file mode 100644 index 0000000..5236127 --- /dev/null +++ b/sugar-toolkit/src/sugar/tutorius/tutorial.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 gtk +import logging + +from sugar.tutorius.dialog import TutoriusDialog + + +logger = logging.getLogger("tutorius") + +class Event: + def __init__(self, object_name, event_name ): + self.object_name = object_name + self.event_name = event_name + + def test(self, sig, name): + if self.object_name == name and self.event_name == sig: + return True + return False + + +class Tutorial (object): + 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): + object.__init__(self) + self.name = name + self.state_machine = fsm + + self.handlers = [] + self.activity = None + #self.setState("INIT") + #self.state="INIT" + #self.register_signals(self.activity, self.handleEvent, max_depth=10) + + def attach(self, activity): + #For now, absolutely detach if a previous one! + if self.activity: + self.detach() + self.activity = activity + self.state="INIT" + self.register_signals(self.activity,self.handleEvent, max_depth=10) + + def detach(self): + self.disconnectHandlers() + self.activity = None + + def handleEvent(self, *args): + 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): + logger.debug("====NEW STATE: %s====" % next) + self.state = next + dlg = TutoriusDialog(self.state_machine[self.state]["Message"]) + dlg.setButtonClickedCallback(dlg.closeSelf) + dlg.run() + +# @staticmethod +# def logEvent(obj, *args): +# logger.debug("%s" % str(args[-1])) + + def disconnectHandlers(self): + for t, id in self.handlers: + t.disconnect_handler(id) + +# def setState(self,name): +# self.disconnectHandlers() +# self.state = name +# newstate = ABIWORD_MEF.get(name,()) +# for event, n in newstate: +# target = self.activity +# try: +# for obj in event.object_name.split("."): +# target = getattr(target,obj) +# id = target.connect(self.handler,(event.object_name, event.event_name)) +# self.handlers.append(target, id) +# id = target.connect(Tutorial.logEvent,"EVENT %s ON %s" % (event.object_name, event.event_name)) +# self.handlers.append(target, id) +# except Exception, e: +# logger.debug(str(e)) + + def register_signals(self,object,handler,prefix=None,max_depth=None): + """ + Recursive function to register event handlers on an object + 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 object 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 objects + -Handler ID's stored in self.handlers + + @param object the object to recurse on + @param handler the handler function to connect + @param prefix name prepended to the object name to form a chain + @param max_depth maximum recursion depth, None for infinity + """ + #Gtk Containers have a get_children() function + if hasattr(object,"get_children") and \ + hasattr(object.get_children,"__call__"): + for child in object.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, object.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, object.get_name()) \ + if not (p is None or p in Tutorial.IGNORED_WIDGETS)] \ + ) + #register events on the object if a widget XXX necessary to check this? + if isinstance(object,gtk.Widget): + for sig in Tutorial.EVENTS: + try: + self.handlers.append( (object,object.connect(sig,handler,(sig, name) )) ) + except TypeError: + continue + + |