Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@member.fsf.org>2010-10-01 18:00:38 (GMT)
committer Aleksey Lim <alsroot@member.fsf.org>2010-10-01 18:00:38 (GMT)
commitdee3a3d2ba0f3495400cc5cdd608f0269d9622f1 (patch)
tree3c58b868c030bc404857030e748f0c47dcfb0bb2
parent18a0d61b47f79fde51769492b54fea42e6fe9454 (diff)
Bundle toolkit dependecy
-rw-r--r--README3
-rw-r--r--activity/activity.info15
-rw-r--r--toolkit/__init__.py16
-rw-r--r--toolkit/activity.py331
-rw-r--r--toolkit/activity_widgets.py348
-rw-r--r--toolkit/chooser.py69
-rw-r--r--toolkit/combobox.py201
-rw-r--r--toolkit/internals/__init__.py16
-rw-r--r--toolkit/internals/palettewindow.py976
-rw-r--r--toolkit/json.py35
-rw-r--r--toolkit/pixbuf.py116
-rw-r--r--toolkit/radiopalette.py109
-rw-r--r--toolkit/scrolledbox.py191
-rw-r--r--toolkit/tarball.py125
-rw-r--r--toolkit/temposlider.py211
-rw-r--r--toolkit/toolbarbox.py333
-rw-r--r--toolkit/toolitem.py79
17 files changed, 3163 insertions, 11 deletions
diff --git a/README b/README
index 956aa3d..e69de29 100644
--- a/README
+++ b/README
@@ -1,3 +0,0 @@
-Speak is in intermediate state of switching to 0sugar, "master" branch code
-should work as-is but requires toolkit dependency, grab it from "fully-bundled"
-branch.
diff --git a/activity/activity.info b/activity/activity.info
index 908a7f4..c2512c9 100644
--- a/activity/activity.info
+++ b/activity/activity.info
@@ -1,5 +1,5 @@
[Activity]
-slug = speak
+sweet = speak
name = Speak
summary = An animated face that speaks whatever you type
description = Speak is a talking face for the XO laptop. Anything you type will
@@ -8,20 +8,19 @@ description = Speak is a talking face for the XO laptop. Anything you type will
of the eyes and mouth.
This is a great way to experiment with the speech synthesizer,
learn to type or just have fun making a funny face for your XO.
-icon = activity/activity-speak.svg
homepage = http://wiki.sugarlabs.org/go/Activities/Speak
license = GPLv3+
+icon = activity-speak
+exec = sugar-activity activity.SpeakActivity
+requires = gst-plugins-espeak; pyaiml
+
version = 18
stability = testing
-requires = gst-plugins-espeak; pyaiml
-exec = sugar-activity activity.SpeakActivity
-
# deprecated
-icon = activity-speak
bundle_id = vu.lux.olpc.Speak
-activity_version = 17
+activity_version = %(version)s
[Source]
-exclude = MANIFEST; locale/**; aiml/**
+exclude = aiml/**
diff --git a/toolkit/__init__.py b/toolkit/__init__.py
new file mode 100644
index 0000000..17a92ac
--- /dev/null
+++ b/toolkit/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
diff --git a/toolkit/activity.py b/toolkit/activity.py
new file mode 100644
index 0000000..1512610
--- /dev/null
+++ b/toolkit/activity.py
@@ -0,0 +1,331 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Extend sugar-toolkit activity class"""
+
+import gtk
+import logging
+import telepathy
+import gobject
+
+from sugar.activity import activity
+from sugar.presence.sugartubeconn import SugarTubeConnection
+from sugar.graphics.alert import ConfirmationAlert, NotifyAlert
+
+
+_NEW_INSTANCE = 0
+_NEW_INSTANCE = 1
+_PRE_INSTANCE = 2
+_POST_INSTANCE = 3
+
+
+class CursorFactory:
+
+ __shared_state = {"cursors": {}}
+
+ def __init__(self):
+ self.__dict__ = self.__shared_state
+
+ def get_cursor(self, cur_type):
+ if not self.cursors.has_key(cur_type):
+ cur = gtk.gdk.Cursor(cur_type)
+ self.cursors[cur_type] = cur
+ return self.cursors[cur_type]
+
+
+class Activity(activity.Activity):
+
+ """Basic activity class"""
+
+ def new_instance(self):
+ """
+ New instance was created.
+
+ Will be invoked after __init__() instead of resume_instance().
+ Subclass should implement this method to catch creation stage.
+ """
+ pass
+
+ def resume_instance(self, filepath):
+ """
+ Instance was resumed.
+
+ Will be invoked after __init__() instead of new_instance().
+ Subclass should implement this method to catch resuming stage.
+
+ """
+ pass
+
+ def save_instance(self, filepath):
+ """
+ Save activity instance.
+
+ Subclass should implement this method to save activity data.
+ """
+ raise NotImplementedError
+
+ def on_save_instance(self, cb, *args):
+ """ Register callback which will be invoked before save_instance """
+ self.__on_save_instance.append((cb, args))
+
+ def share_instance(self, connection, is_initiator):
+ """
+ Activity was shared/joined.
+
+ connection -- SugarTubeConnection object
+ wich represents telepathy connection
+
+ is_initiator -- boolean
+ if True activity was shared and
+ (current activity is an initiator of sharing)
+ otherwise activity was joined(to existed sharing session)
+
+ Will be invoked after __init__() and {new,resume}_instance().
+ Subclass should implement this method to catch sharing stage.
+ """
+ pass
+
+ def set_toolbar_box(self, toolbox):
+ if hasattr(activity.Activity, 'set_toolbar_box'):
+ activity.Activity.set_toolbar_box(self, toolbox)
+ else:
+ self.set_toolbox(toolbox)
+
+ def get_toolbar_box(self):
+ if hasattr(activity.Activity, 'get_toolbar_box'):
+ return activity.Activity.get_toolbar_box(self)
+ else:
+ return self.get_toolbox()
+
+ toolbar_box = property(get_toolbar_box, set_toolbar_box)
+
+ def get_shared_activity(self):
+ if hasattr(activity.Activity, 'get_shared_activity'):
+ return activity.Activity.get_shared_activity(self)
+ else:
+ return self._shared_activity
+
+ def notify_alert(self, title, msg):
+ """Raise standard notify alert"""
+ alert = NotifyAlert(title=title, msg=msg)
+
+ def response(alert, response_id, self):
+ self.remove_alert(alert)
+
+ alert.connect('response', response, self)
+ alert.show_all()
+ self.add_alert(alert)
+
+ def confirmation_alert(self, title, msg, cb, *cb_args):
+ """Raise standard confirmation alert"""
+ alert = ConfirmationAlert(title=title, msg=msg)
+
+ def response(alert, response_id, self, cb, *cb_args):
+ self.remove_alert(alert)
+ if response_id is gtk.RESPONSE_OK:
+ cb(*cb_args)
+
+ alert.connect('response', response, self, cb, *cb_args)
+ alert.show_all()
+ self.add_alert(alert)
+
+ def get_cursor(self):
+ return self._cursor
+
+ def set_cursor(self, cursor):
+ if not isinstance(cursor, gtk.gdk.Cursor):
+ cursor = CursorFactory().get_cursor(cursor)
+
+ if self._cursor != cursor:
+ self._cursor = cursor
+ self.window.set_cursor(self._cursor)
+
+ def __init__(self, canvas, handle):
+ """
+ Initialise the Activity.
+
+ canvas -- gtk.Widget
+ root widget for activity content
+
+ handle -- sugar.activity.activityhandle.ActivityHandle
+ instance providing the activity id and access to the
+
+ """
+ activity.Activity.__init__(self, handle)
+
+ if handle.object_id:
+ self.__state = _NEW_INSTANCE
+ else:
+ self.__state = _NEW_INSTANCE
+
+ self.__resume_filename = None
+ self.__postponed_share = []
+ self.__on_save_instance = []
+
+ self._cursor = None
+ self.set_cursor(gtk.gdk.LEFT_PTR)
+
+ # XXX do it after(possible) read_file() invoking
+ # have to rely on calling read_file() from map_cb in sugar-toolkit
+ canvas.connect_after('map', self.__map_canvasactivity_cb)
+ self.set_canvas(canvas)
+
+ def __instance(self):
+ logging.debug('Activity.__instance')
+
+ if self.__resume_filename:
+ self.resume_instance(self.__resume_filename)
+ else:
+ self.new_instance()
+
+ for i in self.__postponed_share:
+ self.share_instance(*i)
+ self.__postponed_share = []
+
+ self.__state = _POST_INSTANCE
+
+ def read_file(self, filepath):
+ """Subclass should not override this method"""
+ logging.debug('Activity.read_file state=%s' % self.__state)
+
+ self.__resume_filename = filepath
+
+ if self.__state == _NEW_INSTANCE:
+ self.__state = _PRE_INSTANCE
+ elif self.__state == _PRE_INSTANCE:
+ self.__instance();
+
+ def write_file(self, filepath):
+ """Subclass should not override this method"""
+ for cb, args in self.__on_save_instance:
+ cb(*args)
+ self.save_instance(filepath)
+
+ def __map_canvasactivity_cb(self, widget):
+ logging.debug('Activity.__map_canvasactivity_cb state=%s' % \
+ self.__state)
+
+ if self.__state == _NEW_INSTANCE:
+ self.__instance()
+ elif self.__state == _NEW_INSTANCE:
+ self.__state = _PRE_INSTANCE
+ elif self.__state == _PRE_INSTANCE:
+ self.__instance();
+
+ return False
+
+ def _share(self, tube_conn, initiator):
+ logging.debug('Activity._share state=%s' % self.__state)
+
+ if self.__state == _NEW_INSTANCE:
+ self.__postponed_share.append((tube_conn, initiator))
+ self.__state = _PRE_INSTANCE
+ elif self.__state == _PRE_INSTANCE:
+ self.__postponed_share.append((tube_conn, initiator))
+ self.__instance();
+ elif self.__state == _POST_INSTANCE:
+ self.share_instance(tube_conn, initiator)
+
+
+class SharedActivity(Activity):
+ """Basic activity class with sharing features"""
+
+ def __init__(self, canvas, service, handle):
+ """
+ Initialise the Activity.
+
+ canvas -- gtk.Widget
+ root widget for activity content
+
+ service -- string
+ dbus service for activity
+
+ handle -- sugar.activity.activityhandle.ActivityHandle
+ instance providing the activity id and access to the
+ presence service which *may* provide sharing for this
+ application
+
+ """
+ Activity.__init__(self, canvas, handle)
+ self.service = service
+
+ self.connect('shared', self._shared_cb)
+
+ # Owner.props.key
+ if self._shared_activity:
+ # We are joining the activity
+ self.connect('joined', self._joined_cb)
+ if self.get_shared():
+ # We've already joined
+ self._joined_cb()
+
+ def _shared_cb(self, activity):
+ logging.debug('My activity was shared')
+ self.__initiator = True
+ self._sharing_setup()
+
+ logging.debug('This is my activity: making a tube...')
+ id = self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(
+ self.service, {})
+
+ def _joined_cb(self, activity):
+ if not self._shared_activity:
+ return
+
+ logging.debug('Joined an existing shared activity')
+
+ self.__initiator = False
+ self._sharing_setup()
+
+ logging.debug('This is not my activity: waiting for a tube...')
+ self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=self._list_tubes_reply_cb,
+ error_handler=self._list_tubes_error_cb)
+
+ def _sharing_setup(self):
+ if self._shared_activity is None:
+ logging.error('Failed to share or join activity')
+ return
+ self._conn = self._shared_activity.telepathy_conn
+ self._tubes_chan = self._shared_activity.telepathy_tubes_chan
+ self._text_chan = self._shared_activity.telepathy_text_chan
+
+ self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal(
+ 'NewTube', self._new_tube_cb)
+
+ def _list_tubes_reply_cb(self, tubes):
+ for tube_info in tubes:
+ self._new_tube_cb(*tube_info)
+
+ def _list_tubes_error_cb(self, e):
+ logging.error('ListTubes() failed: %s', e)
+
+ def _new_tube_cb(self, id, initiator, type, service, params, state):
+ logging.debug('New tube: ID=%d initator=%d type=%d service=%s '
+ 'params=%r state=%d', id, initiator, type, service,
+ params, state)
+
+ if (type == telepathy.TUBE_TYPE_DBUS and
+ service == self.service):
+ if state == telepathy.TUBE_STATE_LOCAL_PENDING:
+ self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES] \
+ .AcceptDBusTube(id)
+
+ tube_conn = SugarTubeConnection(self._conn,
+ self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES], id,
+ group_iface=self._text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
+
+ self._share(tube_conn, self.__initiator)
diff --git a/toolkit/activity_widgets.py b/toolkit/activity_widgets.py
new file mode 100644
index 0000000..50e2c6c
--- /dev/null
+++ b/toolkit/activity_widgets.py
@@ -0,0 +1,348 @@
+# Copyright (C) 2009, Aleksey Lim, Simon Schampijer
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+import gtk
+import gobject
+import gettext
+
+from sugar import profile
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.toolbox import Toolbox
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.icon import Icon
+from sugar.bundle.activitybundle import ActivityBundle
+
+from toolkit.toolbarbox import ToolbarButton
+from toolkit.radiopalette import RadioPalette
+from toolkit.radiopalette import RadioMenuButton
+
+_ = lambda msg: gettext.dgettext('sugar-toolkit', msg)
+
+
+def _create_activity_icon(metadata):
+ if metadata.get('icon-color', ''):
+ color = XoColor(metadata['icon-color'])
+ else:
+ color = profile.get_color()
+
+ from sugar.activity.activity import get_bundle_path
+ bundle = ActivityBundle(get_bundle_path())
+ icon = Icon(file=bundle.get_icon(), xo_color=color)
+
+ return icon
+
+
+class ActivityButton(ToolButton):
+
+ def __init__(self, activity, **kwargs):
+ ToolButton.__init__(self, **kwargs)
+
+ icon = _create_activity_icon(activity.metadata)
+ self.set_icon_widget(icon)
+ icon.show()
+
+ self.props.tooltip = activity.metadata['title']
+ activity.metadata.connect('updated', self.__jobject_updated_cb)
+
+ def __jobject_updated_cb(self, jobject):
+ self.props.tooltip = jobject['title']
+
+
+class ActivityToolbarButton(ToolbarButton):
+
+ def __init__(self, activity, **kwargs):
+ toolbar = ActivityToolbar(activity, orientation_left=True)
+ toolbar.stop.hide()
+
+ ToolbarButton.__init__(self, page=toolbar, **kwargs)
+
+ icon = _create_activity_icon(activity.metadata)
+ self.set_icon_widget(icon)
+ icon.show()
+
+
+class StopButton(ToolButton):
+
+ def __init__(self, activity, **kwargs):
+ ToolButton.__init__(self, 'activity-stop', **kwargs)
+ self.props.tooltip = _('Stop')
+ self.props.accelerator = '<Ctrl>Q'
+ self.connect('clicked', self.__stop_button_clicked_cb, activity)
+
+ def __stop_button_clicked_cb(self, button, activity):
+ activity.close()
+
+
+class UndoButton(ToolButton):
+
+ def __init__(self, **kwargs):
+ ToolButton.__init__(self, 'edit-undo', **kwargs)
+ self.props.tooltip = _('Undo')
+ self.props.accelerator = '<Ctrl>Z'
+
+
+class RedoButton(ToolButton):
+
+ def __init__(self, **kwargs):
+ ToolButton.__init__(self, 'edit-redo', **kwargs)
+ self.props.tooltip = _('Redo')
+
+
+class CopyButton(ToolButton):
+
+ def __init__(self, **kwargs):
+ ToolButton.__init__(self, 'edit-copy', **kwargs)
+ self.props.tooltip = _('Copy')
+
+
+class PasteButton(ToolButton):
+
+ def __init__(self, **kwargs):
+ ToolButton.__init__(self, 'edit-paste', **kwargs)
+ self.props.tooltip = _('Paste')
+
+
+class ShareButton(RadioMenuButton):
+
+ def __init__(self, activity, **kwargs):
+ palette = RadioPalette()
+
+ self.private = RadioToolButton(
+ icon_name='zoom-home')
+ palette.append(self.private, _('Private'))
+
+ self.neighborhood = RadioToolButton(
+ icon_name='zoom-neighborhood',
+ group=self.private)
+ self._neighborhood_handle = self.neighborhood.connect(
+ 'clicked', self.__neighborhood_clicked_cb, activity)
+ palette.append(self.neighborhood, _('My Neighborhood'))
+
+ activity.connect('shared', self.__update_share_cb)
+ activity.connect('joined', self.__update_share_cb)
+
+ RadioMenuButton.__init__(self, **kwargs)
+ self.props.palette = palette
+ if activity.props.max_participants == 1:
+ self.props.sensitive = False
+
+ def __neighborhood_clicked_cb(self, button, activity):
+ activity.share()
+
+ def __update_share_cb(self, activity):
+ self.neighborhood.handler_block(self._neighborhood_handle)
+ try:
+ if activity.get_shared():
+ self.private.props.sensitive = False
+ self.neighborhood.props.sensitive = False
+ self.neighborhood.props.active = True
+ else:
+ self.private.props.sensitive = True
+ self.neighborhood.props.sensitive = True
+ self.private.props.active = True
+ finally:
+ self.neighborhood.handler_unblock(self._neighborhood_handle)
+
+
+class KeepButton(ToolButton):
+
+ def __init__(self, activity, **kwargs):
+ ToolButton.__init__(self, **kwargs)
+ self.props.tooltip = _('Keep')
+ self.props.accelerator = '<Ctrl>S'
+
+ color = profile.get_color()
+ keep_icon = Icon(icon_name='document-save', xo_color=color)
+ keep_icon.show()
+
+ self.set_icon_widget(keep_icon)
+ self.connect('clicked', self.__keep_button_clicked_cb, activity)
+
+ def __keep_button_clicked_cb(self, button, activity):
+ activity.copy()
+
+
+class TitleEntry(gtk.ToolItem):
+
+ def __init__(self, activity, **kwargs):
+ gtk.ToolItem.__init__(self)
+ self.set_expand(False)
+ self._update_title_sid = None
+
+ self.entry = gtk.Entry(**kwargs)
+ self.entry.set_size_request(int(gtk.gdk.screen_width() / 3), -1)
+ self.entry.set_text(activity.metadata['title'])
+ self.entry.connect('changed', self.__title_changed_cb, activity)
+ self.entry.show()
+ self.add(self.entry)
+
+ activity.metadata.connect('updated', self.__jobject_updated_cb)
+
+ def modify_bg(self, state, color):
+ gtk.ToolItem.modify_bg(self, state, color)
+ self.entry.modify_bg(state, color)
+
+ def __jobject_updated_cb(self, jobject):
+ self.entry.set_text(jobject['title'])
+
+ def __title_changed_cb(self, entry, activity):
+ if not self._update_title_sid:
+ self._update_title_sid = gobject.timeout_add_seconds(
+ 1, self.__update_title_cb, activity)
+
+ def __update_title_cb(self, activity):
+ title = self.entry.get_text()
+
+ activity.metadata['title'] = title
+ activity.metadata['title_set_by_user'] = '1'
+ activity.save()
+
+ shared_activity = activity.get_shared_activity()
+ if shared_activity is not None:
+ shared_activity.props.name = title
+
+ self._update_title_sid = None
+ return False
+
+
+class ActivityToolbar(gtk.Toolbar):
+ """The Activity toolbar with the Journal entry title, sharing,
+ Keep and Stop buttons
+
+ All activities should have this toolbar. It is easiest to add it to your
+ Activity by using the ActivityToolbox.
+ """
+
+ def __init__(self, activity, orientation_left=False):
+ gtk.Toolbar.__init__(self)
+
+ self._activity = activity
+
+ if activity.metadata:
+ title_button = TitleEntry(activity)
+ title_button.show()
+ self.insert(title_button, -1)
+ self.title = title_button.entry
+
+ if orientation_left == False:
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ separator.set_expand(True)
+ self.insert(separator, -1)
+ separator.show()
+
+ self.share = ShareButton(activity)
+ self.share.show()
+ self.insert(self.share, -1)
+
+ self.keep = KeepButton(activity)
+ self.insert(self.keep, -1)
+ self.keep.show()
+
+ self.stop = StopButton(activity)
+ self.insert(self.stop, -1)
+ self.stop.show()
+
+
+class EditToolbar(gtk.Toolbar):
+ """Provides the standard edit toolbar for Activities.
+
+ Members:
+ undo -- the undo button
+ redo -- the redo button
+ copy -- the copy button
+ paste -- the paste button
+ separator -- A separator between undo/redo and copy/paste
+
+ This class only provides the 'edit' buttons in a standard layout,
+ your activity will need to either hide buttons which make no sense for your
+ Activity, or you need to connect the button events to your own callbacks:
+
+ ## Example from Read.activity:
+ # Create the edit toolbar:
+ self._edit_toolbar = EditToolbar(self._view)
+ # Hide undo and redo, they're not needed
+ self._edit_toolbar.undo.props.visible = False
+ self._edit_toolbar.redo.props.visible = False
+ # Hide the separator too:
+ self._edit_toolbar.separator.props.visible = False
+
+ # As long as nothing is selected, copy needs to be insensitive:
+ self._edit_toolbar.copy.set_sensitive(False)
+ # When the user clicks the button, call _edit_toolbar_copy_cb()
+ self._edit_toolbar.copy.connect('clicked', self._edit_toolbar_copy_cb)
+
+ # Add the edit toolbar:
+ toolbox.add_toolbar(_('Edit'), self._edit_toolbar)
+ # And make it visible:
+ self._edit_toolbar.show()
+ """
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self.undo = UndoButton()
+ self.insert(self.undo, -1)
+ self.undo.show()
+
+ self.redo = RedoButton()
+ self.insert(self.redo, -1)
+ self.redo.show()
+
+ self.separator = gtk.SeparatorToolItem()
+ self.separator.set_draw(True)
+ self.insert(self.separator, -1)
+ self.separator.show()
+
+ self.copy = CopyButton()
+ self.insert(self.copy, -1)
+ self.copy.show()
+
+ self.paste = PasteButton()
+ self.insert(self.paste, -1)
+ self.paste.show()
+
+
+class ActivityToolbox(Toolbox):
+ """Creates the Toolbox for the Activity
+
+ By default, the toolbox contains only the ActivityToolbar. After creating
+ the toolbox, you can add your activity specific toolbars, for example the
+ EditToolbar.
+
+ To add the ActivityToolbox to your Activity in MyActivity.__init__() do:
+
+ # Create the Toolbar with the ActivityToolbar:
+ toolbox = activity.ActivityToolbox(self)
+ ... your code, inserting all other toolbars you need, like EditToolbar
+
+ # Add the toolbox to the activity frame:
+ self.set_toolbox(toolbox)
+ # And make it visible:
+ toolbox.show()
+ """
+
+ def __init__(self, activity):
+ Toolbox.__init__(self)
+
+ self._activity_toolbar = ActivityToolbar(activity)
+ self.add_toolbar(_('Activity'), self._activity_toolbar)
+ self._activity_toolbar.show()
+
+ def get_activity_toolbar(self):
+ return self._activity_toolbar
diff --git a/toolkit/chooser.py b/toolkit/chooser.py
new file mode 100644
index 0000000..e957fd7
--- /dev/null
+++ b/toolkit/chooser.py
@@ -0,0 +1,69 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Object chooser method"""
+
+import gtk
+import logging
+
+from sugar import mime
+from sugar.graphics.objectchooser import ObjectChooser
+
+TEXT = hasattr(mime, 'GENERIC_TYPE_TEXT') and mime.GENERIC_TYPE_TEXT or None
+IMAGE = hasattr(mime, 'GENERIC_TYPE_IMAGE') and mime.GENERIC_TYPE_IMAGE or None
+AUDIO = hasattr(mime, 'GENERIC_TYPE_AUDIO') and mime.GENERIC_TYPE_AUDIO or None
+VIDEO = hasattr(mime, 'GENERIC_TYPE_VIDEO') and mime.GENERIC_TYPE_VIDEO or None
+LINK = hasattr(mime, 'GENERIC_TYPE_LINK') and mime.GENERIC_TYPE_LINK or None
+
+
+def pick(cb=None, default=None, parent=None, what=None):
+ """
+ Opens object chooser.
+
+ Method returns:
+
+ * cb(jobject), if object was choosen and cb is not None
+ * jobject, if object was choosen and cb is None
+ * default, otherwise
+
+ NOTE: 'what' makes sense only for sugar >= 0.84
+ """
+ what = what and {'what_filter': what} or {}
+ chooser = ObjectChooser(parent=parent, **what)
+
+ jobject = None
+ out = None
+
+ try:
+ if chooser.run() == gtk.RESPONSE_ACCEPT:
+ jobject = chooser.get_selected_object()
+ logging.debug('ObjectChooser: %r' % jobject)
+
+ if jobject and jobject.file_path:
+ if cb:
+ out = cb(jobject)
+ else:
+ out = jobject
+ finally:
+ if jobject and id(jobject) != id(out):
+ jobject.destroy()
+ chooser.destroy()
+ del chooser
+
+ if out:
+ return out
+ else:
+ return default
diff --git a/toolkit/combobox.py b/toolkit/combobox.py
new file mode 100644
index 0000000..d021106
--- /dev/null
+++ b/toolkit/combobox.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2007, One Laptop Per Child
+# Copyright (C) 2009, Aleksey Lim
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+STABLE.
+"""
+
+import gobject
+import gtk
+
+
+class ComboBox(gtk.ComboBox):
+
+ def __init__(self):
+ gtk.ComboBox.__init__(self)
+
+ self._text_renderer = None
+ self._icon_renderer = None
+
+ model = gtk.ListStore(gobject.TYPE_PYOBJECT,
+ gobject.TYPE_STRING,
+ gtk.gdk.Pixbuf,
+ gobject.TYPE_BOOLEAN)
+ self.set_model(model)
+
+ self.set_row_separator_func(self._is_separator)
+
+ def get_value(self):
+ """
+ Parameters
+ ----------
+ None :
+
+ Returns:
+ --------
+ value :
+
+ """
+ row = self.get_active_item()
+ if not row:
+ return None
+ return row[0]
+
+ value = gobject.property(
+ type=object, getter=get_value, setter=None)
+
+ def _get_real_name_from_theme(self, name, size):
+ icon_theme = gtk.icon_theme_get_default()
+ width, height = gtk.icon_size_lookup(size)
+ info = icon_theme.lookup_icon(name, max(width, height), 0)
+ if not info:
+ raise ValueError("Icon '" + name + "' not found.")
+ fname = info.get_filename()
+ del info
+ return fname
+
+ def append_item(self, action_id, text, icon_name=None, file_name=None):
+ """
+ Parameters
+ ----------
+ action_id :
+
+ text :
+
+ icon_name=None :
+
+ file_name=None :
+
+ Returns
+ -------
+ None
+
+ """
+ item = self._item_new(action_id, text, icon_name, file_name)
+ self.get_model().append(item)
+
+ def set_item(self, action_id, text=None, icon_name=None, file_name=None):
+ for i, value in enumerate(self.get_model()):
+ if value[0] == action_id:
+ item = self._item_new(action_id, text, icon_name, file_name)
+ iter = self.get_model().iter_nth_child(None, i)
+ if text is not None:
+ self.get_model().set(iter, 1, item[1])
+ if icon_name is not None or file_name is not None:
+ self.get_model().set(iter, 2, item[2])
+ return True
+ return False
+
+ def select(self, action_id=None, text=None):
+ if action_id is not None:
+ column = 0
+ value = action_id
+ elif text is not None:
+ column = 1
+ value = text
+ else:
+ return
+
+ for i, item in enumerate(self.get_model()):
+ if item[column] != value:
+ continue
+ self.set_active(i)
+ break
+
+ def _item_new(self, action_id, text, icon_name, file_name):
+ if not self._icon_renderer and (icon_name or file_name):
+ self._icon_renderer = gtk.CellRendererPixbuf()
+
+ settings = self.get_settings()
+ w, h = gtk.icon_size_lookup_for_settings(
+ settings, gtk.ICON_SIZE_MENU)
+ self._icon_renderer.props.stock_size = max(w, h)
+
+ self.pack_start(self._icon_renderer, False)
+ self.add_attribute(self._icon_renderer, 'pixbuf', 2)
+
+ if not self._text_renderer and text:
+ self._text_renderer = gtk.CellRendererText()
+ self.pack_end(self._text_renderer, True)
+ self.add_attribute(self._text_renderer, 'text', 1)
+
+ if icon_name or file_name:
+ if text:
+ size = gtk.ICON_SIZE_MENU
+ else:
+ size = gtk.ICON_SIZE_LARGE_TOOLBAR
+ width, height = gtk.icon_size_lookup(size)
+
+ if icon_name:
+ file_name = self._get_real_name_from_theme(icon_name, size)
+
+ pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(
+ file_name, width, height)
+ else:
+ pixbuf = None
+
+ return (action_id, text, pixbuf, False)
+
+ def append_separator(self):
+ """
+ Parameters
+ ----------
+ None
+
+ Returns
+ -------
+ None
+
+ """
+ self.get_model().append([0, None, None, True])
+
+ def get_active_item(self):
+ """
+ Parameters
+ ----------
+ None
+
+ Returns
+ -------
+ Active_item :
+
+ """
+ index = self.get_active()
+ if index == -1:
+ index = 0
+
+ row = self.get_model().iter_nth_child(None, index)
+ if not row:
+ return None
+ return self.get_model()[row]
+
+ def remove_all(self):
+ """
+ Parameters
+ ----------
+ None
+
+ Returns
+ -------
+ None
+
+ """
+ self.get_model().clear()
+
+ def _is_separator(self, model, row):
+ return model[row][3]
diff --git a/toolkit/internals/__init__.py b/toolkit/internals/__init__.py
new file mode 100644
index 0000000..17a92ac
--- /dev/null
+++ b/toolkit/internals/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
diff --git a/toolkit/internals/palettewindow.py b/toolkit/internals/palettewindow.py
new file mode 100644
index 0000000..d8c4326
--- /dev/null
+++ b/toolkit/internals/palettewindow.py
@@ -0,0 +1,976 @@
+# Copyright (C) 2007, Eduardo Silva <edsiper@gmail.com>
+# Copyright (C) 2008, One Laptop Per Child
+# Copyright (C) 2009, Tomeu Vizoso
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+STABLE.
+"""
+
+import logging
+
+import gtk
+import gobject
+import hippo
+
+from sugar.graphics import palettegroup
+from sugar.graphics import animator
+from sugar.graphics import style
+
+
+def _calculate_gap(a, b):
+ """Helper function to find the gap position and size of widget a"""
+ # Test for each side if the palette and invoker are
+ # adjacent to each other.
+ gap = True
+
+ if a.y + a.height == b.y:
+ gap_side = gtk.POS_BOTTOM
+ elif a.x + a.width == b.x:
+ gap_side = gtk.POS_RIGHT
+ elif a.x == b.x + b.width:
+ gap_side = gtk.POS_LEFT
+ elif a.y == b.y + b.height:
+ gap_side = gtk.POS_TOP
+ else:
+ gap = False
+
+ if gap:
+ if gap_side == gtk.POS_BOTTOM or gap_side == gtk.POS_TOP:
+ gap_start = min(a.width, max(0, b.x - a.x))
+ gap_size = max(0, min(a.width,
+ (b.x + b.width) - a.x) - gap_start)
+ elif gap_side == gtk.POS_RIGHT or gap_side == gtk.POS_LEFT:
+ gap_start = min(a.height, max(0, b.y - a.y))
+ gap_size = max(0, min(a.height,
+ (b.y + b.height) - a.y) - gap_start)
+
+ if gap and gap_size > 0:
+ return (gap_side, gap_start, gap_size)
+ else:
+ return False
+
+
+class MouseSpeedDetector(gobject.GObject):
+
+ __gsignals__ = {
+ 'motion-slow': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'motion-fast': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ }
+
+ _MOTION_SLOW = 1
+ _MOTION_FAST = 2
+
+ def __init__(self, parent, delay, thresh):
+ """Create MouseSpeedDetector object,
+ delay in msec
+ threshold in pixels (per tick of 'delay' msec)"""
+
+ gobject.GObject.__init__(self)
+
+ self._threshold = thresh
+ self._parent = parent
+ self._delay = delay
+ self._state = None
+ self._timeout_hid = None
+ self._mouse_pos = None
+
+ def start(self):
+ self.stop()
+
+ self._mouse_pos = self._get_mouse_position()
+ self._timeout_hid = gobject.timeout_add(self._delay, self._timer_cb)
+
+ def stop(self):
+ if self._timeout_hid is not None:
+ gobject.source_remove(self._timeout_hid)
+ self._state = None
+
+ def _get_mouse_position(self):
+ display = gtk.gdk.display_get_default()
+ screen_, x, y, mask_ = display.get_pointer()
+ return (x, y)
+
+ def _detect_motion(self):
+ oldx, oldy = self._mouse_pos
+ (x, y) = self._get_mouse_position()
+ self._mouse_pos = (x, y)
+
+ dist2 = (oldx - x)**2 + (oldy - y)**2
+ if dist2 > self._threshold**2:
+ return True
+ else:
+ return False
+
+ def _timer_cb(self):
+ motion = self._detect_motion()
+ if motion and self._state != self._MOTION_FAST:
+ self.emit('motion-fast')
+ self._state = self._MOTION_FAST
+ elif not motion and self._state != self._MOTION_SLOW:
+ self.emit('motion-slow')
+ self._state = self._MOTION_SLOW
+
+ return True
+
+
+class PaletteWindow(gtk.Window):
+
+ __gsignals__ = {
+ 'popup': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'popdown': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'activate': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ }
+
+ def __init__(self, **kwargs):
+ self._group_id = None
+ self._invoker = None
+ self._invoker_hids = []
+ self._cursor_x = 0
+ self._cursor_y = 0
+ self._alignment = None
+ self._up = False
+ self._old_alloc = None
+
+ self._popup_anim = animator.Animator(.5, 10)
+ self._popup_anim.add(_PopupAnimation(self))
+
+ self._popdown_anim = animator.Animator(0.6, 10)
+ self._popdown_anim.add(_PopdownAnimation(self))
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ self.set_decorated(False)
+ self.set_resizable(False)
+ # Just assume xthickness and ythickness are the same
+ self.set_border_width(self.get_style().xthickness)
+
+ accel_group = gtk.AccelGroup()
+ self.set_data('sugar-accel-group', accel_group)
+ self.add_accel_group(accel_group)
+
+ self.set_group_id("default")
+
+ self.connect('show', self.__show_cb)
+ self.connect('hide', self.__hide_cb)
+ self.connect('realize', self.__realize_cb)
+ self.connect('destroy', self.__destroy_cb)
+ self.connect('enter-notify-event', self.__enter_notify_event_cb)
+ self.connect('leave-notify-event', self.__leave_notify_event_cb)
+
+ self._mouse_detector = MouseSpeedDetector(self, 200, 5)
+ self._mouse_detector.connect('motion-slow', self._mouse_slow_cb)
+
+ def __destroy_cb(self, palette):
+ self.set_group_id(None)
+
+ def set_invoker(self, invoker):
+ for hid in self._invoker_hids[:]:
+ self._invoker.disconnect(hid)
+ self._invoker_hids.remove(hid)
+
+ self._invoker = invoker
+ if invoker is not None:
+ self._invoker_hids.append(self._invoker.connect(
+ 'mouse-enter', self._invoker_mouse_enter_cb))
+ self._invoker_hids.append(self._invoker.connect(
+ 'mouse-leave', self._invoker_mouse_leave_cb))
+ self._invoker_hids.append(self._invoker.connect(
+ 'right-click', self._invoker_right_click_cb))
+
+ def get_invoker(self):
+ return self._invoker
+
+ invoker = gobject.property(type=object,
+ getter=get_invoker,
+ setter=set_invoker)
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+
+ def _mouse_slow_cb(self, widget):
+ self._mouse_detector.stop()
+ self._palette_do_popup()
+
+ def _palette_do_popup(self):
+ immediate = False
+
+ if self.is_up():
+ self._popdown_anim.stop()
+ return
+
+ if self._group_id:
+ group = palettegroup.get_group(self._group_id)
+ if group and group.is_up():
+ immediate = True
+ group.popdown()
+
+ self.popup(immediate=immediate)
+
+ def is_up(self):
+ return self._up
+
+ def set_group_id(self, group_id):
+ if self._group_id:
+ group = palettegroup.get_group(self._group_id)
+ group.remove(self)
+ if group_id:
+ self._group_id = group_id
+ group = palettegroup.get_group(group_id)
+ group.add(self)
+
+ def get_group_id(self):
+ return self._group_id
+
+ group_id = gobject.property(type=str,
+ getter=get_group_id,
+ setter=set_group_id)
+
+ def do_size_request(self, requisition):
+ gtk.Window.do_size_request(self, requisition)
+ requisition.width = max(requisition.width, style.GRID_CELL_SIZE * 2)
+
+ def do_size_allocate(self, allocation):
+ gtk.Window.do_size_allocate(self, allocation)
+
+ if self._old_alloc is None or \
+ self._old_alloc.x != allocation.x or \
+ self._old_alloc.y != allocation.y or \
+ self._old_alloc.width != allocation.width or \
+ self._old_alloc.height != allocation.height:
+ self.queue_draw()
+
+ # We need to store old allocation because when size_allocate
+ # is called widget.allocation is already updated.
+ # gtk.Window resizing is different from normal containers:
+ # the X window is resized, widget.allocation is updated from
+ # the configure request handler and finally size_allocate is called.
+ self._old_alloc = allocation
+
+ def do_expose_event(self, event):
+ # We want to draw a border with a beautiful gap
+ if self._invoker is not None and self._invoker.has_rectangle_gap():
+ invoker = self._invoker.get_rect()
+ palette = self.get_rect()
+
+ gap = _calculate_gap(palette, invoker)
+ else:
+ gap = False
+
+ allocation = self.get_allocation()
+ wstyle = self.get_style()
+
+ if gap:
+ wstyle.paint_box_gap(event.window, gtk.STATE_PRELIGHT,
+ gtk.SHADOW_IN, event.area, self, "palette",
+ 0, 0, allocation.width, allocation.height,
+ gap[0], gap[1], gap[2])
+ else:
+ wstyle.paint_box(event.window, gtk.STATE_PRELIGHT,
+ gtk.SHADOW_IN, event.area, self, "palette",
+ 0, 0, allocation.width, allocation.height)
+
+ # Fall trough to the container expose handler.
+ # (Leaving out the window expose handler which redraws everything)
+ gtk.Bin.do_expose_event(self, event)
+
+ def update_position(self):
+ invoker = self._invoker
+ if invoker is None or self._alignment is None:
+ logging.error('Cannot update the palette position.')
+ return
+
+ rect = self.size_request()
+ position = invoker.get_position_for_alignment(self._alignment, rect)
+ if position is None:
+ position = invoker.get_position(rect)
+
+ self.move(position.x, position.y)
+
+ def get_full_size_request(self):
+ return self.size_request()
+
+ def popup(self, immediate=False):
+ if self._invoker is not None:
+ full_size_request = self.get_full_size_request()
+ self._alignment = self._invoker.get_alignment(full_size_request)
+
+ self.update_position()
+ self.set_transient_for(self._invoker.get_toplevel())
+
+ self._popdown_anim.stop()
+
+ if not immediate:
+ self._popup_anim.start()
+ else:
+ self._popup_anim.stop()
+ self.show()
+ # we have to invoke update_position() twice
+ # since WM could ignore first move() request
+ self.update_position()
+
+ def popdown(self, immediate=False):
+ logging.debug('PaletteWindow.popdown immediate %r', immediate)
+
+ self._popup_anim.stop()
+ self._mouse_detector.stop()
+
+ if not immediate:
+ self._popdown_anim.start()
+ else:
+ self._popdown_anim.stop()
+ self.size_request()
+ self.hide()
+
+ def on_invoker_enter(self):
+ self._popdown_anim.stop()
+ self._mouse_detector.start()
+
+ def on_invoker_leave(self):
+ self._mouse_detector.stop()
+ self.popdown()
+
+ def on_enter(self, event):
+ self._popdown_anim.stop()
+
+ def on_leave(self, event):
+ self.popdown()
+
+ def _invoker_mouse_enter_cb(self, invoker):
+ self.on_invoker_enter()
+
+ def _invoker_mouse_leave_cb(self, invoker):
+ self.on_invoker_leave()
+
+ def _invoker_right_click_cb(self, invoker):
+ self.popup(immediate=True)
+
+ def __enter_notify_event_cb(self, widget, event):
+ if event.detail != gtk.gdk.NOTIFY_INFERIOR and \
+ event.mode == gtk.gdk.CROSSING_NORMAL:
+ self.on_enter(event)
+
+ def __leave_notify_event_cb(self, widget, event):
+ if event.detail != gtk.gdk.NOTIFY_INFERIOR and \
+ event.mode == gtk.gdk.CROSSING_NORMAL:
+ self.on_leave(event)
+
+ def __show_cb(self, widget):
+ if self._invoker is not None:
+ self._invoker.notify_popup()
+
+ self._up = True
+ self.emit('popup')
+
+ def __hide_cb(self, widget):
+ logging.debug('__hide_cb')
+
+ if self._invoker:
+ self._invoker.notify_popdown()
+
+ self._up = False
+ self.emit('popdown')
+
+ def get_rect(self):
+ win_x, win_y = self.window.get_origin()
+ rectangle = self.get_allocation()
+
+ x = win_x + rectangle.x
+ y = win_y + rectangle.y
+ width, height = self.size_request()
+
+ return gtk.gdk.Rectangle(x, y, width, height)
+
+ def get_palette_state(self):
+ return self._palette_state
+
+ def _set_palette_state(self, state):
+ self._palette_state = state
+
+ def set_palette_state(self, state):
+ self._set_palette_state(state)
+
+ palette_state = property(get_palette_state)
+
+
+class _PopupAnimation(animator.Animation):
+
+ def __init__(self, palette):
+ animator.Animation.__init__(self, 0.0, 1.0)
+ self._palette = palette
+
+ def next_frame(self, current):
+ if current == 1.0:
+ self._palette.popup(immediate=True)
+
+
+class _PopdownAnimation(animator.Animation):
+
+ def __init__(self, palette):
+ animator.Animation.__init__(self, 0.0, 1.0)
+ self._palette = palette
+
+ def next_frame(self, current):
+ if current == 1.0:
+ self._palette.popdown(immediate=True)
+
+
+class Invoker(gobject.GObject):
+
+ __gsignals__ = {
+ 'mouse-enter': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'mouse-leave': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'right-click': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'focus-out': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ }
+
+ ANCHORED = 0
+ AT_CURSOR = 1
+
+ BOTTOM = [(0.0, 0.0, 0.0, 1.0), (-1.0, 0.0, 1.0, 1.0)]
+ RIGHT = [(0.0, 0.0, 1.0, 0.0), (0.0, -1.0, 1.0, 1.0)]
+ TOP = [(0.0, -1.0, 0.0, 0.0), (-1.0, -1.0, 1.0, 0.0)]
+ LEFT = [(-1.0, 0.0, 0.0, 0.0), (-1.0, -1.0, 0.0, 1.0)]
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self.parent = None
+
+ self._screen_area = gtk.gdk.Rectangle(0, 0, gtk.gdk.screen_width(),
+ gtk.gdk.screen_height())
+ self._position_hint = self.ANCHORED
+ self._cursor_x = -1
+ self._cursor_y = -1
+ self._palette = None
+
+ def attach(self, parent):
+ self.parent = parent
+
+ def detach(self):
+ self.parent = None
+ if self._palette is not None:
+ self._palette.destroy()
+ self._palette = None
+
+ def _get_position_for_alignment(self, alignment, palette_dim):
+ palette_halign = alignment[0]
+ palette_valign = alignment[1]
+ invoker_halign = alignment[2]
+ invoker_valign = alignment[3]
+
+ if self._cursor_x == -1 or self._cursor_y == -1:
+ display = gtk.gdk.display_get_default()
+ screen_, x, y, mask_ = display.get_pointer()
+ self._cursor_x = x
+ self._cursor_y = y
+
+ if self._position_hint is self.ANCHORED:
+ rect = self.get_rect()
+ else:
+ dist = style.PALETTE_CURSOR_DISTANCE
+ rect = gtk.gdk.Rectangle(self._cursor_x - dist,
+ self._cursor_y - dist,
+ dist * 2, dist * 2)
+
+ palette_width, palette_height = palette_dim
+
+ x = rect.x + rect.width * invoker_halign + \
+ palette_width * palette_halign
+
+ y = rect.y + rect.height * invoker_valign + \
+ palette_height * palette_valign
+
+ return gtk.gdk.Rectangle(int(x), int(y),
+ palette_width, palette_height)
+
+ def _in_screen(self, rect):
+ return rect.x >= self._screen_area.x and \
+ rect.y >= self._screen_area.y and \
+ rect.x + rect.width <= self._screen_area.width and \
+ rect.y + rect.height <= self._screen_area.height
+
+ def _get_area_in_screen(self, rect):
+ """Return area of rectangle visible in the screen"""
+
+ x1 = max(rect.x, self._screen_area.x)
+ y1 = max(rect.y, self._screen_area.y)
+ x2 = min(rect.x + rect.width,
+ self._screen_area.x + self._screen_area.width)
+ y2 = min(rect.y + rect.height,
+ self._screen_area.y + self._screen_area.height)
+
+ return (x2 - x1) * (y2 - y1)
+
+ def _get_alignments(self):
+ if self._position_hint is self.AT_CURSOR:
+ return [(0.0, 0.0, 1.0, 1.0),
+ (0.0, -1.0, 1.0, 0.0),
+ (-1.0, -1.0, 0.0, 0.0),
+ (-1.0, 0.0, 0.0, 1.0)]
+ else:
+ return self.BOTTOM + self.RIGHT + self.TOP + self.LEFT
+
+ def get_position_for_alignment(self, alignment, palette_dim):
+ rect = self._get_position_for_alignment(alignment, palette_dim)
+ if self._in_screen(rect):
+ return rect
+ else:
+ return None
+
+ def get_position(self, palette_dim):
+ alignment = self.get_alignment(palette_dim)
+ rect = self._get_position_for_alignment(alignment, palette_dim)
+
+ # In case our efforts to find an optimum place inside the screen
+ # failed, just make sure the palette fits inside the screen if at all
+ # possible.
+ rect.x = max(0, rect.x)
+ rect.y = max(0, rect.y)
+
+ rect.x = min(rect.x, self._screen_area.width - rect.width)
+ rect.y = min(rect.y, self._screen_area.height - rect.height)
+
+ return rect
+
+ def get_alignment(self, palette_dim):
+ best_alignment = None
+ best_area = -1
+ for alignment in self._get_alignments():
+ pos = self._get_position_for_alignment(alignment, palette_dim)
+ if self._in_screen(pos):
+ return alignment
+
+ area = self._get_area_in_screen(pos)
+ if area > best_area:
+ best_alignment = alignment
+ best_area = area
+
+ # Palette horiz/vert alignment
+ ph = best_alignment[0]
+ pv = best_alignment[1]
+
+ # Invoker horiz/vert alignment
+ ih = best_alignment[2]
+ iv = best_alignment[3]
+
+ rect = self.get_rect()
+ screen_area = self._screen_area
+
+ if best_alignment in self.LEFT or best_alignment in self.RIGHT:
+ dtop = rect.y - screen_area.y
+ dbottom = screen_area.y + screen_area.height - rect.y - rect.width
+
+ iv = 0
+
+ # Set palette_valign to align to screen on the top
+ if dtop > dbottom:
+ pv = -float(dtop) / palette_dim[1]
+
+ # Set palette_valign to align to screen on the bottom
+ else:
+ pv = -float(palette_dim[1] - dbottom - rect.height) \
+ / palette_dim[1]
+
+ elif best_alignment in self.TOP or best_alignment in self.BOTTOM:
+ dleft = rect.x - screen_area.x
+ dright = screen_area.x + screen_area.width - rect.x - rect.width
+
+ ih = 0
+
+ # Set palette_halign to align to screen on left
+ if dleft > dright:
+ ph = -float(dleft) / palette_dim[0]
+
+ # Set palette_halign to align to screen on right
+ else:
+ ph = -float(palette_dim[0] - dright - rect.width) \
+ / palette_dim[0]
+
+ return (ph, pv, ih, iv)
+
+ def has_rectangle_gap(self):
+ return False
+
+ def draw_rectangle(self, event, palette):
+ pass
+
+ def notify_popup(self):
+ pass
+
+ def notify_popdown(self):
+ self._cursor_x = -1
+ self._cursor_y = -1
+
+ def _ensure_palette_exists(self):
+ if self.parent and self.palette is None:
+ palette = self.parent.create_palette()
+ if palette is not None:
+ self.palette = palette
+
+ def notify_mouse_enter(self):
+ self._ensure_palette_exists()
+ self.emit('mouse-enter')
+
+ def notify_mouse_leave(self):
+ self.emit('mouse-leave')
+
+ def notify_right_click(self):
+ self._ensure_palette_exists()
+ self.emit('right-click')
+
+ def get_palette(self):
+ return self._palette
+
+ def set_palette(self, palette):
+ if self._palette is not None:
+ self._palette.popdown(immediate=True)
+
+ if self._palette:
+ self._palette.props.invoker = None
+
+ self._palette = palette
+
+ if self._palette:
+ self._palette.props.invoker = self
+
+ palette = gobject.property(
+ type=object, setter=set_palette, getter=get_palette)
+
+
+class WidgetInvoker(Invoker):
+
+ def __init__(self, parent=None, widget=None):
+ Invoker.__init__(self)
+
+ self._widget = None
+ self._enter_hid = None
+ self._leave_hid = None
+ self._release_hid = None
+
+ if parent or widget:
+ self.attach_widget(parent, widget)
+
+ def attach_widget(self, parent, widget=None):
+ if widget:
+ self._widget = widget
+ else:
+ self._widget = parent
+
+ self.notify('widget')
+
+ self._enter_hid = self._widget.connect('enter-notify-event',
+ self.__enter_notify_event_cb)
+ self._leave_hid = self._widget.connect('leave-notify-event',
+ self.__leave_notify_event_cb)
+ self._release_hid = self._widget.connect('button-release-event',
+ self.__button_release_event_cb)
+
+ self.attach(parent)
+
+ def detach(self):
+ Invoker.detach(self)
+ self._widget.disconnect(self._enter_hid)
+ self._widget.disconnect(self._leave_hid)
+ self._widget.disconnect(self._release_hid)
+
+ def get_rect(self):
+ allocation = self._widget.get_allocation()
+ if self._widget.window is not None:
+ x, y = self._widget.window.get_origin()
+ else:
+ logging.warning(
+ "Trying to position palette with invoker that's not realized.")
+ x = 0
+ y = 0
+
+ if self._widget.flags() & gtk.NO_WINDOW:
+ x += allocation.x
+ y += allocation.y
+
+ width = allocation.width
+ height = allocation.height
+
+ return gtk.gdk.Rectangle(x, y, width, height)
+
+ def has_rectangle_gap(self):
+ return True
+
+ def draw_rectangle(self, event, palette):
+ if self._widget.flags() & gtk.NO_WINDOW:
+ x, y = self._widget.allocation.x, self._widget.allocation.y
+ else:
+ x = y = 0
+
+ wstyle = self._widget.get_style()
+ gap = _calculate_gap(self.get_rect(), palette.get_rect())
+
+ if gap:
+ wstyle.paint_box_gap(event.window, gtk.STATE_PRELIGHT,
+ gtk.SHADOW_IN, event.area, self._widget,
+ "palette-invoker", x, y,
+ self._widget.allocation.width,
+ self._widget.allocation.height,
+ gap[0], gap[1], gap[2])
+ else:
+ wstyle.paint_box(event.window, gtk.STATE_PRELIGHT,
+ gtk.SHADOW_IN, event.area, self._widget,
+ "palette-invoker", x, y,
+ self._widget.allocation.width,
+ self._widget.allocation.height)
+
+ def __enter_notify_event_cb(self, widget, event):
+ self.notify_mouse_enter()
+
+ def __leave_notify_event_cb(self, widget, event):
+ self.notify_mouse_leave()
+
+ def __button_release_event_cb(self, widget, event):
+ if event.button == 3:
+ self.notify_right_click()
+ return True
+ else:
+ return False
+
+ def get_toplevel(self):
+ return self._widget.get_toplevel()
+
+ def notify_popup(self):
+ Invoker.notify_popup(self)
+ self._widget.queue_draw()
+
+ def notify_popdown(self):
+ Invoker.notify_popdown(self)
+ self._widget.queue_draw()
+
+ def _get_widget(self):
+ return self._widget
+ widget = gobject.property(type=object, getter=_get_widget, setter=None)
+
+
+class CanvasInvoker(Invoker):
+
+ def __init__(self, parent=None):
+ Invoker.__init__(self)
+
+ self._position_hint = self.AT_CURSOR
+ self._motion_hid = None
+ self._release_hid = None
+ self._item = None
+
+ if parent:
+ self.attach(parent)
+
+ def attach(self, parent):
+ Invoker.attach(self, parent)
+
+ self._item = parent
+ self._motion_hid = self._item.connect('motion-notify-event',
+ self.__motion_notify_event_cb)
+ self._release_hid = self._item.connect('button-release-event',
+ self.__button_release_event_cb)
+
+ def detach(self):
+ Invoker.detach(self)
+ self._item.disconnect(self._motion_hid)
+ self._item.disconnect(self._release_hid)
+
+ def get_default_position(self):
+ return self.AT_CURSOR
+
+ def get_rect(self):
+ context = self._item.get_context()
+ if context:
+ x, y = context.translate_to_screen(self._item)
+ width, height = self._item.get_allocation()
+ return gtk.gdk.Rectangle(x, y, width, height)
+ else:
+ return gtk.gdk.Rectangle()
+
+ def __motion_notify_event_cb(self, button, event):
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ self.notify_mouse_enter()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ self.notify_mouse_leave()
+
+ return False
+
+ def __button_release_event_cb(self, button, event):
+ if event.button == 3:
+ self.notify_right_click()
+ return True
+ else:
+ return False
+
+ def get_toplevel(self):
+ return hippo.get_canvas_for_item(self._item).get_toplevel()
+
+
+class ToolInvoker(WidgetInvoker):
+
+ def __init__(self, parent=None):
+ WidgetInvoker.__init__(self)
+
+ if parent:
+ self.attach_tool(parent)
+
+ def attach_tool(self, widget):
+ self.attach_widget(widget, widget.child)
+
+ def _get_alignments(self):
+ parent = self._widget.get_parent()
+ if parent is None:
+ return WidgetInvoker._get_alignments()
+
+ if parent.get_orientation() is gtk.ORIENTATION_HORIZONTAL:
+ return self.BOTTOM + self.TOP
+ else:
+ return self.LEFT + self.RIGHT
+
+
+class CellRendererInvoker(Invoker):
+
+ def __init__(self):
+ Invoker.__init__(self)
+
+ self._position_hint = self.AT_CURSOR
+ self._tree_view = None
+ self._cell_renderer = None
+ self._motion_hid = None
+ self._leave_hid = None
+ self._release_hid = None
+ self.path = None
+
+ def attach_cell_renderer(self, tree_view, cell_renderer):
+ self._tree_view = tree_view
+ self._cell_renderer = cell_renderer
+
+ self._motion_hid = tree_view.connect('motion-notify-event',
+ self.__motion_notify_event_cb)
+ self._leave_hid = tree_view.connect('leave-notify-event',
+ self.__leave_notify_event_cb)
+ self._release_hid = tree_view.connect('button-release-event',
+ self.__button_release_event_cb)
+
+ self.attach(cell_renderer)
+
+ def detach(self):
+ Invoker.detach(self)
+ self._tree_view.disconnect(self._motion_hid)
+ self._tree_view.disconnect(self._leave_hid)
+ self._tree_view.disconnect(self._release_hid)
+
+ def get_rect(self):
+ allocation = self._tree_view.get_allocation()
+ if self._tree_view.window is not None:
+ x, y = self._tree_view.window.get_origin()
+ else:
+ logging.warning(
+ "Trying to position palette with invoker that's not realized.")
+ x = 0
+ y = 0
+
+ if self._tree_view.flags() & gtk.NO_WINDOW:
+ x += allocation.x
+ y += allocation.y
+
+ width = allocation.width
+ height = allocation.height
+
+ return gtk.gdk.Rectangle(x, y, width, height)
+
+ def __motion_notify_event_cb(self, widget, event):
+ if event.window != widget.get_bin_window():
+ return
+ if self._point_in_cell_renderer(event.x, event.y):
+
+ tree_view = self._tree_view
+ path, column_, x_, y_ = tree_view.get_path_at_pos(int(event.x),
+ int(event.y))
+ if path != self.path:
+ if self.path is not None:
+ self._redraw_path(self.path)
+ if path is not None:
+ self._redraw_path(path)
+ if self.palette is not None:
+ self.palette.popdown(immediate=True)
+ self.palette = None
+ self.path = path
+
+ self.notify_mouse_enter()
+ else:
+ if self.path is not None:
+ self._redraw_path(self.path)
+ self.path = None
+ self.notify_mouse_leave()
+
+ def _redraw_path(self, path):
+ for column in self._tree_view.get_columns():
+ if self._cell_renderer in column.get_cell_renderers():
+ break
+ area = self._tree_view.get_background_area(path, column)
+ x, y = \
+ self._tree_view.convert_bin_window_to_widget_coords(area.x, area.y)
+ self._tree_view.queue_draw_area(x, y, area.width, area.height)
+
+ def __leave_notify_event_cb(self, widget, event):
+ self.notify_mouse_leave()
+
+ def __button_release_event_cb(self, widget, event):
+ if event.button == 1 and self._point_in_cell_renderer(event.x,
+ event.y):
+ tree_view = self._tree_view
+ path, column_, x_, y_ = tree_view.get_path_at_pos(int(event.x),
+ int(event.y))
+ self._cell_renderer.emit('clicked', path)
+ # So the treeview receives it and knows a drag isn't going on
+ return False
+ if event.button == 3 and self._point_in_cell_renderer(event.x,
+ event.y):
+ self.notify_right_click()
+ return True
+ else:
+ return False
+
+ def _point_in_cell_renderer(self, event_x, event_y):
+ pos = self._tree_view.get_path_at_pos(int(event_x), int(event_y))
+ if pos is None:
+ return False
+
+ path_, column, x, y_ = pos
+
+ for cell_renderer in column.get_cell_renderers():
+ if cell_renderer == self._cell_renderer:
+ cell_x, cell_width = column.cell_get_position(cell_renderer)
+ if x > cell_x and x < (cell_x + cell_width):
+ return True
+ return False
+
+ return False
+
+ def get_toplevel(self):
+ return self._tree_view.get_toplevel()
+
+ def notify_popup(self):
+ Invoker.notify_popup(self)
+
+ def notify_popdown(self):
+ Invoker.notify_popdown(self)
+ self.palette = None
+
+ def get_default_position(self):
+ return self.AT_CURSOR
diff --git a/toolkit/json.py b/toolkit/json.py
new file mode 100644
index 0000000..a8cbcbd
--- /dev/null
+++ b/toolkit/json.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""
+Unify usage of simplejson in Python 2.5/2.6
+
+In Python 2.5 it imports simplejson module, in 2.6 native json module.
+
+Usage:
+
+ import toolkit.json as json
+
+ # and using regular simplejson interface with module json
+ json.dumps([])
+
+"""
+
+try:
+ from json import *
+ dumps
+except (ImportError, NameError):
+ from simplejson import *
diff --git a/toolkit/pixbuf.py b/toolkit/pixbuf.py
new file mode 100644
index 0000000..c3bb7d1
--- /dev/null
+++ b/toolkit/pixbuf.py
@@ -0,0 +1,116 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""gtk.gdk.Pixbuf extensions"""
+
+import re
+import cStringIO
+import gtk
+import rsvg
+import cairo
+import logging
+
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor, is_valid
+from sugar.util import LRU
+
+
+def to_file(pixbuf):
+ """Convert pixbuf object to file object"""
+
+ def push(pixbuf, buffer):
+ buffer.write(pixbuf)
+
+ buffer = cStringIO.StringIO()
+ pixbuf.save_to_callback(push, 'png', user_data=buffer)
+ buffer.seek(0)
+
+ return buffer
+
+def to_str(pixbuf):
+ """Convert pixbuf object to string"""
+ return to_file(pixbuf).getvalue()
+
+def from_str(str):
+ """Convert string to pixbuf object"""
+
+ loader = gtk.gdk.pixbuf_loader_new_with_mime_type('image/png')
+
+ try:
+ loader.write(str)
+ except Exception, e:
+ logging.error('pixbuf.from_str: %s' % e)
+ return None
+ finally:
+ loader.close()
+
+ return loader.get_pixbuf()
+
+
+def at_size_with_ratio(pixbuf, width, height, type=gtk.gdk.INTERP_BILINEAR):
+ image_width = pixbuf.get_width()
+ image_height = pixbuf.get_height()
+
+ ratio_width = float(width) / image_width
+ ratio_height = float(height) / image_height
+ ratio = min(ratio_width, ratio_height)
+
+ if ratio_width != ratio:
+ ratio_width = ratio
+ width = int(image_width * ratio)
+ elif ratio_height != ratio:
+ ratio_height = ratio
+ height = int(image_height * ratio)
+
+ return pixbuf.scale_simple(width, height, type)
+
+def from_svg_at_size(filename=None, width=None, height=None, handle=None,
+ keep_ratio=True):
+ """Scale and load SVG into pixbuf"""
+
+ if not handle:
+ handle = rsvg.Handle(filename)
+
+ dimensions = handle.get_dimension_data()
+ icon_width = dimensions[0]
+ icon_height = dimensions[1]
+
+ if icon_width != width or icon_height != height:
+ ratio_width = float(width) / icon_width
+ ratio_height = float(height) / icon_height
+
+ if keep_ratio:
+ ratio = min(ratio_width, ratio_height)
+ if ratio_width != ratio:
+ ratio_width = ratio
+ width = int(icon_width * ratio)
+ elif ratio_height != ratio:
+ ratio_height = ratio
+ height = int(icon_height * ratio)
+ else:
+ ratio_width = 1
+ ratio_height = 1
+
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+ context = cairo.Context(surface)
+ context.scale(ratio_width, ratio_height)
+ handle.render_cairo(context)
+
+ loader = gtk.gdk.pixbuf_loader_new_with_mime_type('image/png')
+ surface.write_to_png(loader)
+ loader.close()
+
+ return loader.get_pixbuf()
diff --git a/toolkit/radiopalette.py b/toolkit/radiopalette.py
new file mode 100644
index 0000000..9c902b1
--- /dev/null
+++ b/toolkit/radiopalette.py
@@ -0,0 +1,109 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+import gtk
+
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.palette import Palette
+
+
+class RadioMenuButton(ToolButton):
+
+ def __init__(self, **kwargs):
+ ToolButton.__init__(self, **kwargs)
+ self.selected_button = None
+
+ if self.props.palette:
+ self.__palette_cb(None, None)
+
+ self.connect('clicked', self.__clicked_cb)
+ self.connect('notify::palette', self.__palette_cb)
+
+ def _do_clicked(self):
+ if self.palette is None:
+ return
+ if self.palette.is_up() and \
+ self.palette.palette_state == Palette.SECONDARY:
+ self.palette.popdown(immediate=True)
+ else:
+ self.palette.popup(immediate=True)
+ self.palette.props.invoker.emit('right-click')
+
+ def __palette_cb(self, widget, pspec):
+ if not isinstance(self.props.palette, RadioPalette):
+ return
+ self.props.palette.update_button()
+
+ def __clicked_cb(self, button):
+ self._do_clicked()
+
+
+class RadioToolsButton(RadioMenuButton):
+
+ def __init__(self, **kwargs):
+ RadioMenuButton.__init__(self, **kwargs)
+
+ def _do_clicked(self):
+ if not self.selected_button:
+ return
+ self.selected_button.emit('clicked')
+
+
+class RadioPalette(Palette):
+
+ def __init__(self, **kwargs):
+ Palette.__init__(self, **kwargs)
+
+ self.button_box = gtk.HBox()
+ self.button_box.show()
+ self.set_content(self.button_box)
+
+ def append(self, button, label):
+ children = self.button_box.get_children()
+
+ if button.palette is not None:
+ raise RuntimeError("Palette's button should not have sub-palettes")
+
+ button.show()
+ button.connect('clicked', self.__clicked_cb)
+ self.button_box.pack_start(button, fill=False)
+ button.palette_label = label
+
+ if not children:
+ self.__clicked_cb(button)
+
+ def update_button(self):
+ for i in self.button_box.get_children():
+ self.__clicked_cb(i)
+
+ def __clicked_cb(self, button):
+ if not button.get_active():
+ return
+
+ self.set_primary_text(button.palette_label)
+ self.popdown(immediate=True)
+
+ if self.props.invoker is not None:
+ parent = self.props.invoker.parent
+ else:
+ parent = None
+ if not isinstance(parent, RadioMenuButton):
+ return
+
+ parent.props.label = button.palette_label
+ parent.set_icon(button.props.icon_name)
+ parent.selected_button = button
diff --git a/toolkit/scrolledbox.py b/toolkit/scrolledbox.py
new file mode 100644
index 0000000..ead071e
--- /dev/null
+++ b/toolkit/scrolledbox.py
@@ -0,0 +1,191 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+
+from sugar.graphics.icon import Icon
+
+class ScrollButton(gtk.ToolButton):
+ def __init__(self, icon_name):
+ gtk.ToolButton.__init__(self)
+
+ icon = Icon(icon_name = icon_name,
+ icon_size=gtk.ICON_SIZE_SMALL_TOOLBAR)
+ # The alignment is a hack to work around gtk.ToolButton code
+ # that sets the icon_size when the icon_widget is a gtk.Image
+ alignment = gtk.Alignment(0.5, 0.5)
+ alignment.add(icon)
+ self.set_icon_widget(alignment)
+
+class ScrolledBox(gtk.EventBox):
+ def __init__(self, orientation,
+ arrows_policy=gtk.POLICY_AUTOMATIC,
+ scroll_policy=gtk.POLICY_AUTOMATIC):
+
+ gtk.EventBox.__init__(self)
+ self.orientation = orientation
+ self._viewport = None
+ self._abox = None
+ self._aviewport = None
+ self._aviewport_sig = None
+ self._arrows_policy = arrows_policy
+ self._scroll_policy = scroll_policy
+ self._left = None
+ self._right = None
+
+ if orientation == gtk.ORIENTATION_HORIZONTAL:
+ box = gtk.HBox()
+ else:
+ box = gtk.VBox()
+ if self._arrows_policy == gtk.POLICY_AUTOMATIC:
+ box.connect("size-allocate", self._box_allocate_cb)
+ self.add(box)
+
+ if self._arrows_policy != gtk.POLICY_NEVER:
+ if orientation == gtk.ORIENTATION_HORIZONTAL:
+ self._left = ScrollButton('go-left')
+ else:
+ self._left = ScrollButton('go-up')
+ self._left.connect('clicked', self._scroll_cb,
+ gtk.gdk.SCROLL_LEFT)
+ box.pack_start(self._left, False, False, 0)
+
+ self._scrolled = gtk.ScrolledWindow()
+ if orientation == gtk.ORIENTATION_HORIZONTAL:
+ self._scrolled.set_policy(scroll_policy, gtk.POLICY_NEVER)
+ else:
+ self._scrolled.set_policy(gtk.POLICY_NEVER, scroll_policy)
+ self._scrolled.connect('scroll-event', self._scroll_event_cb)
+ box.pack_start(self._scrolled, True, True, 0)
+
+ if orientation == gtk.ORIENTATION_HORIZONTAL:
+ self._adj = self._scrolled.get_hadjustment()
+ else:
+ self._adj = self._scrolled.get_vadjustment()
+ self._adj.connect('changed', self._scroll_changed_cb)
+ self._adj.connect('value-changed', self._scroll_changed_cb)
+
+ if self._arrows_policy != gtk.POLICY_NEVER:
+ if orientation == gtk.ORIENTATION_HORIZONTAL:
+ self._right = ScrollButton('go-right')
+ else:
+ self._right = ScrollButton('go-down')
+ self._right.connect('clicked', self._scroll_cb,
+ gtk.gdk.SCROLL_RIGHT)
+ box.pack_start(self._right, False, False, 0)
+
+ def modify_fg(self, state, bg):
+ gtk.EventBox.modify_fg(self, state, bg)
+ self._viewport.get_parent().modify_fg(state, bg)
+
+ def modify_bg(self, state, bg):
+ gtk.EventBox.modify_bg(self, state, bg)
+ self._viewport.get_parent().modify_bg(state, bg)
+
+ def set_viewport(self, widget):
+ if widget == self._viewport: return
+ if self._viewport and self._aviewport_sig:
+ self._viewport.disconnect(self._aviewport_sig)
+ self._viewport = widget
+
+ if self._arrows_policy == gtk.POLICY_AUTOMATIC:
+ self._aviewport_sig = self._viewport.connect('size-allocate',
+ self._viewport_allocate_cb)
+
+ self._scrolled.add_with_viewport(widget)
+
+ def get_viewport_allocation(self):
+ alloc = self._scrolled.get_allocation()
+ alloc.x -= self._adj.get_value()
+ return alloc
+
+ def get_adjustment(self):
+ return self._adj
+
+ def _box_allocate_cb(self, w, a):
+ self._abox = a
+ self._update_arrows()
+
+ def _viewport_allocate_cb(self, w, a):
+ self._aviewport = a
+ self._update_arrows()
+
+ def _update_arrows(self):
+ if not self._abox or not self._aviewport: return
+
+ if self.orientation == gtk.ORIENTATION_HORIZONTAL:
+ show_flag = self._abox.width < self._aviewport.width
+ else:
+ show_flag = self._abox.height < self._aviewport.height
+
+ if show_flag:
+ self._left.show()
+ self._right.show()
+ else:
+ self._left.hide()
+ self._right.hide()
+
+ def _scroll_event_cb(self, widget, event):
+ if self.orientation == gtk.ORIENTATION_HORIZONTAL:
+ if event.direction == gtk.gdk.SCROLL_UP:
+ event.direction = gtk.gdk.SCROLL_LEFT
+ if event.direction == gtk.gdk.SCROLL_DOWN:
+ event.direction = gtk.gdk.SCROLL_RIGHT
+ else:
+ if event.direction == gtk.gdk.SCROLL_LEFT:
+ event.direction = gtk.gdk.SCROLL_UP
+ if event.direction == gtk.gdk.SCROLL_RIGHT:
+ event.direction = gtk.gdk.SCROLL_DOWN
+
+ if self._scroll_policy == gtk.POLICY_NEVER:
+ self._scroll_cb(None, event.direction)
+
+ return False
+
+ def _scroll_cb(self, widget, direction):
+ if direction in (gtk.gdk.SCROLL_LEFT, gtk.gdk.SCROLL_UP):
+ val = max(self._adj.get_property('lower'), self._adj.get_value()
+ - self._adj.get_property('page_increment'))
+ else:
+ val = min(self._adj.get_property('upper')
+ - self._adj.get_property('page_size'),
+ self._adj.get_value()
+ + self._adj.get_property('page_increment'))
+
+ self._adj.set_value(val)
+
+ def _scroll_changed_cb(self, widget):
+ val = self._adj.get_value()
+ if self._left:
+ if val == 0:
+ self._left.set_sensitive(False)
+ else:
+ self._left.set_sensitive(True)
+
+ if self._right:
+ if val >= self._adj.get_property('upper') - \
+ self._adj.get_property('page_size'):
+ self._right.set_sensitive(False)
+ else:
+ self._right.set_sensitive(True)
+
+class HScrolledBox(ScrolledBox):
+ def __init__(self, **kwargs):
+ ScrolledBox.__init__(self, gtk.ORIENTATION_HORIZONTAL, **kwargs)
+
+class VScrolledBox(ScrolledBox):
+ def __init__(self, **kwargs):
+ ScrolledBox.__init__(self, gtk.ORIENTATION_VERTICAL, **kwargs)
diff --git a/toolkit/tarball.py b/toolkit/tarball.py
new file mode 100644
index 0000000..0a4a1b2
--- /dev/null
+++ b/toolkit/tarball.py
@@ -0,0 +1,125 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Simplify tarfile module usage"""
+
+import os
+import time
+import tarfile
+import cStringIO
+import gtk
+import zipfile
+import tempfile
+import shutil
+
+
+class TarballError(Exception):
+ """Base Tarball exception."""
+ pass
+
+
+class BadDataTypeError(TarballError):
+ """Exception for unsupported data type in read/write methods."""
+ pass
+
+
+class Tarball:
+ """
+ Wrap standart tarfile module to simplify read/write operations.
+ In read mode Tarball can load zip files as well.
+
+ Write usage:
+
+ # create Tarball object
+ # to see all supported modes use
+ # http://docs.python.org/library/tarfile.html#tarfile.open
+ tar = Tarball(tarfile, 'w')
+
+ # write string to file in tarball
+ tar.write('name within tarball', 'string to write')
+
+ # save and close tarball file
+ tar.close()
+
+ Read usage:
+
+ # create Tarball object
+ tar = Tarball(tarfile)
+
+ # read content of file in tarball to string
+ str_content = tar.read('name within tarball')
+ """
+
+ def __init__(self, name=None, mode='r', mtime=None):
+ if not mode.startswith('r') or tarfile.is_tarfile(name):
+ self.__tar = tarfile.TarFile(name=name, mode=mode)
+ else:
+ # convert for tar
+
+ if not zipfile.is_zipfile(name):
+ raise tarfile.ReadError()
+
+ try:
+ tmp_dir = tempfile.mkdtemp()
+ tmp_fd, tmp_name = tempfile.mkstemp()
+ tmp_fo = os.fdopen(tmp_fd, 'w')
+
+ zip = zipfile.ZipFile(name)
+ zip.extractall(tmp_dir)
+
+ tar = tarfile.TarFile(fileobj=tmp_fo, mode='w')
+ tar.add(tmp_dir, arcname='')
+ tar.close()
+
+ self.__tar = tarfile.TarFile(name=tmp_name, mode=mode)
+ finally:
+ tmp_fo.close()
+ os.unlink(tmp_name)
+ shutil.rmtree(tmp_dir)
+
+ if mtime:
+ self.mtime = mtime
+ else:
+ self.mtime = time.time()
+
+ def close(self):
+ """Save(if 'r' mode was given) and close tarball file."""
+ self.__tar.close()
+
+ def getnames(self):
+ """Return names of members sorted by creation order."""
+ return self.__tar.getnames()
+
+ def read(self, arcname):
+ """Returns sring with content of given file from tarball."""
+ file_o = self.__tar.extractfile(arcname.encode('utf8'))
+ if not file_o:
+ return None
+ out = file_o.read()
+ file_o.close()
+ return out
+
+ def write(self, arcname, data, mode=0644):
+ """
+ Stores given object to file in tarball.
+ Raises BadDataTypeError exception If data type isn't supported.
+ """
+ info = tarfile.TarInfo(arcname.encode('utf8'))
+ info.mode = mode
+ info.mtime = self.mtime
+ info.size = len(data)
+
+ self.__tar.addfile(info, cStringIO.StringIO(data))
diff --git a/toolkit/temposlider.py b/toolkit/temposlider.py
new file mode 100644
index 0000000..8fcf8cb
--- /dev/null
+++ b/toolkit/temposlider.py
@@ -0,0 +1,211 @@
+# Copyright (C) 2006-2008, TamTam Team
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+# Widget was copy&pasted from TamTam activities
+
+import gtk
+import rsvg
+import cairo
+
+from sugar.graphics import style
+
+class TempoSlider(gtk.HBox):
+ def __init__(self, min_value, max_value):
+ gtk.HBox.__init__(self)
+
+ self._pixbuf = [None] * 8
+ self._image = gtk.Image()
+ self._image.show()
+
+ # used to store tempo updates while the slider is active
+ self._delayed = 0
+ self._active = False
+
+ self.adjustment = gtk.Adjustment(min_value, min_value, max_value,
+ (max_value - min_value) / 8, (max_value - min_value) / 8, 0)
+ self._adjustment_h = self.adjustment.connect('value-changed',
+ self._changed_cb)
+
+ slider = gtk.HScale(adjustment = self.adjustment)
+ slider.show()
+ slider.set_draw_value(False)
+ slider.connect("button-press-event", self._press_cb)
+ slider.connect("button-release-event", self._release_cb)
+
+ self.pack_start(slider, True, True)
+ self.pack_end(self._image, False, False)
+
+ def set_value(self, tempo, quiet = False):
+ if self._active:
+ self._delayed = tempo
+ elif quiet:
+ self.adjustment.handler_block(self._adjustment_h)
+ self.adjustment.set_value(tempo)
+ self._update(tempo)
+ self.adjustment.handler_unblock(self._adjustment_h)
+ else:
+ self.adjustment.set_value(tempo)
+
+ def _changed_cb(self, widget):
+ self._update(widget.get_value())
+
+ def _update(self, tempo):
+ def map_range(value, ilower, iupper, olower, oupper):
+ if value == iupper:
+ return oupper
+ return olower + int((oupper-olower+1) * (value-ilower) /
+ float(iupper-ilower))
+
+ img = map_range(tempo, self.adjustment.lower,
+ self.adjustment.upper, 0, 7)
+
+ if not self._pixbuf[img]:
+ svg = rsvg.Handle(data=IMAGE[img])
+ self._pixbuf[img] = _from_svg_at_size(handle=svg,
+ width=style.STANDARD_ICON_SIZE,
+ height=style.STANDARD_ICON_SIZE)
+
+ self._image.set_from_pixbuf(self._pixbuf[img])
+
+ def _press_cb(self, widget, event):
+ self._active = True
+
+ def _release_cb(self, widget, event):
+ self._active = False
+ if self._delayed != 0:
+ self.set_value(self._delayed, True)
+ self._delayed = 0
+
+def _from_svg_at_size(filename=None, width=None, height=None, handle=None,
+ keep_ratio=True):
+ """ import from pixbuf.py """
+
+ if not handle:
+ handle = rsvg.Handle(filename)
+
+ dimensions = handle.get_dimension_data()
+ icon_width = dimensions[0]
+ icon_height = dimensions[1]
+
+ if icon_width != width or icon_height != height:
+ ratio_width = float(width) / icon_width
+ ratio_height = float(height) / icon_height
+
+ if keep_ratio:
+ ratio = min(ratio_width, ratio_height)
+ if ratio_width != ratio:
+ ratio_width = ratio
+ width = int(icon_width * ratio)
+ elif ratio_height != ratio:
+ ratio_height = ratio
+ height = int(icon_height * ratio)
+ else:
+ ratio_width = 1
+ ratio_height = 1
+
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+ context = cairo.Context(surface)
+ context.scale(ratio_width, ratio_height)
+ handle.render_cairo(context)
+
+ loader = gtk.gdk.pixbuf_loader_new_with_mime_type('image/png')
+ surface.write_to_png(loader)
+ loader.close()
+
+ return loader.get_pixbuf()
+
+IMAGE = [None] * 8
+
+IMAGE[0] = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M23.5,6.5c3,3,7,7,9,11c-7,5-4,6-3,26c-1,1-8,1-9,0c0,0,2,1,2-1
+ c0-3-2-7-2-11c0-2,1-4,1-6c0-3-2-1-2-3c0-3,3-8,3-11c0-2-1-1-2-2v-3H23.5z"/>
+</svg>
+"""
+
+IMAGE[1] = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M27.5,44.5v-3C28.5,42.5,28.5,43.5,27.5,44.5z M26.5,10.5
+ c2,2,2,6,2,8c0,4-3,11-3,13s4,7,7,10c-2,2-4,3-5,5h-6c1-1,2-3,2-5c0-3-2-9-3-14c0,0,0-1-1,0v-6c0-3,3-8,3-11c0-1-2-2-2-6h3
+ C23.5,5.5,26.5,9.5,26.5,10.5z"/>
+</svg>
+"""
+
+IMAGE[2] = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M30.5,17.5c0,3-2,2-2,4c0,3,4,14,7,21c-1,0-3,1-5,1c1-1,2,0,2-3
+ c0-2-4-7-6-10c-3,3-5,8-7,13c-1,0-3-1-4-1c3-3,7-14,7-18s-1-3-4-4c3-2,4-8,4-14h3C23.5,9.5,30.5,14.5,30.5,17.5z"/>
+</svg>
+"""
+
+IMAGE[3] = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M34.5,22.5c-1-1-2-4-5-6c-1,2,0,3,0,6c0,2-3,4-3,7c0,2,4,2,4,4
+ c0,3-1,4-2,5c0-1,0-3-1-4c-1,3-2,7-3,10c-4-3,0-6,0-9s-3-11-4-17l-4,4c1-5,8.25-11.12,7.25-16.12c0.68,0.68,3.029,0,2.87,2.12
+ C26.5,10.25,33.62,17.75,34.5,22.5z"/>
+</svg>
+"""
+
+IMAGE[4] = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M24.5,13.5c2,1,5,3,5,6c0,2-2,3-2,5c0,9,11,4,11,13c-1,0-3-2-4-3
+ c-3-1-9,1-10-3c-2,3-5,7-7,11c-3,0-3-1-4-1c0-2,3-3,4-6s4-8,4-10c0-3-1-3-2-5c-1,0-2,1-3,2c0-1,2-3,2-4c1-2,3-5,2-8c0,0,1-1,4-2
+ C25.5,9.5,25.5,11.5,24.5,13.5z"/>
+</svg>
+"""
+
+IMAGE[5] = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M22.5,10.5c3,2,7,5,7,7c0,3-4,8-4,10c0,3,1,3,1,5h5l2-2l2,2v4
+ c-1,0-3-2-5-2c-3,0-5,1-8,1c-1,3-2,7-2,10h-5c1-1,3-3,3-4c1-5,1-11,1-18l-1-1c-1,1-1.75,2.88-2.75,2.88c0,0-0.25-0.63-0.25-1.63
+ c4-4,2-8.25,2-13.25c0-1,0.25-2.5,0.38-5.38L22.5,5.5C23.12,6.5,22.5,8.5,22.5,10.5z"/>
+<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#333333" stroke="#333333" stroke-linecap="round" stroke-linejoin="round" points="
+ 25,20 25.25,16.75 26.5,17.88 "/>
+</svg>
+"""
+
+IMAGE[6] = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M20.5,7.5c1,1,1,3,1,4c10,4,8,6,8,14c0,2,6,9,10,13c-1,2-2,4-4,5
+ c1.62-8.88-8.75-13.88-12-15c-1,1-1,0-1,2c0,3,2,5,3,7c-1,1-3,2-6,2c0-1,2-1,2-4c0-2-4-4-4-6c0-3,3-4,5-6c-3-8-8-2-11-6h6
+ c0-1,1,0,1-3c0-2-1-1-2-2l1-5H20.5z"/>
+</svg>
+"""
+
+IMAGE[7] = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M20.5,12.5c0.67,0.4,0.4,1.9,1.75,2.25s1.05-0.38,1.5-0.37
+ c4.971,0,10.95-0.88,11.75,7.12c-1-2-3-4-5-5l-4,1c1,2,4,4,5,7c1,1,1,4,1,6c3,3,8-1,11,6c-2.88-0.82-4.25-2.62-12.75-2.75
+ c-1.561-0.02-2.34-1.561-3.75-1.87c-3.42-0.76-4.67-0.38-5.5-0.38c-3,0-8,7-11,7c-2,0-3-1-3-2c4,2,8-4,9-7c2-1,5-1,8-3c-2-4-6-5-8-3
+ l-6-6l2-2c1,1,1,2,1,4c1,0,4.12,0.38,6.12-0.62L16.5,17.5v-5H20.5z"/>
+</svg>
+"""
diff --git a/toolkit/toolbarbox.py b/toolkit/toolbarbox.py
new file mode 100644
index 0000000..7172b8b
--- /dev/null
+++ b/toolkit/toolbarbox.py
@@ -0,0 +1,333 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+import gtk
+import gobject
+
+from sugar.graphics import style
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics import palettegroup
+
+from toolkit.internals.palettewindow import PaletteWindow
+from toolkit.internals.palettewindow import ToolInvoker
+
+
+_ARROW_SIZE = style.zoom(24)
+_LINE_WIDTH = 2
+
+class ToolbarButton(ToolButton):
+
+ def __init__(self, page=None, **kwargs):
+ ToolButton.__init__(self, **kwargs)
+
+ self.page_widget = None
+
+ self.set_page(page)
+
+ self.connect('clicked',
+ lambda widget: self.set_expanded(not self.is_expanded()))
+ self.connect('size-allocate', self.__size_allocate_cb)
+
+ def get_toolbar_box(self):
+ if not hasattr(self.parent, 'owner'):
+ return None
+ return self.parent.owner
+
+ toolbar_box = property(get_toolbar_box)
+
+ def get_page(self):
+ if self.page_widget is None:
+ return None
+ return _get_embedded_page(self.page_widget)
+
+ def set_page(self, page):
+ if page is None:
+ self.page_widget = None
+ return
+
+ self.page_widget, alignment_ = _embed_page(_Box, page)
+ w_, h = gtk.icon_size_lookup(gtk.ICON_SIZE_LARGE_TOOLBAR)
+ page.show()
+
+ if self.props.palette is None:
+ self.props.palette = _ToolbarPalette(invoker=ToolInvoker(self))
+ self._move_page_to_palette()
+
+ page = gobject.property(type=object, getter=get_page, setter=set_page)
+
+ def is_in_palette(self):
+ return self.page is not None and \
+ self.page_widget.parent == self.props.palette
+
+ def is_expanded(self):
+ return self.page is not None and \
+ not self.is_in_palette()
+
+ def popdown(self):
+ if self.props.palette is not None:
+ self.props.palette.popdown(immediate=True)
+
+ def set_expanded(self, expanded):
+ self.popdown()
+
+ if self.page is None or self.is_expanded() == expanded:
+ return
+
+ if not expanded:
+ self._move_page_to_palette()
+ return
+
+ box = self.toolbar_box
+
+ if box.expanded_button is not None:
+ if box.expanded_button.window is not None:
+ # need to redraw it to erase arrow
+ box.expanded_button.window.invalidate_rect(None, True)
+ box.expanded_button.set_expanded(False)
+ box.expanded_button = self
+
+ self._unparent()
+
+ self.modify_bg(gtk.STATE_NORMAL, box.background)
+ _setup_page(self.page_widget, box.background, box.props.padding)
+ box.pack_start(self.page_widget)
+
+ def _move_page_to_palette(self):
+ if self.is_in_palette():
+ return
+
+ self._unparent()
+
+ if isinstance(self.props.palette, _ToolbarPalette):
+ self.props.palette.add(self.page_widget)
+
+ def _unparent(self):
+ if self.page_widget.parent is None:
+ return
+ self.page_widget.parent.remove(self.page_widget)
+
+ def do_expose_event(self, event):
+ if not self.is_expanded() or self.props.palette is not None and \
+ self.props.palette.is_up():
+ ToolButton.do_expose_event(self, event)
+ _paint_arrow(self, event, gtk.ARROW_DOWN)
+ return
+
+ alloc = self.allocation
+
+ self.get_style().paint_box(event.window,
+ gtk.STATE_NORMAL, gtk.SHADOW_IN, event.area, self,
+ 'palette-invoker', alloc.x, 0,
+ alloc.width, alloc.height + _LINE_WIDTH)
+
+ if self.child.state != gtk.STATE_PRELIGHT:
+ self.get_style().paint_box(event.window,
+ gtk.STATE_NORMAL, gtk.SHADOW_NONE, event.area, self, None,
+ alloc.x + _LINE_WIDTH, _LINE_WIDTH,
+ alloc.width - _LINE_WIDTH * 2, alloc.height)
+
+ gtk.ToolButton.do_expose_event(self, event)
+ _paint_arrow(self, event, gtk.ARROW_UP)
+
+ def __size_allocate_cb(self, button, allocation):
+ if self.page_widget is not None:
+ self.page_widget.set_size_request(-1, allocation.height)
+
+
+class ToolbarBox(gtk.VBox):
+
+ def __init__(self, padding=style.TOOLBOX_HORIZONTAL_PADDING):
+ gtk.VBox.__init__(self)
+ self._expanded_button_index = -1
+ self.background = None
+
+ self._toolbar = gtk.Toolbar()
+ self._toolbar.owner = self
+ self._toolbar.connect('remove', self.__remove_cb)
+
+ self._toolbar_widget, self._toolbar_alignment = \
+ _embed_page(gtk.EventBox, self._toolbar)
+ self.pack_start(self._toolbar_widget)
+
+ self.props.padding = padding
+ self.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_TOOLBAR_GREY.get_gdk_color())
+
+ def get_toolbar(self):
+ return self._toolbar
+
+ toolbar = property(get_toolbar)
+
+ def get_expanded_button(self):
+ if self._expanded_button_index == -1:
+ return None
+ return self.toolbar.get_nth_item(self._expanded_button_index)
+
+ def set_expanded_button(self, button):
+ if not button in self.toolbar:
+ self._expanded_button_index = -1
+ return
+ self._expanded_button_index = self.toolbar.get_item_index(button)
+
+ expanded_button = property(get_expanded_button, set_expanded_button)
+
+ def get_padding(self):
+ return self._toolbar_alignment.props.left_padding
+
+ def set_padding(self, pad):
+ self._toolbar_alignment.set_padding(0, 0, pad, pad)
+
+ padding = gobject.property(type=object,
+ getter=get_padding, setter=set_padding)
+
+ def modify_bg(self, state, color):
+ if state == gtk.STATE_NORMAL:
+ self.background = color
+ self._toolbar_widget.modify_bg(state, color)
+ self.toolbar.modify_bg(state, color)
+
+ def __remove_cb(self, sender, button):
+ if not isinstance(button, ToolbarButton):
+ return
+ button.popdown()
+ if button == self.expanded_button:
+ self.remove(button.page_widget)
+ self._expanded_button_index = -1
+
+
+class _ToolbarPalette(PaletteWindow):
+
+ def __init__(self, **kwargs):
+ PaletteWindow.__init__(self, **kwargs)
+ self.set_border_width(0)
+ self._has_focus = False
+
+ group = palettegroup.get_group('default')
+ group.connect('popdown', self.__group_popdown_cb)
+ self.set_group_id('toolbarbox')
+
+ def get_expanded_button(self):
+ return self.invoker.parent
+
+ expanded_button = property(get_expanded_button)
+
+ def on_invoker_enter(self):
+ PaletteWindow.on_invoker_enter(self)
+ self._set_focus(True)
+
+ def on_invoker_leave(self):
+ PaletteWindow.on_invoker_leave(self)
+ self._set_focus(False)
+
+ def on_enter(self, event):
+ PaletteWindow.on_enter(self, event)
+ self._set_focus(True)
+
+ def on_leave(self, event):
+ PaletteWindow.on_enter(self, event)
+ self._set_focus(False)
+
+ def _set_focus(self, new_focus):
+ self._has_focus = new_focus
+ if not self._has_focus:
+ group = palettegroup.get_group('default')
+ if not group.is_up():
+ self.popdown()
+
+ def do_size_request(self, requisition):
+ gtk.Window.do_size_request(self, requisition)
+ requisition.width = max(requisition.width,
+ gtk.gdk.screen_width())
+
+ def popup(self, immediate=False):
+ button = self.expanded_button
+ if button.is_expanded():
+ return
+ box = button.toolbar_box
+ _setup_page(button.page_widget, style.COLOR_BLACK.get_gdk_color(),
+ box.props.padding)
+ PaletteWindow.popup(self, immediate)
+
+ def __group_popdown_cb(self, group):
+ if not self._has_focus:
+ self.popdown(immediate=True)
+
+
+class _Box(gtk.EventBox):
+
+ def __init__(self):
+ gtk.EventBox.__init__(self)
+ self.connect('expose-event', self.do_expose_event)
+ self.set_app_paintable(True)
+
+ def do_expose_event(self, widget, event):
+ if self.parent.expanded_button is None:
+ return
+ alloc = self.parent.expanded_button.allocation
+ self.get_style().paint_box(event.window,
+ gtk.STATE_NORMAL, gtk.SHADOW_IN, event.area, self,
+ 'palette-invoker', -_LINE_WIDTH, 0,
+ self.allocation.width + _LINE_WIDTH * 2,
+ self.allocation.height + _LINE_WIDTH)
+ self.get_style().paint_box(event.window,
+ gtk.STATE_NORMAL, gtk.SHADOW_NONE, event.area, self, None,
+ alloc.x + _LINE_WIDTH, 0,
+ alloc.width - _LINE_WIDTH * 2, _LINE_WIDTH)
+
+
+def _setup_page(page_widget, color, hpad):
+ vpad = _LINE_WIDTH
+ page_widget.child.set_padding(vpad, vpad, hpad, hpad)
+
+ page = _get_embedded_page(page_widget)
+ page.modify_bg(gtk.STATE_NORMAL, color)
+ if isinstance(page, gtk.Container):
+ for i in page.get_children():
+ i.modify_bg(gtk.STATE_INSENSITIVE, color)
+
+ page_widget.modify_bg(gtk.STATE_NORMAL, color)
+ page_widget.modify_bg(gtk.STATE_PRELIGHT, color)
+
+
+def _embed_page(box_class, page):
+ page.show()
+
+ alignment = gtk.Alignment(0.0, 0.0, 1.0, 1.0)
+ alignment.add(page)
+ alignment.show()
+
+ page_widget = box_class()
+ page_widget.modify_bg(gtk.STATE_ACTIVE,
+ style.COLOR_BUTTON_GREY.get_gdk_color())
+ page_widget.add(alignment)
+ page_widget.show()
+
+ return (page_widget, alignment)
+
+
+def _get_embedded_page(page_widget):
+ return page_widget.child.child
+
+
+def _paint_arrow(widget, event, arrow_type):
+ alloc = widget.allocation
+ x = alloc.x + alloc.width / 2 - _ARROW_SIZE / 2
+ y = alloc.y + alloc.height - int(_ARROW_SIZE * .85)
+
+ widget.get_style().paint_arrow(event.window,
+ gtk.STATE_NORMAL, gtk.SHADOW_NONE, event.area, widget,
+ None, arrow_type, True, x, y, _ARROW_SIZE, _ARROW_SIZE)
diff --git a/toolkit/toolitem.py b/toolkit/toolitem.py
new file mode 100644
index 0000000..e490c22
--- /dev/null
+++ b/toolkit/toolitem.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2009, Aleksey Lim
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""A set of toolitem widets"""
+
+import gtk
+import gobject
+
+from sugar.graphics import style
+
+from toolkit.combobox import ComboBox
+
+
+class ToolWidget(gtk.ToolItem):
+
+ def __init__(self, **kwargs):
+ self._widget = None
+ self._label = None
+ self._label_text = None
+ self._box = gtk.HBox(False, style.DEFAULT_SPACING)
+
+ gobject.GObject.__init__(self, **kwargs)
+ self.props.border_width = style.DEFAULT_PADDING
+
+ self._box.show()
+ self.add(self._box)
+
+ if self.label is None:
+ self.label = gtk.Label()
+
+ def get_label_text(self):
+ return self._label_text
+
+ def set_label_text(self, value):
+ self._label_text = value
+ if self.label is not None and value:
+ self.label.set_text(self._label_text)
+
+ label_text = gobject.property(getter=get_label_text, setter=set_label_text)
+
+ def get_label(self):
+ return self._label
+
+ def set_label(self, label):
+ if self._label is not None:
+ self._box.remove(self._label)
+ self._label = label
+ self._box.pack_start(label, False)
+ self._box.reorder_child(label, 0)
+ label.show()
+ self.set_label_text(self._label_text)
+
+ label = gobject.property(getter=get_label, setter=set_label)
+
+ def get_widget(self):
+ return self._widget
+
+ def set_widget(self, widget):
+ if self._widget is not None:
+ self._box.remove(self._widget)
+ self._widget = widget
+ self._box.pack_end(widget)
+ widget.show()
+
+ widget = gobject.property(getter=get_widget, setter=set_widget)