From f347ec202fe5b4404fa380694d7fe3d3d070ae7b Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Mon, 18 May 2009 20:27:41 +0000 Subject: fixed major missing parts in bundler integrated bundler to creator tutorial loading still is unusable (fiters don't seem to load) --- (limited to 'src') diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py index 3e2d3d4..fd6f4ab 100644 --- a/src/sugar/activity/activity.py +++ b/src/sugar/activity/activity.py @@ -76,8 +76,9 @@ from sugar.graphics.xocolor import XoColor from sugar.datastore import datastore from sugar.session import XSMPClient from sugar import wm +from sugar.tutorius.creator import Creator from sugar.tutorius.services import ObjectStore -from sugar.tutorius.tutoserialize import TutoSerializer +from sugar.tutorius.bundler import TutorialStore, XMLSerializer _ = lambda msg: gettext.dgettext('sugar-toolkit', msg) @@ -124,17 +125,23 @@ class ActivityToolbar(gtk.Toolbar): self.insert(separator, -1) separator.show() + self.creator_button = ToolButton("tutortool") + self.creator_button.set_tooltip(_('Start tutorial creator')) + self.creator_button.connect("clicked", Creator.launch) + self.insert(self.creator_button, -1) + self.creator_button.show() + if hasattr(self._activity,"get_tutorials") and hasattr(self._activity.get_tutorials,"__call__"): self.tutorials = ToolComboBox(label_text=_('Tutorials:')) self.tutorials.combo.connect('changed', self.__tutorial_changed_cb) # Get tutorial list by file - logging.debug("************************************ before creating serialize") - serialize = TutoSerializer() - logging.debug("************************************ before calling load_tuto_list()") + store = TutorialStore() #tutorials = self._activity.get_tutorials() if getattr(self._activity,"_tutorials",None) is None: - tutorials = serialize.load_tuto_list() + tutorials = store.list_available_tutorials( + get_bundle_name(), + os.environ['SUGAR_BUNDLE_VERSION']) self._current_tutorial = None if tutorials: @@ -207,32 +214,23 @@ class ActivityToolbar(gtk.Toolbar): """ Callback for tutorial combobox item change """ - logging.debug("************ function __tutorial_changed_cb called") - serialize = TutoSerializer() - + store = TutorialStore() + if self._current_tutorial: self._current_tutorial.detach() model = self.tutorials.combo.get_model() it = self.tutorials.combo.get_active_iter() - (key,) = model.get(it, 0) - - #Load and build chosen tutorial from Pickle file - logging.debug("****************** before tuto build") -## tutorials = self._activity.get_tutorials() - tuto = serialize.build_tutorial(key) - self._activity._tutorials = tuto - logging.debug("****************** after tuto build") -## tutorial = self._activity.get_tutorials().get(key,None) - tutorial = tuto.get(key, None) - - if not getattr(self._activity,"_tutorials",None) is None: - if not self._current_tutorial is None: - self._current_tutorial.detach() - - self._current_tutorial = tutorial - logging.debug(" *************** try to attach tuto") - self._current_tutorial.attach(self._activity) + (guid,) = model.get(it, 0) + + tutorial = store.load_tutorial(guid) + + if not self._current_tutorial is None: + self._current_tutorial.detach() + + self._current_tutorial = tutorial + logging.debug(" *************** try to attach tuto") + self._current_tutorial.attach(self._activity) def __keep_clicked_cb(self, button): diff --git a/src/sugar/graphics/window.py b/src/sugar/graphics/window.py index a17ebcc..17a6dba 100644 --- a/src/sugar/graphics/window.py +++ b/src/sugar/graphics/window.py @@ -98,7 +98,6 @@ class Window(gtk.Window): self._hbox.pack_start(self._event_box) self._event_box.show() -## self.add(self._vbox) self._vbox.show() self._overlayer = Overlayer(self._vbox) diff --git a/src/sugar/tutorius/Makefile.am b/src/sugar/tutorius/Makefile.am index 7223c60..65b20f9 100644 --- a/src/sugar/tutorius/Makefile.am +++ b/src/sugar/tutorius/Makefile.am @@ -11,7 +11,8 @@ sugar_PYTHON = \ services.py \ overlayer.py \ editor.py \ - linear_creator.py \ constraints.py \ properties.py \ - bundler.py + creator.py \ + bundler.py \ + linear_creator.py diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py index ff7f427..570bff8 100644 --- a/src/sugar/tutorius/actions.py +++ b/src/sugar/tutorius/actions.py @@ -24,6 +24,112 @@ import overlayer from sugar.tutorius.editor import WidgetIdentifier from sugar.tutorius.services import ObjectStore from sugar.tutorius.properties import * +import gtk.gdk + +class DragWrapper(object): + """Wrapper to allow gtk widgets to be dragged around""" + def __init__(self, widget, position, draggable=False): + """ + Creates a wrapper to allow gtk widgets to be mouse dragged, if the + parent container supports the move() method, like a gtk.Layout. + @param widget the widget to enhance with drag capability + @param position the widget's position. Will translate the widget if needed + @param draggable wether to enable the drag functionality now + """ + self._widget = widget + self._eventbox = None + self._drag_on = False # whether dragging is enabled + self._rel_pos = (0,0) # mouse pos relative to widget + self._handles = [] # event handlers + self._dragging = False # whether a drag is in progress + self.position = position # position of the widget + + self.draggable = draggable + + def _pressed_cb(self, widget, evt): + """Callback for start of drag event""" + self._eventbox.grab_add() + self._dragging = True + self._rel_pos = evt.get_coords() + + def _moved_cb(self, widget, evt): + """Callback for mouse drag events""" + if not self._dragging: + return + + # Focus on a widget before dragging another would + # create addititonal move event, making the widget jump unexpectedly. + # Solution found was to process those focus events before dragging. + if gtk.events_pending(): + return + + xrel, yrel = self._rel_pos + xparent, yparent = evt.get_coords() + xparent, yparent = widget.translate_coordinates(widget.parent, + xparent, yparent) + self.position = (xparent-xrel, yparent-yrel) + self._widget.parent.move(self._eventbox, *self.position) + self._widget.parent.move(self._widget, *self.position) + self._widget.parent.queue_draw() + + def _released_cb(self, *args): + """Callback for end of drag (mouse release).""" + self._eventbox.grab_remove() + self._dragging = False + + def _drag_end(self, *args): + """Callback for end of drag (stolen focus).""" + self._dragging = False + + def set_draggable(self, value): + """Setter for the draggable property""" + if bool(value) ^ bool(self._drag_on): + if value: + self._eventbox = gtk.EventBox() + self._eventbox.show() + self._eventbox.set_visible_window(False) + size = self._widget.size_request() + self._eventbox.set_size_request(*size) + self._widget.parent.put(self._eventbox, *self.position) + self._handles.append(self._eventbox.connect( + "button-press-event", self._pressed_cb)) + self._handles.append(self._eventbox.connect( + "button-release-event", self._released_cb)) + self._handles.append(self._eventbox.connect( + "motion-notify-event", self._moved_cb)) + self._handles.append(self._eventbox.connect( + "grab-broken-event", self._drag_end)) + else: + while len(self._handles): + handle = self._handles.pop() + self._eventbox.disconnect(handle) + self._eventbox.parent.remove(self._eventbox) + self._eventbox.destroy() + self._eventbox = None + self._drag_on = value + + def get_draggable(self): + """Getter for the draggable property""" + return self._drag_on + + draggable = property(fset=set_draggable, fget=get_draggable, \ + doc="Property to enable the draggable behaviour of the widget") + + def set_widget(self, widget): + """Setter for the widget property""" + if self._dragging or self._drag_on: + raise Exception("Can't change widget while dragging is enabled.") + + assert hasattr(widget, "parent"), "wrapped widget should have a parent" + parent = widget.parent + assert hasattr(parent, "move"), "container of widget need move method" + self._widget = widget + + def get_widget(self): + """Getter for the widget property""" + return self._widget + + widget = property(fset=set_widget, fget=get_widget) class Action(object): """Base class for Actions""" @@ -56,6 +162,13 @@ class Action(object): self.properties[i] = getattr(self,i) return self.properties.keys() + def enter_editmode(self, **kwargs): + """ + Enters edit mode. The action should display itself in some way, + without affecting the currently running application. + """ + raise NotImplementedError("Not implemented") + class OnceWrapper(object): """ Wraps a class to perform an action once only @@ -142,6 +255,7 @@ class BubbleMessage(Action): self.overlay = None self._bubble = None self._speaker = None + self.__drag = None def do(self): """ @@ -154,12 +268,14 @@ class BubbleMessage(Action): # handled either by rendering over them, or by finding different way to # draw the overlay. + if not self.overlay: + self.overlay = ObjectStore().activity._overlayer if not self._bubble: - x, y = self._position + x, y = self.position.value # 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 = overlayer.TextBubble(text=self.message.value, + tailpos=self.tail_pos.value) self._bubble.show() self.overlay.put(self._bubble, x, y) self.overlay.queue_draw() @@ -171,7 +287,34 @@ class BubbleMessage(Action): if self._bubble: self._bubble.destroy() self._bubble = None - + + def enter_editmode(self, *args): + """ + Enters edit mode. The action should display itself in some way, + without affecting the currently running application. + """ + if not self.overlay: + self.overlay = ObjectStore().activity._overlayer + assert not self.__drag, "bubble action set to editmode twice" + x, y = self.position.value + self._bubble = overlayer.TextBubble(text=self.message.value, + tailpos=self.tail_pos.value) + self.overlay.put(self._bubble, x, y) + self._bubble.show() + + self.__drag = DragWrapper(self._bubble, self.position.value, True) + + def exit_editmode(self, *args): + x,y = self.__drag.position + self.position.set([int(x), int(y)]) + if self.__drag: + self.__drag.draggable = False + self.__drag = None + if self._bubble: + self.overlay.remove(self._bubble) + self._bubble = None + self.overlay = None + class WidgetIdentifyAction(Action): def __init__(self): self.activity = None diff --git a/src/sugar/tutorius/bundler.py b/src/sugar/tutorius/bundler.py index f9a3911..0eb6b64 100644 --- a/src/sugar/tutorius/bundler.py +++ b/src/sugar/tutorius/bundler.py @@ -25,14 +25,16 @@ import os import uuid import xml.dom.minidom -from sugar.tutorius import gtkutils, overlayer +from sugar.tutorius import gtkutils, overlayer, tutorial from sugar.tutorius.core import Tutorial, State, FiniteStateMachine from sugar.tutorius.filters import * from sugar.tutorius.actions import * from ConfigParser import SafeConfigParser +# this is where user installed/generated tutorials will go def _get_store_root(): - return os.path.join(os.getenv("SUGAR_PREFIX"),"share","tutorius","data") + return os.path.join(os.getenv("HOME"),".sugar",os.getenv("SUGAR_PROFILE"),"tutorius","data") +# this is where activity bundled tutorials should be, under the activity bundle def _get_bundle_root(): return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data") @@ -41,62 +43,76 @@ INI_METADATA_SECTION = "GENERAL_METADATA" INI_GUID_PROPERTY = "GUID" INI_NAME_PROPERTY = "NAME" INI_XML_FSM_PROPERTY = "FSM_FILENAME" +INI_FILENAME = "meta.ini" +TUTORIAL_FILENAME = "tutorial.xml" -class TutorialStore: +class TutorialStore(object): - def list_avaible_tutorials(self, activity_name, activity_vers): + def list_available_tutorials(self, activity_name, activity_vers): """ - Recuperate the list of all tutorials present on disk for a + Generate the list of all tutorials present on disk for a given activity. + + @returns a map of tutorial {names : GUID}. """ - - store_root = _get_store_root() - bundle_root = _get_bundle_root() - - logging.debug("*********** Path of store_root : " + store_root) - - # Create /data/tutorius if no exists - if not os.path.exists(store_root): - os.mkdir(store_root) - logging.debug("************* Creating %s folder" % store_root) - + # check both under the activity data and user installed folders + paths = [_get_store_root(), _get_bundle_root()] + tutoGuidName = {} - - # iterate in each GUID subfolder - for dir in os.listdir(store_root): - # iterate for each ".ini" file in the activity store_root folder - for file_name in os.listdir(store_root + "/" + dir): - - if file_name.endswith(".ini"): - logging.debug("************** .ini file found : " + file_name) - # Filter for just .ini files who metadata ACTIVITY_NAME - # match 'activity_name' given in argument. - config = SafeConfigParser() - config.read(file_name) - # Get all activity tuples (Activity_Name: Activity_Version) - file_activity_tuples = config.items(INI_ACTIVITY_SECTION) - - for i in range(0, len(file_activity_tuples) - 1): - - if file_activity_tuples[i][0] == activity_name and \ - int(file_activity_tuples[i][1]) == activity_vers: - # Add this tutorial guid and name in the dictionary - file_activity_guid = config.get(INI_METADATA_SECTION, - INI_GUID_PROPERTY) - file_activity_name = config.get(INI_METADATA_SECTION, - INI_NAME_PROPERTY) - tutoGuidName[file_activity_name] = file_activity_guid + + for repository in paths: + # (our) convention dictates that tutorial folders are named + # with their GUID (for unicity) but this is not enforced. + try: + for tuto in os.listdir(repository): + parser = SafeConfigParser() + parser.read(os.path.join(repository, tuto, INI_FILENAME)) + guid = parser.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) + name = parser.get(INI_METADATA_SECTION, INI_NAME_PROPERTY) + activities = parser.options(INI_ACTIVITY_SECTION) + # enforce matching activity name AND version, as UI changes + # break tutorials. We may lower this requirement when the + # UAM gets less dependent on the widget order. + # Also note property names are always stored lowercase. + if activity_name.lower() in activities: + version = parser.get(INI_ACTIVITY_SECTION, activity_name) + if activity_vers == version: + tutoGuidName[guid] = name + except OSError: + # the repository may not exist. Continue scanning + pass return tutoGuidName - -class Serializer: + def load_tutorial(self, Guid): + """ + Rebuilds a tutorial object from it's serialized state. + Common storing paths will be scanned. + + @param Guid the generic identifier of the tutorial + @returns a Tutorial object containing an FSM + """ + bundle = TutorialBundler(Guid) + bundle_path = bundle.get_tutorial_path() + config = SafeConfigParser() + config.read(os.path.join(bundle_path, INI_FILENAME)) + + serializer = XMLSerializer() + + name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY) + fsm = serializer.load_fsm(Guid) + + tuto = Tutorial(name, fsm) + return tuto + + +class Serializer(object): """ Interface that provide serializing and deserializing of the FSM used in the tutorials to/from disk. Must be inherited. """ - def save_fsm(self,fsm, guid = None): + def save_fsm(self,fsm): """ Save fsm to disk. If a GUID parameter is provided, the existing GUID is located in the .ini files in the store root and bundle root and @@ -106,7 +122,7 @@ class Serializer: """ NotImplementedError - def load_fsm(self, guid): + def load_fsm(self): """ Load fsm from disk. """ @@ -143,9 +159,7 @@ class XMLSerializer(Serializer): # Write down just the name of the Action class as the Class # property -- - # Using .__class__ since type() doesn't have the same behavior - # with class derivating from object and class that don't - actionNode.setAttribute("Class", str(action.__class__)) + actionNode.setAttribute("Class",type(action).__name__) if type(action) is DialogMessage: actionNode.setAttribute("Message", action.message.value) @@ -211,9 +225,7 @@ class XMLSerializer(Serializer): # Write down just the name of the Action class as the Class # property -- - # using .__class__ since type() doesn't have the same behavior - # with class derivating from object and class that don't - eventFilterNode.setAttribute("Class", str(event_f.__class__)) + eventFilterNode.setAttribute("Class", type(event_f).__name__) # Write the name of the next state eventFilterNode.setAttribute("NextState", event_f.next_state) @@ -365,38 +377,38 @@ class XMLSerializer(Serializer): description """ # TO ADD: an elif for each type of action - if action.getAttribute("Class") == str(DialogMessage): + if action.getAttribute("Class") == 'DialogMessage': message = action.getAttribute("Message") positionX = int(action.getAttribute("PositionX")) positionY = int(action.getAttribute("PositionY")) position = [positionX, positionY] return DialogMessage(message,position) - elif action.getAttribute("Class") == str(BubbleMessage): + elif action.getAttribute("Class") == 'BubbleMessage': message = action.getAttribute("Message") positionX = int(action.getAttribute("PositionX")) positionY = int(action.getAttribute("PositionY")) position = [positionX, positionY] - tail_posX = action.getAttribute("Tail_posX") - tail_posY = action.getAttribute("Tail_posY") + tail_posX = int(action.getAttribute("Tail_posX")) + tail_posY = int(action.getAttribute("Tail_posY")) tail_pos = [tail_posX, tail_posY] return BubbleMessage(message,position,None,tail_pos) - elif action.getAttribute("Class") == str(WidgetIdentifyAction): + elif action.getAttribute("Class") == 'WidgetIdentifyAction': return WidgetIdentifyAction() - elif action.getAttribute("Class") == str(ChainAction): + elif action.getAttribute("Class") == 'ChainAction': # Load the subactions subActionsList = self._load_xml_actions(action.getElementsByTagName("Actions")[0]) return ChainAction(subActionsList) - elif action.getAttribute("Class") == str(DisableWidgetAction): + elif action.getAttribute("Class") == 'DisableWidgetAction': # Get the target targetName = action.getAttribute("Target") return DisableWidgetAction(targetName) - elif action.getAttribute("Class") == str(TypeTextAction): + elif action.getAttribute("Class") == 'TypeTextAction': # Get the widget and the text to type widget = action.getAttribute("Widget") text = action.getAttribute("Text") return TypeTextAction(widget, text) - elif action.getAttribute("Class") == str(ClickAction): + elif action.getAttribute("Class") == 'ClickAction': # Load the widget to click widget = action.getAttribute("Widget") @@ -484,7 +496,7 @@ class XMLSerializer(Serializer): tutorial_dir = self._find_tutorial_dir_with_guid(guid) # Open the XML file - tutorial_file = os.path.join(tutorial_dir, "fsm.xml") + tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME) xml_dom = xml.dom.minidom.parse(tutorial_file) @@ -493,7 +505,7 @@ class XMLSerializer(Serializer): return self._load_xml_fsm(fsm_elem) -class TutorialBundler: +class TutorialBundler(object): """ This class provide the various data handling methods useable by the tutorial editor. @@ -506,7 +518,7 @@ class TutorialBundler: a new GUID will be generated, """ - self.Guid = generated_guid or uuid.uuid1() + self.Guid = generated_guid or str(uuid.uuid1()) #Look for the file in the path if a uid is supplied if generated_guid: @@ -524,48 +536,28 @@ class TutorialBundler: else: #Create the folder, any failure will go through to the caller for now - store_path = os.path.join(_get_store_root(), generated_guid) - os.mkdir(store_path) + store_path = os.path.join(_get_store_root(), self.Guid) + os.makedirs(store_path) self.Path = store_path - - def __SetGuid(self, value): - self.__guid = value - - def __GetGuid(self): - return self.__guid - - def __DelGuid(self): - del self.__guid - - def __SetPath(self, value): - self.__path = value - - def __GetPath(self): - return self.__path - - def __DelPath(self): - del self.__path - - Guid = property(fget=__SetGuid, - fset=__GetGuid, - fdel=__DelGuid, - doc="The guid associated with the Tutoria_Bundler") - - Path = property(fget=__SetPath, - fset=__GetPath, - fdel=__DelPath, - doc="The path associated with the Tutoria_Bundler") - - - def write_metadata_file(self, data): - """ - Write metadata to a property file. If a GUID is provided, TutorialBundler - will try to find and overwrite the existing property file who contain the - given GUID, and will raise an exception if it cannot find it. - """ - NotImplementedError - + def write_metadata_file(self, tutorial): + """ + Write metadata to the property file. + @param tutorial Tutorial for which to write metadata + """ + #Create the Config Object and populate it + cfg = SafeConfigParser() + cfg.add_section(INI_METADATA_SECTION) + cfg.set(INI_METADATA_SECTION, INI_GUID_PROPERTY, self.Guid) + cfg.set(INI_METADATA_SECTION, INI_NAME_PROPERTY, tutorial.name) + cfg.set(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY, TUTORIAL_FILENAME) + cfg.add_section(INI_ACTIVITY_SECTION) + cfg.set(INI_ACTIVITY_SECTION, os.environ['SUGAR_BUNDLE_NAME'], + os.environ['SUGAR_BUNDLE_VERSION']) + + #Write the ini file + cfg.write( file( os.path.join(self.Path, INI_FILENAME), 'w' ) ) + def get_tutorial_path(self): """ Return the path of the .ini file associated with the guiven guid set in @@ -587,16 +579,16 @@ class TutorialBundler: # iterate for each .ini file in the store_root folder - for file_name in os.listdir(store_root + "/" + dir): + for file_name in os.listdir(os.path.join(store_root, dir)): if file_name.endswith(".ini"): logging.debug("******************* Found .ini file : " \ + file_name) - config.read(file_name) + config.read(os.path.join(store_root, dir, file_name)) if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == self.Guid: xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY) - path = os.path.join(store_root, self.Guid) + path = os.path.join(store_root, dir) return path logging.debug("************ Path of bundle_root folder of activity : " \ @@ -607,12 +599,12 @@ class TutorialBundler: for dir in os.listdir(bundle_root): # iterate for each .ini file in the bundle_root folder - for file_name in os.listdir(bundle_root + "/" + dir): + for file_name in os.listdir(os.path.join(bundle_root, dir)): if file_name.endswith(".ini"): logging.debug("******************* Found .ini file : " \ + file_name) - config.read(file_name) - if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == guid: + config.read(os.path.join(bundle_root, dir, file_name)) + if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == self.Guid: path = os.path.join(bundle_root, self.Guid) return path @@ -620,25 +612,24 @@ class TutorialBundler: logging.debug("**************** Error : GUID not found") raise KeyError - def write_fsm(self, fsm, guid=None): + def write_fsm(self, fsm): """ Save fsm to disk. If a GUID parameter is provided, the existing GUID is located in the .ini files in the store root and bundle root and the corresponding FSM is/are created or overwritten. If the GUID is not found, an exception occur. - """ - + """ + config = SafeConfigParser() - - if guid is not None: - serializer = XMLSerializer() - path = get_tutorial_path() + "/meta.ini" - config.read(path) - xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY) - serializer.save_fsm(fsm, xml_filename, store_root) - - + + serializer = XMLSerializer() + path = os.path.join(self.Path, "meta.ini") + config.read(path) + xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY) + serializer.save_fsm(fsm, xml_filename, self.Path) + + def add_resources(self, typename, file): """ Add ressources to metadata. diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py index 602947f..dd2435e 100644 --- a/src/sugar/tutorius/core.py +++ b/src/sugar/tutorius/core.py @@ -123,7 +123,7 @@ class State(object): with associated actions that point to a possible next state. """ - def __init__(self, name, action_list=None, event_filter_list=None, tutorial=None): + def __init__(self, name="", action_list=None, event_filter_list=None, tutorial=None): """ Initializes the content of the state, like loading the actions that are required and building the correct tests. @@ -525,4 +525,4 @@ class FiniteStateMachine(State): out_string = "" for st in self._states.itervalues(): out_string += st.name + ", " - return out_string \ No newline at end of file + return out_string diff --git a/src/sugar/tutorius/creator.py b/src/sugar/tutorius/creator.py new file mode 100644 index 0000000..f24257e --- /dev/null +++ b/src/sugar/tutorius/creator.py @@ -0,0 +1,416 @@ +""" +This package contains UI classes related to tutorial authoring. +This includes visual display of tools to edit and create tutorials from within +the activity itself. +""" +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Simon Poirier +# +# +# 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 1 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.gdk +import gobject +from gettext import gettext as T + +from sugar.graphics.toolbutton import ToolButton + +from sugar.tutorius import overlayer, gtkutils, actions, bundler, properties +from sugar.tutorius import filters +from sugar.tutorius.services import ObjectStore +from sugar.tutorius.linear_creator import LinearCreator +from sugar.tutorius.tutorial import Tutorial + +insertable_actions = { + "MessageBubble" : actions.BubbleMessage +} + +class Creator(object): + """ + Class acting as a bridge between the creator, serialization and core + classes. This contains most of the UI part of the editor. + """ + def __init__(self, activity, tutorial=None): + """ + Instanciate a tutorial creator for the activity. + + @param activity to bind the creator to + @param tutorial an existing tutorial to edit, or None to create one + """ + self._activity = activity + if not tutorial: + self._tutorial = LinearCreator() + else: + self._tutorial = tutorial + + self._action_panel = None + self._current_filter = None + self._intro_mask = None + self._intro_handle = None + self._state_bubble = overlayer.TextBubble(self._tutorial.state_name) + allocation = self._activity.get_allocation() + self._width = allocation.width + self._height = allocation.height + self._selected_widget = None + self._eventmenu = None + + self._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5)) + self._activity._overlayer.put(self._hlmask, 0, 0) + + self._activity._overlayer.put(self._state_bubble, + self._width/2-self._state_bubble.allocation.width/2, 0) + + dlg_width = 300 + dlg_height = 70 + sw = gtk.gdk.screen_width() + sh = gtk.gdk.screen_height() + self._tooldialog = gtk.Window() + self._tooldialog.set_title("Tutorius tools") + self._tooldialog.set_transient_for(self._activity) + self._tooldialog.set_decorated(True) + self._tooldialog.set_resizable(False) + self._tooldialog.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY) + self._tooldialog.set_destroy_with_parent(True) + self._tooldialog.set_deletable(False) + self._tooldialog.set_size_request(dlg_width, dlg_height) + + toolbar = gtk.Toolbar() + toolitem = ToolButton("message-bubble") + toolitem.set_tooltip("Message Bubble") + toolitem.connect("clicked", self._add_action_cb, "MessageBubble") + toolbar.insert(toolitem, -1) + toolitem = ToolButton("go-next") + toolitem.connect("clicked", self._add_step_cb) + toolitem.set_tooltip("Add Step") + toolbar.insert(toolitem, -1) + toolitem = ToolButton("stop") + toolitem.connect("clicked", self._cleanup_cb) + toolitem.set_tooltip("End Tutorial") + toolbar.insert(toolitem, -1) + self._tooldialog.add(toolbar) + self._tooldialog.show_all() + # simpoir: I suspect the realized widget is a tiny bit larger than + # it should be, thus the -10. + self._tooldialog.move(sw-10-dlg_width, sh-dlg_height) + + self._propedit = EditToolBox(self._activity) + + def _evfilt_cb(self, menuitem, event_name, *args): + """ + This will get called once the user has selected a menu item from the + event filter popup menu. This should add the correct event filter + to the FSM and increment states. + """ + self.introspecting = False + eventfilter = filters.GtkWidgetEventFilter( + next_state=None, + object_id=self._selected_widget, + event_name=event_name) + # undo actions so they don't persist through step editing + for action in self._tutorial.current_actions: + action.exit_editmode() + self._tutorial.event(eventfilter) + self._state_bubble.label = self._tutorial.state_name + self._hlmask.covered = None + self._propedit.action = None + self._activity.queue_draw() + + def _intro_cb(self, widget, evt): + """ + Callback for capture of widget events, when in introspect mode. + """ + if evt.type == gtk.gdk.BUTTON_PRESS: + # widget has focus, let's hilight it + win = gtk.gdk.display_get_default().get_window_at_pointer() + click_wdg = win[0].get_user_data() + if not click_wdg.is_ancestor(self._activity._overlayer): + # as popups are not (yet) supported, it would break + # badly if we were to play with a widget not in the + # hierarchy. + return + for hole in self._intro_mask.pass_thru: + self._intro_mask.mask(hole) + self._intro_mask.unmask(click_wdg) + self._selected_widget = gtkutils.raddr_lookup(click_wdg) + + if self._eventmenu: + self._eventmenu.destroy() + self._eventmenu = gtk.Menu() + menuitem = gtk.MenuItem(label=type(click_wdg).__name__) + menuitem.set_sensitive(False) + self._eventmenu.append(menuitem) + self._eventmenu.append(gtk.MenuItem()) + + for item in gobject.signal_list_names(click_wdg): + menuitem = gtk.MenuItem(label=item) + menuitem.connect("activate", self._evfilt_cb, item) + self._eventmenu.append(menuitem) + self._eventmenu.show_all() + self._eventmenu.popup(None, None, None, evt.button, evt.time) + self._activity.queue_draw() + + def set_intropecting(self, value): + """ + Set whether creator is in UI introspection mode. Setting this will + connect necessary handlers. + @param value True to setup introspection handlers. + """ + if bool(value) ^ bool(self._intro_mask): + if value: + self._intro_mask = overlayer.Mask(catch_events=True) + self._intro_handle = self._intro_mask.connect_after( + "button-press-event", self._intro_cb) + self._activity._overlayer.put(self._intro_mask, 0, 0) + else: + self._intro_mask.catch_events = False + self._intro_mask.disconnect(self._intro_handle) + self._intro_handle = None + self._activity._overlayer.remove(self._intro_mask) + self._intro_mask = None + + def get_introspecting(self): + """ + Whether creator is in UI introspection (catch all event) mode. + @return True if introspection handlers are connected, or False if not. + """ + return bool(self._intro_mask) + + introspecting = property(fset=set_intropecting, fget=get_introspecting) + + def _add_action_cb(self, widget, actiontype): + """Callback for the action creation toolbar tool""" + action = actions.BubbleMessage("Bubble") + action.enter_editmode() + self._tutorial.action(action) + # TODO: replace following with event catching + action._BubbleMessage__drag._eventbox.connect_after( + "button-release-event", self._action_refresh_cb, action) + + def _action_refresh_cb(self, widget, evt, action): + """ + Callback for refreshing properties values and notifying the + property dialog of the new values. + """ + action.exit_editmode() + action.enter_editmode() + self._activity.queue_draw() + # TODO: replace following with event catching + action._BubbleMessage__drag._eventbox.connect_after( + "button-release-event", self._action_refresh_cb, action) + self._propedit.action = action + + def _add_step_cb(self, widget): + """Callback for the "add step" tool""" + self.introspecting = True + + def _cleanup_cb(self, *args): + """ + Quit editing and cleanup interface artifacts. + """ + self.introspecting = False + eventfilter = filters.EventFilter(None) + # undo actions so they don't persist through step editing + for action in self._tutorial.current_actions: + action.exit_editmode() + self._tutorial.event(eventfilter) + + dlg = TextInputDialog(text=T("Enter a tutorial title."), + field=T("Title")) + tutorialName = "" + while not tutorialName: tutorialName = dlg.pop() + dlg.destroy() + + # prepare tutorial for serialization + tuto = Tutorial(tutorialName, self._tutorial.fsm) + bundle = bundler.TutorialBundler() + bundle.write_metadata_file(tuto) + bundle.write_fsm(self._tutorial.fsm) + + # remove UI remains + self._hlmask.covered = None + self._activity._overlayer.remove(self._hlmask) + self._activity._overlayer.remove(self._state_bubble) + self._hlmask.destroy() + self._hlmask = None + self._tooldialog.destroy() + self._propedit.destroy() + self._activity.queue_draw() + del self._activity._creator + + def launch(*args, **kwargs): + """ + Launch and attach a creator to the currently running activity. + """ + activity = ObjectStore().activity + if not hasattr(activity, "_creator"): + activity._creator = Creator(activity) + launch = staticmethod(launch) + +class EditToolBox(gtk.Window): + """Helper toolbox class for managing action properties""" + def __init__(self, parent, action=None): + """ + Create the property edition toolbox and display it. + + @param parent the parent window of this toolbox, usually an activity + @param action the action to introspect/edit + """ + gtk.Window.__init__(self) + self._action = None + self.__parent = parent # private avoid gtk clash + + self.set_title("Action Properties") + self.set_transient_for(parent) + self.set_decorated(True) + self.set_resizable(False) + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY) + self.set_destroy_with_parent(True) + self.set_deletable(False) + self.set_size_request(200, 400) + + self._vbox = gtk.VBox() + self.add(self._vbox) + propwin = gtk.ScrolledWindow() + propwin.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC + propwin.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC + self._vbox.pack_start(propwin) + self._propbox = gtk.VBox(spacing=10) + propwin.add(self._propbox) + + self.action = action + + sw = gtk.gdk.screen_width() + sh = gtk.gdk.screen_height() + + self.show_all() + self.move(sw-10-200, (sh-400)/2) + + def refresh(self): + """Refresh property values from the selected action.""" + if self._action is None: + return + props = self._action.get_properties() + for propnum in xrange(len(props)): + row = self._propbox.get_children()[propnum] + prop = self._action.properties[props[propnum]] + if isinstance(prop, properties.TStringProperty): + propwdg = row.get_children()[1] + propwdg.get_buffer().set_text(prop.value) + elif isinstance(prop, properties.TIntProperty): + propwdg = row.get_children()[1] + propwdg.set_value(prop.value) + elif isinstance(prop, properties.TArrayProperty): + propwdg = row.get_children()[1] + for i in xrange(len(prop.value)): + entry = propwdg.get_children()[i] + entry.set_text(str(prop.value[i])) + else: + propwdg = row.get_children()[1] + propwdg.set_text(str(prop.value)) + + def set_action(self, action): + """Setter for the action property.""" + if self._action is action: + self.refresh() + return + parent = self._propbox.get_parent() + parent.remove(self._propbox) + self._propbox = gtk.VBox(spacing=10) + parent.add(self._propbox) + + self._action = action + if action is None: + return + for propname in action.get_properties(): + row = gtk.HBox() + row.pack_start(gtk.Label(T(propname)), False, False, 10) + prop = action.properties[propname] + if isinstance(prop, properties.TStringProperty): + propwdg = gtk.TextView() + propwdg.get_buffer().set_text(prop.value) + propwdg.connect_after("focus-out-event", \ + self._str_prop_changed, action, prop) + elif isinstance(prop, properties.TIntProperty): + adjustment = gtk.Adjustment(value=prop.value, + lower=prop.lower_limit.limit, + upper=prop.upper_limit.limit, + step_incr=1) + propwdg = gtk.SpinButton(adjustment=adjustment) + propwdg.connect_after("focus-out-event", \ + self._int_prop_changed, action, prop) + elif isinstance(prop, properties.TArrayProperty): + propwdg = gtk.HBox() + for i in xrange(len(prop.value)): + entry = gtk.Entry() + propwdg.pack_start(entry) + entry.connect_after("focus-out-event", \ + self._list_prop_changed, action, prop, i) + else: + propwdg = gtk.Entry() + propwdg.set_text(str(prop.value)) + row.pack_end(propwdg) + self._propbox.pack_start(row, expand=False) + self._vbox.show_all() + self.refresh() + + def get_action(self): + """Getter for the action property""" + return self._action + action = property(fset=set_action, fget=get_action, doc=\ + "Action to be edited through introspection.") + + def _list_prop_changed(self, widget, evt, action, prop, idx): + try: + prop.value[idx] = int(widget.get_text()) + except ValueError: + widget.set_text(str(prop.value[idx])) + self.__parent._creator._action_refresh_cb(None, None, action) + def _str_prop_changed(self, widget, evt, action, prop): + buf = widget.get_buffer() + prop.set(buf.get_text(buf.get_start_iter(), buf.get_end_iter())) + self.__parent._creator._action_refresh_cb(None, None, action) + def _int_prop_changed(self, widget, evt, action, prop): + prop.set(widget.get_value_as_int()) + self.__parent._creator._action_refresh_cb(None, None, action) + +class TextInputDialog(gtk.MessageDialog): + def __init__(self, text, field): + gtk.MessageDialog.__init__(self, None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, + gtk.BUTTONS_OK, + None) + self.set_markup(text) + self.entry = gtk.Entry() + self.entry.connect("activate", self._dialog_done_cb, gtk.RESPONSE_OK) + hbox = gtk.HBox() + lbl = gtk.Label(field) + hbox.pack_start(lbl, False) + hbox.pack_end(self.entry) + self.vbox.pack_end(hbox, True, True) + self.show_all() + + def pop(self): + self.run() + self.hide() + text = self.entry.get_text() + return text + + def _dialog_done_cb(self, entry, response): + self.response(response) + + + +# vim:set ts=4 sts=4 sw=4 et: diff --git a/src/sugar/tutorius/gtkutils.py b/src/sugar/tutorius/gtkutils.py index a745b9d..1a9cb0f 100644 --- a/src/sugar/tutorius/gtkutils.py +++ b/src/sugar/tutorius/gtkutils.py @@ -19,6 +19,19 @@ Utility classes and functions that are gtk related """ import gtk +def raddr_lookup(widget): + name = [] + child = widget + parent = widget.parent + while parent: + name.append(str(parent.get_children().index(child))) + child = parent + parent = child.parent + + name.append("0") # root object itself + name.reverse() + return ".".join(name) + def find_widget(base, target_fqdn): """Find a widget by digging into a parent widget's children tree diff --git a/src/sugar/tutorius/linear_creator.py b/src/sugar/tutorius/linear_creator.py index 02bb497..91b11f4 100644 --- a/src/sugar/tutorius/linear_creator.py +++ b/src/sugar/tutorius/linear_creator.py @@ -31,6 +31,7 @@ class LinearCreator(object): self.fsm = FiniteStateMachine("Sample Tutorial") self.current_actions = [] self.nb_state = 0 + self.state_name = "INIT" def set_name(self, name): """ @@ -54,17 +55,14 @@ class LinearCreator(object): be replaced to point to the next event in the line. """ if len(self.current_actions) != 0: - state_name = "" - if self.nb_state == 0: - state_name = "INIT" - else: - state_name = "State" + str(self.nb_state) # Set the next state name - there is no way the caller should have # to deal with that. - next_state_name = "State" + str(self.nb_state+1) + next_state_name = "State %d" % (self.nb_state+1) event_filter.set_next_state(next_state_name) - - state = State(state_name, action_list=self.current_actions, event_filter_list=[event_filter]) + state = State(self.state_name, action_list=self.current_actions, + event_filter_list=[event_filter]) + self.state_name = next_state_name + self.nb_state += 1 self.fsm.add_state(state) @@ -94,4 +92,4 @@ class LinearCreator(object): new_fsm.add_state(state) return new_fsm - \ No newline at end of file + diff --git a/src/sugar/tutorius/overlayer.py b/src/sugar/tutorius/overlayer.py index c08ed4c..12ea82f 100644 --- a/src/sugar/tutorius/overlayer.py +++ b/src/sugar/tutorius/overlayer.py @@ -1,6 +1,6 @@ """ -This guy manages drawing of overlayed widgets. The class responsible for drawing -management (Overlayer) and overlayable widgets are defined here. +This module manages drawing of overlayed widgets. The class responsible for +drawing management (Overlayer) and basic overlayable widgets are defined here. """ # Copyright (C) 2009, Tutorius.org # @@ -22,6 +22,20 @@ import gobject import gtk import cairo import pangocairo +from math import pi + +from sugar import profile + +# for easy profile access from cairo +color = profile.get_color().get_stroke_color() +xo_line_color = (int(color[1:3], 16)/255.0, + int(color[3:5], 16)/255.0, + int(color[5:7], 16)/255.0) +color = profile.get_color().get_fill_color() +xo_fill_color = (int(color[1:3], 16)/255.0, + int(color[3:5], 16)/255.0, + int(color[5:7], 16)/255.0) +del color # This is the CanvasDrawable protocol. Any widget wishing to be drawn on the # overlay must implement it. See TextBubble for a sample implementation. @@ -71,6 +85,9 @@ class Overlayer(gtk.Layout): child.no_expose = True gtk.Layout.put(self, child, x, y) + # be sure to redraw or the overlay may not show + self.queue_draw() + def __init_realized(self, widget, event): """ @@ -138,10 +155,10 @@ class Overlayer(gtk.Layout): class TextBubble(gtk.Widget): """ - A CanvasDrawableWidget drawing a round textbox and a tail pointing - to a specified widget. + A CanvasDrawableWidget drawing a round textbox and a tail pointing + to a specified widget. """ - def __init__(self, text, speaker=None, tailpos=None): + def __init__(self, text, speaker=None, tailpos=[0,0]): """ Creates a new cairo rendered text bubble. @@ -156,14 +173,15 @@ class TextBubble(gtk.Widget): # 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.padding = 20 - self.__exposer = self.connect("expose-event", self.__on_expose) + self._no_expose = False + self.__exposer = None def draw_with_context(self, context): """ @@ -178,58 +196,55 @@ class TextBubble(gtk.Widget): 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) + # draw bubble tail if present + if self.tailpos != [0,0]: + context.move_to(xradius-width/4, yradius) context.line_to(self.tailpos[0], self.tailpos[1]) - context.line_to(xradius+40, yradius) + context.line_to(xradius+width/4, yradius) context.set_line_width(self.line_width) - context.set_source_rgb(0.0, 0.0, 0.0) + context.set_source_rgb(*xo_line_color) 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) + # bubble border + context.move_to(width-self.padding, 0.0) + context.line_to(self.padding, 0.0) + context.arc_negative(self.padding, self.padding, self.padding, + 3*pi/2, pi) + context.line_to(0.0, height-self.padding) + context.arc_negative(self.padding, height-self.padding, self.padding, + pi, pi/2) + context.line_to(width-self.padding, height) + context.arc_negative(width-self.padding, height-self.padding, + self.padding, pi/2, 0) + context.line_to(width, self.padding) + context.arc_negative(width-self.padding, self.padding, self.padding, + 0.0, -pi/2) + context.set_line_width(self.line_width) + context.set_source_rgb(*xo_line_color) + context.stroke_preserve() + context.set_source_rgb(*xo_fill_color) context.fill() - # text - # FIXME create text layout when setting text or in realize method - context.set_source_rgb(0.0, 0.0, 0.0) + # bubble painting. Redrawing the inside after the tail will combine + if self.tailpos != [0,0]: + context.move_to(xradius-width/4, yradius) + context.line_to(self.tailpos[0], self.tailpos[1]) + context.line_to(xradius+width/4, yradius) + context.set_line_width(self.line_width) + context.set_source_rgb(*xo_fill_color) + context.fill() + + context.set_source_rgb(1.0, 1.0, 1.0) pangoctx = pangocairo.CairoContext(context) - text_layout = pangoctx.create_layout() - text_layout.set_text(self.__label) + self._text_layout.set_markup(self.__label) + text_size = self._text_layout.get_pixel_size() 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) + int((self.allocation.width-text_size[0])/2), + int((self.allocation.height-text_size[1])/2)) + pangoctx.show_layout(self._text_layout) # work done. Be kind to next cairo widgets and reset matrix. context.identity_matrix() @@ -237,33 +252,10 @@ class TextBubble(gtk.Widget): 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) + if not self._no_expose: + self.__exposer = self.connect_after("expose-event", \ + self.__on_expose) def __on_expose(self, widget, event): """Redraw event callback.""" @@ -275,26 +267,25 @@ class TextBubble(gtk.Widget): 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) + self.__label = "%s"%value + if not self.parent: + return + ctx = self.parent.window.cairo_create() 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 + self._text_layout = pangoctx.create_layout() + self._text_layout.set_markup(value) + del pangoctx, ctx#, surf def do_size_request(self, requisition): """Fill requisition with size occupied by the widget.""" - width, height = self.__text_dimentions + ctx = self.parent.window.cairo_create() + pangoctx = pangocairo.CairoContext(ctx) + self._text_layout = pangoctx.create_layout() + self._text_layout.set_markup(self.__label) - # 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) + width, height = self._text_layout.get_pixel_size() + requisition.width = int(width+2*self.padding) + requisition.height = int(height+2*self.padding) def do_size_allocate(self, allocation): """Save zone allocated to the widget.""" @@ -302,19 +293,24 @@ class TextBubble(gtk.Widget): def _get_label(self): """Getter method for the label property""" - return self.__label + return self.__label[3:-4] def _set_no_expose(self, value): """setter for no_expose property""" + self._no_expose = value + if not (self.flags() and gtk.REALIZED): + return + if self.__exposer and value: - self.disconnect(self.__exposer) + self.parent.disconnect(self.__exposer) self.__exposer = None elif (not self.__exposer) and (not value): - self.__exposer = self.connect("expose-event", self.__on_expose) + self.__exposer = self.parent.connect_after("expose-event", + self.__on_expose) def _get_no_expose(self): """getter for no_expose property""" - return not self.__exposer + return self._no_expose no_expose = property(fset=_set_no_expose, fget=_get_no_expose, doc="Whether the widget should handle exposition events or not.") @@ -324,5 +320,185 @@ class TextBubble(gtk.Widget): gobject.type_register(TextBubble) +class Rectangle(gtk.Widget): + """ + A CanvasDrawableWidget drawing a rectangle over a specified widget. + """ + def __init__(self, widget, color): + """ + Creates a new Rectangle + + @param widget the widget to cover + @param color the color of the rectangle, as a 4-tuple RGBA + """ + gtk.Widget.__init__(self) + + self.covered = widget + self.color = color + + 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. + """ + if self.covered is None: + # nothing to hide, no coordinates, no drawing + return + mask_alloc = self.covered.get_allocation() + x, y = self.covered.translate_coordinates(self.parent, 0, 0) + + context.rectangle(x, y, mask_alloc.width, mask_alloc.height) + context.set_source_rgba(*self.color) + context.fill() + + def do_realize(self): + """ Setup gdk window creation. """ + self.set_flags(gtk.REALIZED | gtk.NO_WINDOW) + + self.window = self.get_parent_window() + if not isinstance(self.parent, Overlayer): + assert False, "%s should not realize" % type(self).__name__ + print "Danger, Will Robinson! Rectangle parent is not Overlayer" + + def __on_expose(self, widget, event): + """Redraw event callback.""" + assert False, "%s wasn't meant to be exposed by gtk" % \ + type(self).__name__ + ctx = self.window.cairo_create() + + self.draw_with_context(ctx) + + return True + + def do_size_request(self, requisition): + """Fill requisition with size occupied by the masked widget.""" + # This is a bit pointless, as this will always ignore allocation and + # be rendered directly on overlay, but for sanity, let's put some values + # in there. + if not self.covered: + requisition.width = 0 + requisition.height = 0 + return + + masked_alloc = self.covered.get_allocation() + requisition.width = masked_alloc.width + requisition.height = masked_alloc.height + + def do_size_allocate(self, allocation): + """Save zone allocated to the widget.""" + self.allocation = allocation + + 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.") +gobject.type_register(Rectangle) + +class Mask(gtk.EventBox): + """ + A CanvasDrawableWidget drawing a rectangle over a specified widget. + """ + def __init__(self, catch_events=False, pass_thru=()): + """ + Creates a new Rectangle + + @param catch_events whether the Mask should catch events + @param pass_thru the widgets that "punch holes" through this Mask. + Events will pass through to those widgets. + """ + gtk.EventBox.__init__(self) + self.no_expose = True # ignored + self._catch_events = False + self.catch_events = catch_events + self.pass_thru = list(pass_thru) + + def __del__(self): + for widget in self.pass_thru: + widget.drag_unhighlight() + + def mask(self, widget): + """ + Remove the widget from the unmasked list. + @param widget a widget to remask + """ + assert widget in self.pass_thru, \ + "trying to mask already masked widget" + self.pass_thru.remove(widget) + widget.drag_unhighlight() + + def unmask(self, widget): + """ + Add to the unmasked list the widget passed. + A hole will be punched through the mask at that widget's position. + @param widget a widget to unmask + """ + if widget not in self.pass_thru: + widget.drag_highlight() + self.pass_thru.append(widget) + + + def set_catch_events(self, do_catch): + """Sets whether the mask catches events of widgets under it""" + if bool(self._catch_events) ^ bool(do_catch): + if do_catch: + self._catch_events = True + self.grab_add() + else: + self.grab_remove() + self._catch_events = False + + def get_catch_events(self): + """Gets whether the mask catches events of widgets under it""" + return bool(self._catch_handle) + + catch_events = property(fset=set_catch_events, fget=get_catch_events) + + 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. + """ + # Fill parent container + mask_alloc = self.parent.get_allocation() + oldrule = context.get_fill_rule() + context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + x, y = self.translate_coordinates(self.parent, 0, 0) + + context.rectangle(x, y, mask_alloc.width, mask_alloc.height) + for hole in self.pass_thru: + alloc = hole.get_allocation() + x, y = hole.translate_coordinates(self.parent, 0, 0) + context.rectangle(x, y, alloc.width, alloc.height) + context.set_source_rgba(0, 0, 0, 0.7) + context.fill() + context.set_fill_rule(oldrule) + + def do_size_request(self, requisition): + """Fill requisition with size occupied by the masked widget.""" + # This is required for the event box to span across all the parent. + alloc = self.parent.get_allocation() + requisition.width = alloc.width + requisition.height = alloc.height + + def do_size_allocate(self, allocation): + """Save zone allocated to the widget.""" + self.allocation = allocation + +gobject.type_register(Mask) + # vim:set ts=4 sts=4 sw=4 et: diff --git a/src/sugar/tutorius/tests/linear_creatortests.py b/src/sugar/tutorius/tests/linear_creatortests.py index f9ffbe7..dcded57 100644 --- a/src/sugar/tutorius/tests/linear_creatortests.py +++ b/src/sugar/tutorius/tests/linear_creatortests.py @@ -50,13 +50,13 @@ class CreatorTests(unittest.TestCase): assert len(init_state.get_action_list()) == 2, "Creator did not insert all the actions" - assert init_state.get_event_filter_list()[0].get_next_state() == "State1" + assert init_state.get_event_filter_list()[0].get_next_state() == "State 1" , "expected next state to be 'State 1' but got %s" % init_state.get_event_filter_list()[0].get_next_state() - state1 = fsm.get_state_by_name("State1") + state1 = fsm.get_state_by_name("State 1") assert len(state1.get_action_list()) == 1, "Creator did not insert all the actions" - assert state1.get_event_filter_list()[0].get_next_state() == "State2" + assert state1.get_event_filter_list()[0].get_next_state() == "State 2" # Make sure we have the final state and that it's empty state2 = fsm.get_state_by_name("State2") diff --git a/src/sugar/tutorius/tests/serializertests.py b/src/sugar/tutorius/tests/serializertests.py index bc29601..097e570 100644 --- a/src/sugar/tutorius/tests/serializertests.py +++ b/src/sugar/tutorius/tests/serializertests.py @@ -70,6 +70,7 @@ class XMLSerializerTest(unittest.TestCase): self.testpath = "/tmp/testdata/" os.environ["SUGAR_BUNDLE_PATH"] = self.testpath os.environ["SUGAR_PREFIX"] = self.testpath + os.environ["SUGAR_PROFILE"] = 'test' ## os.mkdir(sugar.tutorius.bundler._get_store_root()) # Create the sample FSM @@ -103,6 +104,7 @@ class XMLSerializerTest(unittest.TestCase): """ if self.remove == True: os.remove(os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid)) + "/fsm.xml") + shutil.rmtree(os.path.join(os.getenv("HOME"),".sugar",os.getenv("SUGAR_PROFILE"))) if os.path.isdir(self.testpath): shutil.rmtree(self.testpath) -- cgit v0.9.1