From dee3a3d2ba0f3495400cc5cdd608f0269d9622f1 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Fri, 01 Oct 2010 18:00:38 +0000 Subject: Bundle toolkit dependecy --- (limited to 'toolkit') 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 = '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 = '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 = '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 +# 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] = """ + + + + +""" + +IMAGE[1] = """ + + + + +""" + +IMAGE[2] = """ + + + + +""" + +IMAGE[3] = """ + + + + +""" + +IMAGE[4] = """ + + + + +""" + +IMAGE[5] = """ + + + + + +""" + +IMAGE[6] = """ + + + + +""" + +IMAGE[7] = """ + + + + +""" 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) -- cgit v0.9.1