Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/sugar/activity/activity.py50
-rw-r--r--src/sugar/graphics/window.py1
-rw-r--r--src/sugar/tutorius/Makefile.am5
-rw-r--r--src/sugar/tutorius/actions.py151
-rw-r--r--src/sugar/tutorius/bundler.py233
-rw-r--r--src/sugar/tutorius/core.py4
-rw-r--r--src/sugar/tutorius/creator.py416
-rw-r--r--src/sugar/tutorius/gtkutils.py13
-rw-r--r--src/sugar/tutorius/linear_creator.py16
-rw-r--r--src/sugar/tutorius/overlayer.py364
-rw-r--r--src/sugar/tutorius/tests/linear_creatortests.py6
-rw-r--r--src/sugar/tutorius/tests/serializertests.py2
12 files changed, 999 insertions, 262 deletions
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 <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 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 = "<b>%s</b>"%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)