diff options
Diffstat (limited to 'toolkit/src/sugar/graphics')
29 files changed, 6973 insertions, 0 deletions
diff --git a/toolkit/src/sugar/graphics/Makefile.am b/toolkit/src/sugar/graphics/Makefile.am new file mode 100644 index 0000000..7334288 --- /dev/null +++ b/toolkit/src/sugar/graphics/Makefile.am @@ -0,0 +1,30 @@ +sugardir = $(pythondir)/sugar/graphics +sugar_PYTHON = \ + alert.py \ + animator.py \ + canvastextview.py \ + colorbutton.py \ + combobox.py \ + entry.py \ + iconentry.py \ + icon.py \ + __init__.py \ + menuitem.py \ + notebook.py \ + objectchooser.py \ + palettegroup.py \ + palette.py \ + palettewindow.py \ + panel.py \ + radiopalette.py \ + radiotoolbutton.py \ + roundbox.py \ + style.py \ + toggletoolbutton.py \ + toolbarbox.py \ + toolbox.py \ + toolbutton.py \ + toolcombobox.py \ + tray.py \ + window.py \ + xocolor.py diff --git a/toolkit/src/sugar/graphics/__init__.py b/toolkit/src/sugar/graphics/__init__.py new file mode 100644 index 0000000..1e7e0f9 --- /dev/null +++ b/toolkit/src/sugar/graphics/__init__.py @@ -0,0 +1,18 @@ +"""Graphics/controls for use in Sugar""" + +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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/src/sugar/graphics/alert.py b/toolkit/src/sugar/graphics/alert.py new file mode 100644 index 0000000..a4dd017 --- /dev/null +++ b/toolkit/src/sugar/graphics/alert.py @@ -0,0 +1,479 @@ +""" +Alerts appear at the top of the body of your activity. + +At a high level, Alert and its different variations (TimeoutAlert, +ConfirmationAlert, etc.) have a title, an alert message and then several +buttons that the user can click. The Alert class will pass "response" events +to your activity when any of these buttons are clicked, along with a +response_id to help you identify what button was clicked. + + +Examples +-------- +create a simple alert message. + +.. code-block:: python + from sugar.graphics.alert import Alert + ... + # Create a new simple alert + alert = Alert() + # Populate the title and text body of the alert. + alert.props.title=_('Title of Alert Goes Here') + alert.props.msg = _('Text message of alert goes here') + # Call the add_alert() method (inherited via the sugar.graphics.Window + # superclass of Activity) to add this alert to the activity window. + self.add_alert(alert) + alert.show() + +STABLE. +""" +# Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2010, Anish Mangal <anishmangal2002@gmail.com> +# +# 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 gettext + +import gtk +import gobject +import hippo +import math + +from sugar.graphics import style +from sugar.graphics.icon import Icon + + +_ = lambda msg: gettext.dgettext('sugar-toolkit', msg) + + +class Alert(gtk.EventBox): + """ + UI interface for Alerts + + Alerts are used inside the activity window instead of being a + separate popup window. They do not hide canvas content. You can + use add_alert(widget) and remove_alert(widget) inside your activity + to add and remove the alert. The position of the alert is below the + toolbox or top in fullscreen mode. + + Properties: + 'title': the title of the alert, + 'message': the message of the alert, + 'icon': the icon that appears at the far left + + See __gproperties__ + + """ + + __gtype_name__ = 'SugarAlert' + + __gsignals__ = { + 'response': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([object])), + } + + __gproperties__ = { + 'title': (str, None, None, None, gobject.PARAM_READWRITE), + 'msg': (str, None, None, None, gobject.PARAM_READWRITE), + 'icon': (object, None, None, gobject.PARAM_WRITABLE), + } + + def __init__(self, **kwargs): + + self._title = None + self._msg = None + self._icon = None + self._buttons = {} + + self._hbox = gtk.HBox() + self._hbox.set_border_width(style.DEFAULT_SPACING) + self._hbox.set_spacing(style.DEFAULT_SPACING) + + self._msg_box = gtk.VBox() + self._title_label = gtk.Label() + self._title_label.set_alignment(0, 0.5) + self._msg_box.pack_start(self._title_label, False) + + self._msg_label = gtk.Label() + self._msg_label.set_alignment(0, 0.5) + self._msg_box.pack_start(self._msg_label, False) + self._hbox.pack_start(self._msg_box, False) + + self._buttons_box = gtk.HButtonBox() + self._buttons_box.set_layout(gtk.BUTTONBOX_END) + self._buttons_box.set_spacing(style.DEFAULT_SPACING) + self._hbox.pack_start(self._buttons_box) + + gobject.GObject.__init__(self, **kwargs) + + self.set_visible_window(True) + self.add(self._hbox) + self._title_label.show() + self._msg_label.show() + self._buttons_box.show() + self._msg_box.show() + self._hbox.show() + self.show() + + def do_set_property(self, pspec, value): + """ + Set alert property + + Parameters + ---------- + pspec : + + value : + + Returns + ------- + None + + """ + if pspec.name == 'title': + if self._title != value: + self._title = value + self._title_label.set_markup("<b>" + self._title + "</b>") + elif pspec.name == 'msg': + if self._msg != value: + self._msg = value + self._msg_label.set_markup(self._msg) + self._msg_label.set_line_wrap(True) + elif pspec.name == 'icon': + if self._icon != value: + self._icon = value + self._hbox.pack_start(self._icon, False) + self._hbox.reorder_child(self._icon, 0) + + def do_get_property(self, pspec): + """ + Get alert property + + Parameters + ---------- + pspec : + property for which the value will be returned + + Returns + ------- + value of the property specified + + """ + if pspec.name == 'title': + return self._title + elif pspec.name == 'msg': + return self._msg + + def add_button(self, response_id, label, icon=None, position=-1): + """ + Add a button to the alert + + Parameters + ---------- + response_id : + will be emitted with the response signal a response ID should one + of the pre-defined GTK Response Type Constants or a positive number + label : + that will occure right to the buttom + + icon : + this can be a SugarIcon or a gtk.Image + + postion : + the position of the button in the box (optional) + + Returns + ------- + button :gtk.Button + + """ + button = gtk.Button() + self._buttons[response_id] = button + if icon is not None: + button.set_image(icon) + button.set_label(label) + self._buttons_box.pack_start(button) + button.show() + button.connect('clicked', self.__button_clicked_cb, response_id) + if position != -1: + self._buttons_box.reorder_child(button, position) + return button + + def remove_button(self, response_id): + """ + Remove a button from the alert by the given response id + + Parameters + ---------- + response_id : + + Returns + ------- + None + + """ + self._buttons_box.remove(self._buttons[response_id]) + + def _response(self, response_id): + """Emitting response when we have a result + + A result can be that a user has clicked a button or + a timeout has occured, the id identifies the button + that has been clicked and -1 for a timeout + """ + self.emit('response', response_id) + + def __button_clicked_cb(self, button, response_id): + self._response(response_id) + + +class ConfirmationAlert(Alert): + """ + This is a ready-made two button (Cancel,Ok) alert. + + A confirmation alert is a nice shortcut from a standard Alert because it + comes with 'OK' and 'Cancel' buttons already built-in. When clicked, the + 'OK' button will emit a response with a response_id of gtk.RESPONSE_OK, + while the 'Cancel' button will emit gtk.RESPONSE_CANCEL. + + Examples + -------- + + .. code-block:: python + from sugar.graphics.alert import ConfirmationAlert + ... + #### Method: _alert_confirmation, create a Confirmation alert (with ok + and cancel buttons standard) + # and add it to the UI. + def _alert_confirmation(self): + alert = ConfirmationAlert() + alert.props.title=_('Title of Alert Goes Here') + alert.props.msg = _('Text message of alert goes here') + alert.connect('response', self._alert_response_cb) + self.add_alert(alert) + + + #### Method: _alert_response_cb, called when an alert object throws a + response event. + def _alert_response_cb(self, alert, response_id): + #remove the alert from the screen, since either a response button + #was clicked or there was a timeout + self.remove_alert(alert) + + #Do any work that is specific to the type of button clicked. + if response_id is gtk.RESPONSE_OK: + print 'Ok Button was clicked. Do any work upon ok here ...' + elif response_id is gtk.RESPONSE_CANCEL: + print 'Cancel Button was clicked.' + + """ + + def __init__(self, **kwargs): + Alert.__init__(self, **kwargs) + + icon = Icon(icon_name='dialog-cancel') + self.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), icon) + icon.show() + + icon = Icon(icon_name='dialog-ok') + self.add_button(gtk.RESPONSE_OK, _('Ok'), icon) + icon.show() + +class ErrorAlert(Alert): + """ + This is a ready-made one button (Ok) alert. + + An error alert is a nice shortcut from a standard Alert because it + comes with the 'OK' button already built-in. When clicked, the + 'OK' button will emit a response with a response_id of gtk.RESPONSE_OK. + + Examples + -------- + + .. code-block:: python + from sugar.graphics.alert import ErrorAlert + ... + #### Method: _alert_error, create a Error alert (with ok + button standard) + # and add it to the UI. + def _alert_error(self): + alert = ErrorAlert() + alert.props.title=_('Title of Alert Goes Here') + alert.props.msg = _('Text message of alert goes here') + alert.connect('response', self._alert_response_cb) + self.add_alert(alert) + + + #### Method: _alert_response_cb, called when an alert object throws a + response event. + def _alert_response_cb(self, alert, response_id): + #remove the alert from the screen, since either a response button + #was clicked or there was a timeout + self.remove_alert(alert) + + #Do any work that is specific to the response_id. + if response_id is gtk.RESPONSE_OK: + print 'Ok Button was clicked. Do any work upon ok here ...' + + """ + + def __init__(self, **kwargs): + Alert.__init__(self, **kwargs) + + icon = Icon(icon_name='dialog-ok') + self.add_button(gtk.RESPONSE_OK, _('Ok'), icon) + icon.show() + +class _TimeoutIcon(hippo.CanvasText, hippo.CanvasItem): + """An icon with a round border""" + __gtype_name__ = 'AlertTimeoutIcon' + + def __init__(self, **kwargs): + hippo.CanvasText.__init__(self, **kwargs) + + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + self.props.border_left = style.DEFAULT_SPACING + self.props.border_right = style.DEFAULT_SPACING + + def do_paint_background(self, cr, damaged_box): + [width, height] = self.get_allocation() + + xval = width * 0.5 + yval = height * 0.5 + radius = min(width * 0.5, height * 0.5) + + hippo.cairo_set_source_rgba32(cr, self.props.background_color) + cr.arc(xval, yval, radius, 0, 2*math.pi) + cr.fill_preserve() + + +class TimeoutAlert(Alert): + """ + This is a ready-made two button (Cancel,Continue) alert + + It times out with a positive response after the given amount of seconds. + + + Examples + -------- + + .. code-block:: python + from sugar.graphics.alert import TimeoutAlert + ... + #### Method: _alert_timeout, create a Timeout alert (with ok and cancel + buttons standard) + # and add it to the UI. + def _alert_timeout(self): + #Notice that for a TimeoutAlert, you pass the number of seconds in + #which to timeout. By default, this is 5. + alert = TimeoutAlert(10) + alert.props.title=_('Title of Alert Goes Here') + alert.props.msg = _('Text message of timeout alert goes here') + alert.connect('response', self._alert_response_cb) + self.add_alert(alert) + + #### Method: _alert_response_cb, called when an alert object throws a + response event. + def _alert_response_cb(self, alert, response_id): + #remove the alert from the screen, since either a response button + #was clicked or there was a timeout + self.remove_alert(alert) + + #Do any work that is specific to the type of button clicked. + if response_id is gtk.RESPONSE_OK: + print 'Ok Button was clicked. Do any work upon ok here ...' + elif response_id is gtk.RESPONSE_CANCEL: + print 'Cancel Button was clicked.' + elif response_id == -1: + print 'Timout occurred' + + """ + + def __init__(self, timeout=5, **kwargs): + Alert.__init__(self, **kwargs) + + self._timeout = timeout + + icon = Icon(icon_name='dialog-cancel') + self.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), icon) + icon.show() + + self._timeout_text = _TimeoutIcon( + text=self._timeout, + color=style.COLOR_BUTTON_GREY.get_int(), + background_color=style.COLOR_WHITE.get_int()) + canvas = hippo.Canvas() + canvas.set_root(self._timeout_text) + canvas.show() + self.add_button(gtk.RESPONSE_OK, _('Continue'), canvas) + + gobject.timeout_add_seconds(1, self.__timeout) + + def __timeout(self): + self._timeout -= 1 + self._timeout_text.props.text = self._timeout + if self._timeout == 0: + self._response(gtk.RESPONSE_OK) + return False + return True + + +class NotifyAlert(Alert): + """ + Timeout alert with only an "OK" button - just for notifications + + Examples + -------- + + .. code-block:: python + from sugar.graphics.alert import NotifyAlert + ... + #### Method: _alert_notify, create a Notify alert (with only an 'OK' + button) + # and add it to the UI. + def _alert_notify(self): + #Notice that for a NotifyAlert, you pass the number of seconds in + #which to notify. By default, this is 5. + alert = NotifyAlert(10) + alert.props.title=_('Title of Alert Goes Here') + alert.props.msg = _('Text message of notify alert goes here') + alert.connect('response', self._alert_response_cb) + self.add_alert(alert) + + """ + + def __init__(self, timeout=5, **kwargs): + Alert.__init__(self, **kwargs) + + self._timeout = timeout + + self._timeout_text = _TimeoutIcon( + text=self._timeout, + color=style.COLOR_BUTTON_GREY.get_int(), + background_color=style.COLOR_WHITE.get_int()) + canvas = hippo.Canvas() + canvas.set_root(self._timeout_text) + canvas.show() + self.add_button(gtk.RESPONSE_OK, _('Ok'), canvas) + + gobject.timeout_add(1000, self.__timeout) + + def __timeout(self): + self._timeout -= 1 + self._timeout_text.props.text = self._timeout + if self._timeout == 0: + self._response(gtk.RESPONSE_OK) + return False + return True diff --git a/toolkit/src/sugar/graphics/animator.py b/toolkit/src/sugar/graphics/animator.py new file mode 100644 index 0000000..8fb298b --- /dev/null +++ b/toolkit/src/sugar/graphics/animator.py @@ -0,0 +1,151 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 time + +import gobject + +EASE_OUT_EXPO = 0 +EASE_IN_EXPO = 1 + + +class Animator(gobject.GObject): + + __gsignals__ = { + 'completed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, duration, fps=20, easing=EASE_OUT_EXPO): + gobject.GObject.__init__(self) + self._animations = [] + self._duration = duration + self._interval = 1.0 / fps + self._easing = easing + self._timeout_sid = 0 + self._start_time = None + + def add(self, animation): + """ + Parameter + --------- + animation : + + """ + self._animations.append(animation) + + def remove_all(self): + """ + Parameters + ---------- + None : + + Returns + ------- + None : + + """ + self.stop() + self._animations = [] + + def start(self): + """ + Parameters + ---------- + None : + + Returns + ------- + None + + """ + if self._timeout_sid: + self.stop() + + self._start_time = time.time() + self._timeout_sid = gobject.timeout_add( + int(self._interval * 1000), self._next_frame_cb) + + def stop(self): + """ + Parameters + ---------- + None : + + Returns + ------- + None : + + """ + if self._timeout_sid: + gobject.source_remove(self._timeout_sid) + self._timeout_sid = 0 + self.emit('completed') + + def _next_frame_cb(self): + current_time = min(self._duration, time.time() - self._start_time) + current_time = max(current_time, 0.0) + + for animation in self._animations: + animation.do_frame(current_time, self._duration, self._easing) + + if current_time == self._duration: + self.stop() + return False + else: + return True + + +class Animation(object): + + def __init__(self, start, end): + self.start = start + self.end = end + + def do_frame(self, t, duration, easing): + """ + Parameters + ---------- + t: + + duration: + + easing: + + Returns + None: + + """ + start = self.start + change = self.end - self.start + + if t == duration: + # last frame + frame = self.end + else: + if easing == EASE_OUT_EXPO: + frame = change * (-pow(2, -10 * t / duration) + 1) + start + elif easing == EASE_IN_EXPO: + frame = change * pow(2, 10 * (t / duration - 1)) + start + + self.next_frame(frame) + + def next_frame(self, frame): + pass diff --git a/toolkit/src/sugar/graphics/canvastextview.py b/toolkit/src/sugar/graphics/canvastextview.py new file mode 100644 index 0000000..853af9f --- /dev/null +++ b/toolkit/src/sugar/graphics/canvastextview.py @@ -0,0 +1,41 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# 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 hippo + +from sugar.graphics import style + + +class CanvasTextView(hippo.CanvasWidget): + + def __init__(self, text, **kwargs): + hippo.CanvasWidget.__init__(self, **kwargs) + self.text_view_widget = gtk.TextView() + self.text_view_widget.props.buffer.props.text = text + self.text_view_widget.props.left_margin = style.DEFAULT_SPACING + self.text_view_widget.props.right_margin = style.DEFAULT_SPACING + self.text_view_widget.props.wrap_mode = gtk.WRAP_WORD + self.text_view_widget.show() + + # TODO: These fields should expand vertically instead of scrolling + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_shadow_type(gtk.SHADOW_OUT) + scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scrolled_window.add(self.text_view_widget) + + self.props.widget = scrolled_window diff --git a/toolkit/src/sugar/graphics/colorbutton.py b/toolkit/src/sugar/graphics/colorbutton.py new file mode 100644 index 0000000..1fed96d --- /dev/null +++ b/toolkit/src/sugar/graphics/colorbutton.py @@ -0,0 +1,536 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2008, Benjamin Berg <benjamin@sipsolutions.net> +# +# 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 gettext +import gtk +import gobject +import struct +import logging + +from sugar.graphics import style +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, ToolInvoker, WidgetInvoker + + +_ = lambda msg: gettext.dgettext('sugar-toolkit', msg) + + +def get_svg_color_string(color): + return '#%.2X%.2X%.2X' % (color.red / 257, color.green / 257, + color.blue / 257) + + +class _ColorButton(gtk.Button): + """This is a ColorButton for Sugar. It is similar to the gtk.ColorButton, + but does not have any alpha support. + Instead of a color selector dialog it will pop up a Sugar palette. + + As a preview an sugar.graphics.Icon is used. The fill color will be set to + the current color, and the stroke color is set to the font color. + """ + + __gtype_name__ = 'SugarColorButton' + __gsignals__ = {'color-set': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + tuple())} + + def __init__(self, **kwargs): + self._title = _('Choose a color') + self._color = gtk.gdk.Color(0, 0, 0) + self._has_palette = True + self._has_invoker = True + self._palette = None + self._accept_drag = True + + self._preview = Icon(icon_name='color-preview', + icon_size=gtk.ICON_SIZE_BUTTON) + + gobject.GObject.__init__(self, **kwargs) + + if self._accept_drag: + self.drag_dest_set(gtk.DEST_DEFAULT_MOTION | + gtk.DEST_DEFAULT_HIGHLIGHT | + gtk.DEST_DEFAULT_DROP, + [('application/x-color', 0, 0)], + gtk.gdk.ACTION_COPY) + self.drag_source_set(gtk.gdk.BUTTON1_MASK | gtk.gdk.BUTTON3_MASK, + [('application/x-color', 0, 0)], + gtk.gdk.ACTION_COPY) + self.connect('drag_data_received', self.__drag_data_received_cb) + self.connect('drag_data_get', self.__drag_data_get_cb) + + self._preview.fill_color = get_svg_color_string(self._color) + self._preview.stroke_color = \ + get_svg_color_string(self.style.fg[gtk.STATE_NORMAL]) + self.set_image(self._preview) + + if self._has_palette and self._has_invoker: + self._invoker = WidgetInvoker(self) + # FIXME: This is a hack. + self._invoker.has_rectangle_gap = lambda: False + self._invoker.palette = self._palette + + def create_palette(self): + if self._has_palette: + self._palette = _ColorPalette(color=self._color, + primary_text=self._title) + self._palette.connect('color-set', self.__palette_color_set_cb) + self._palette.connect('notify::color', self. + __palette_color_changed) + + return self._palette + + def __palette_color_set_cb(self, palette): + self.emit('color-set') + + def __palette_color_changed(self, palette, pspec): + self.color = self._palette.color + + def do_style_set(self, previous_style): + self._preview.stroke_color = \ + get_svg_color_string(self.style.fg[gtk.STATE_NORMAL]) + + def do_clicked(self): + if self._palette: + if not self._palette.is_up(): + self._palette.popup(immediate=True, + state=self._palette.SECONDARY) + else: + self._palette.popdown(immediate=True) + return True + + def set_color(self, color): + assert isinstance(color, gtk.gdk.Color) + + if self._color.red == color.red and \ + self._color.green == color.green and \ + self._color.blue == color.blue: + return + + self._color = gtk.gdk.Color(color.red, color.green, color.blue) + self._preview.fill_color = get_svg_color_string(self._color) + if self._palette: + self._palette.props.color = self._color + self.notify('color') + + def get_color(self): + return self._color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def set_icon_name(self, icon_name): + self._preview.props.icon_name = icon_name + + def get_icon_name(self): + return self._preview.props.icon_name + + icon_name = gobject.property(type=str, + getter=get_icon_name, setter=set_icon_name) + + def set_icon_size(self, icon_size): + self._preview.props.icon_size = icon_size + + def get_icon_size(self): + return self._preview.props.icon_size + + icon_size = gobject.property(type=int, + getter=get_icon_size, setter=set_icon_size) + + def set_title(self, title): + self._title = title + if self._palette: + self._palette.primary_text = self._title + + def get_title(self): + return self._title + + title = gobject.property(type=str, getter=get_title, setter=set_title) + + def _set_has_invoker(self, has_invoker): + self._has_invoker = has_invoker + + def _get_has_invoker(self): + return self._has_invoker + + has_invoker = gobject.property(type=bool, default=True, + flags=gobject.PARAM_READWRITE | + gobject.PARAM_CONSTRUCT_ONLY, + getter=_get_has_invoker, + setter=_set_has_invoker) + + def _set_has_palette(self, has_palette): + self._has_palette = has_palette + + def _get_has_palette(self): + return self._has_palette + + has_palette = gobject.property(type=bool, default=True, + flags=gobject.PARAM_READWRITE | + gobject.PARAM_CONSTRUCT_ONLY, + getter=_get_has_palette, + setter=_set_has_palette) + + def _set_accept_drag(self, accept_drag): + self._accept_drag = accept_drag + + def _get_accept_drag(self): + return self._accept_drag + + accept_drag = gobject.property(type=bool, default=True, + flags=gobject.PARAM_READWRITE | + gobject.PARAM_CONSTRUCT_ONLY, + getter=_get_accept_drag, + setter=_set_accept_drag) + + def __drag_begin_cb(self, widget, context): + # Drag and Drop + pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, + style.SMALL_ICON_SIZE, + style.SMALL_ICON_SIZE) + + red = self._color.red / 257 + green = self._color.green / 257 + blue = self._color.blue / 257 + + pixbuf.fill(red << 24 + green << 16 + blue << 8 + 0xff) + + context.set_icon_pixbuf(pixbuf) + + def __drag_data_get_cb(self, widget, context, selection_data, info, time): + data = struct.pack('=HHHH', self._color.red, self._color.green, + self._color.blue, 65535) + selection_data.set(selection_data.target, 16, data) + + def __drag_data_received_cb(self, widget, context, x, y, selection_data, \ + info, time): + if len(selection_data.data) != 8: + return + + dropped = selection_data.data + red = struct.unpack_from('=H', dropped, 0)[0] + green = struct.unpack_from('=H', dropped, 2)[0] + blue = struct.unpack_from('=H', dropped, 4)[0] + # dropped[6] and dropped[7] is alpha, but we ignore the alpha channel + + color = gtk.gdk.Color(red, green, blue) + self.set_color(color) + + +class _ColorPalette(Palette): + """This is a color picker palette. It will usually be used indirectly + trough a sugar.graphics.ColorButton. + """ + _RED = 0 + _GREEN = 1 + _BLUE = 2 + + __gtype_name__ = 'SugarColorPalette' + + # The color-set signal is emitted when the user is finished selecting + # a color. + __gsignals__ = {'color-set': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + tuple())} + + def __init__(self, **kwargs): + self._color = gtk.gdk.Color(0, 0, 0) + self._previous_color = self._color.copy() + self._scales = None + + Palette.__init__(self, **kwargs) + + self.connect('popup', self.__popup_cb) + self.connect('popdown', self.__popdown_cb) + + self._picker_hbox = gtk.HBox() + self.set_content(self._picker_hbox) + + self._swatch_tray = gtk.Table() + + self._picker_hbox.pack_start(self._swatch_tray) + self._picker_hbox.pack_start(gtk.VSeparator(), + padding=style.DEFAULT_SPACING) + + self._chooser_table = gtk.Table(3, 2) + self._chooser_table.set_col_spacing(0, style.DEFAULT_PADDING) + + self._scales = [] + self._scales.append( + self._create_color_scale(_('Red'), self._RED, 0)) + self._scales.append( + self._create_color_scale(_('Green'), self._GREEN, 1)) + self._scales.append( + self._create_color_scale(_('Blue'), self._BLUE, 2)) + + self._picker_hbox.add(self._chooser_table) + + self._picker_hbox.show_all() + + self._build_swatches() + + def _create_color_scale(self, text, color, row): + label = gtk.Label(text) + label.props.xalign = 1.0 + scale = gtk.HScale() + scale.set_size_request(style.zoom(250), -1) + scale.set_draw_value(False) + scale.set_range(0, 1.0) + scale.set_increments(0.1, 0.2) + + if color == self._RED: + scale.set_value(self._color.red / 65535.0) + elif color == self._GREEN: + scale.set_value(self._color.green / 65535.0) + elif color == self._BLUE: + scale.set_value(self._color.blue / 65535.0) + + scale.connect('value-changed', + self.__scale_value_changed_cb, + color) + self._chooser_table.attach(label, 0, 1, row, row + 1) + self._chooser_table.attach(scale, 1, 2, row, row + 1) + + return scale + + def _build_swatches(self): + for child in self._swatch_tray.get_children(): + child.destroy() + + # Use a hardcoded list of colors for now. + colors = ['#ed2529', '#69bc47', '#3c54a3', + '#f57f25', '#0b6b3a', '#00a0c6', + '#f6eb1a', '#b93f94', '#5b4a9c', + '#000000', '#919496', '#ffffff'] + + # We want 3 rows of colors. + rows = 3 + i = 0 + self._swatch_tray.props.n_rows = rows + self._swatch_tray.props.n_columns = (len(colors) + rows - 1) / rows + for color in colors: + button = _ColorButton(has_palette=False, + color=gtk.gdk.color_parse(color), + accept_drag=False, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + button.set_relief(gtk.RELIEF_NONE) + self._swatch_tray.attach(button, + i % rows, i % rows + 1, + i / rows, i / rows + 1, + yoptions=0, xoptions=0) + button.connect('clicked', self.__swatch_button_clicked_cb) + i += 1 + + self._swatch_tray.show_all() + + def __popup_cb(self, palette): + self._previous_color = self._color.copy() + + def __popdown_cb(self, palette): + self.emit('color-set') + + def __scale_value_changed_cb(self, widget, color): + new_color = self._color.copy() + if color == self._RED: + new_color.red = int(65535 * widget.get_value()) + elif color == self._GREEN: + new_color.green = int(65535 * widget.get_value()) + elif color == self._BLUE: + new_color.blue = int(65535 * widget.get_value()) + self.color = new_color + + def do_key_press_event(self, event): + if event.keyval == gtk.keysyms.Escape: + self.props.color = self._previous_color + self.popdown(immediate=True) + return True + elif event.keyval == gtk.keysyms.Return: + self.popdown(immediate=True) + return True + return False + + def __swatch_button_clicked_cb(self, button): + self.props.color = button.get_color() + + def set_color(self, color): + assert isinstance(color, gtk.gdk.Color) + + if self._color.red == color.red and \ + self._color.green == color.green and \ + self._color.blue == color.blue: + return + + self._color = color.copy() + + if self._scales: + self._scales[self._RED].set_value(self._color.red / 65535.0) + self._scales[self._GREEN].set_value(self._color.green / 65535.0) + self._scales[self._BLUE].set_value(self._color.blue / 65535.0) + + self.notify('color') + + def get_color(self): + return self._color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + +def _add_accelerator(tool_button): + if not tool_button.props.accelerator or not tool_button.get_toplevel() or \ + not tool_button.child: + return + + # TODO: should we remove the accelerator from the prev top level? + + accel_group = tool_button.get_toplevel().get_data('sugar-accel-group') + if not accel_group: + logging.warning('No gtk.AccelGroup in the top level window.') + return + + keyval, mask = gtk.accelerator_parse(tool_button.props.accelerator) + # the accelerator needs to be set at the child, so the gtk.AccelLabel + # in the palette can pick it up. + tool_button.child.add_accelerator('clicked', accel_group, keyval, mask, + gtk.ACCEL_LOCKED | gtk.ACCEL_VISIBLE) + + +def _hierarchy_changed_cb(tool_button, previous_toplevel): + _add_accelerator(tool_button) + + +def setup_accelerator(tool_button): + _add_accelerator(tool_button) + tool_button.connect('hierarchy-changed', _hierarchy_changed_cb) + + +class ColorToolButton(gtk.ToolItem): + # This not ideal. It would be better to subclass gtk.ToolButton, however + # the python bindings do not seem to be powerfull enough for that. + # (As we need to change a variable in the class structure.) + + __gtype_name__ = 'SugarColorToolButton' + __gsignals__ = {'color-set': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + tuple())} + + def __init__(self, icon_name='color-preview', **kwargs): + self._accelerator = None + self._tooltip = None + self._palette_invoker = ToolInvoker() + self._palette = None + + gobject.GObject.__init__(self, **kwargs) + + # The gtk.ToolButton has already added a normal button. + # Replace it with a ColorButton + color_button = _ColorButton(icon_name=icon_name, has_invoker=False) + self.add(color_button) + + # The following is so that the behaviour on the toolbar is correct. + color_button.set_relief(gtk.RELIEF_NONE) + color_button.icon_size = gtk.ICON_SIZE_LARGE_TOOLBAR + + self._palette_invoker.attach_tool(self) + + # This widget just proxies the following properties to the colorbutton + color_button.connect('notify::color', self.__notify_change) + color_button.connect('notify::icon-name', self.__notify_change) + color_button.connect('notify::icon-size', self.__notify_change) + color_button.connect('notify::title', self.__notify_change) + color_button.connect('color-set', self.__color_set_cb) + color_button.connect('can-activate-accel', + self.__button_can_activate_accel_cb) + + def __button_can_activate_accel_cb(self, button, signal_id): + # Accept activation via accelerators regardless of this widget's state + return True + + def set_accelerator(self, accelerator): + self._accelerator = accelerator + setup_accelerator(self) + + def get_accelerator(self): + return self._accelerator + + accelerator = gobject.property(type=str, setter=set_accelerator, + getter=get_accelerator) + + def create_palette(self): + self._palette = self.get_child().create_palette() + return self._palette + + def get_palette_invoker(self): + return self._palette_invoker + + def set_palette_invoker(self, palette_invoker): + self._palette_invoker.detach() + self._palette_invoker = palette_invoker + + palette_invoker = gobject.property( + type=object, setter=set_palette_invoker, getter=get_palette_invoker) + + def set_color(self, color): + self.get_child().props.color = color + + def get_color(self): + return self.get_child().props.color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def set_icon_name(self, icon_name): + self.get_child().props.icon_name = icon_name + + def get_icon_name(self): + return self.get_child().props.icon_name + + icon_name = gobject.property(type=str, + getter=get_icon_name, setter=set_icon_name) + + def set_icon_size(self, icon_size): + self.get_child().props.icon_size = icon_size + + def get_icon_size(self): + return self.get_child().props.icon_size + + icon_size = gobject.property(type=int, + getter=get_icon_size, setter=set_icon_size) + + def set_title(self, title): + self.get_child().props.title = title + + def get_title(self): + return self.get_child().props.title + + title = gobject.property(type=str, getter=get_title, setter=set_title) + + def do_expose_event(self, event): + child = self.get_child() + allocation = self.get_allocation() + if self._palette and self._palette.is_up(): + invoker = self._palette.props.invoker + invoker.draw_rectangle(event, self._palette) + elif child.state == gtk.STATE_PRELIGHT: + child.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_NONE, event.area, + child, 'toolbutton-prelight', + allocation.x, allocation.y, + allocation.width, allocation.height) + + gtk.ToolButton.do_expose_event(self, event) + + def __notify_change(self, widget, pspec): + self.notify(pspec.name) + + def __color_set_cb(self, widget): + self.emit('color-set') diff --git a/toolkit/src/sugar/graphics/combobox.py b/toolkit/src/sugar/graphics/combobox.py new file mode 100644 index 0000000..bc759c2 --- /dev/null +++ b/toolkit/src/sugar/graphics/combobox.py @@ -0,0 +1,170 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# 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): + + __gtype_name__ = 'SugarComboBox' + + def __init__(self): + gtk.ComboBox.__init__(self) + + self._text_renderer = None + self._icon_renderer = None + + self._model = gtk.ListStore(gobject.TYPE_PYOBJECT, + gobject.TYPE_STRING, + gtk.gdk.Pixbuf, + gobject.TYPE_BOOLEAN) + self.set_model(self._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 + + """ + 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 + + self._model.append([action_id, text, pixbuf, False]) + + def append_separator(self): + """ + Parameters + ---------- + None + + Returns + ------- + None + + """ + self._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._model.iter_nth_child(None, index) + if not row: + return None + return self._model[row] + + def remove_all(self): + """ + Parameters + ---------- + None + + Returns + ------- + None + + """ + self._model.clear() + + def _is_separator(self, model, row): + return model[row][3] diff --git a/toolkit/src/sugar/graphics/entry.py b/toolkit/src/sugar/graphics/entry.py new file mode 100644 index 0000000..6afafa0 --- /dev/null +++ b/toolkit/src/sugar/graphics/entry.py @@ -0,0 +1,41 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 gtk +import hippo + + +class CanvasEntry(hippo.CanvasEntry): + + def set_background(self, color_spec): + """ + Parameters + ---------- + color_spec : + + Returns + ------- + None + + """ + color = gtk.gdk.color_parse(color_spec) + self.props.widget.modify_bg(gtk.STATE_INSENSITIVE, color) + self.props.widget.modify_base(gtk.STATE_INSENSITIVE, color) diff --git a/toolkit/src/sugar/graphics/icon.py b/toolkit/src/sugar/graphics/icon.py new file mode 100644 index 0000000..4a94479 --- /dev/null +++ b/toolkit/src/sugar/graphics/icon.py @@ -0,0 +1,1176 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 small fixed size picture, typically used to decorate components. + +STABLE. +""" + +import re +import math +import logging + +import gobject +import gtk +import hippo +import cairo + +from sugar.graphics.xocolor import XoColor +from sugar.util import LRU + + +_BADGE_SIZE = 0.45 + + +class _SVGLoader(object): + + def __init__(self): + self._cache = LRU(50) + + def load(self, file_name, entities, cache): + if file_name in self._cache: + icon = self._cache[file_name] + else: + icon_file = open(file_name, 'r') + icon = icon_file.read() + icon_file.close() + + if cache: + self._cache[file_name] = icon + + for entity, value in entities.items(): + if isinstance(value, basestring): + xml = '<!ENTITY %s "%s">' % (entity, value) + icon = re.sub('<!ENTITY %s .*>' % entity, xml, icon) + else: + logging.error( + 'Icon %s, entity %s is invalid.', file_name, entity) + + import rsvg # XXX this is very slow! why? + return rsvg.Handle(data=icon) + + +class _IconInfo(object): + + def __init__(self): + self.file_name = None + self.attach_x = 0 + self.attach_y = 0 + + +class _BadgeInfo(object): + + def __init__(self): + self.attach_x = 0 + self.attach_y = 0 + self.size = 0 + self.icon_padding = 0 + + +class _IconBuffer(object): + + _surface_cache = LRU(50) + _loader = _SVGLoader() + + def __init__(self): + self.icon_name = None + self.icon_size = None + self.file_name = None + self.fill_color = None + self.background_color = None + self.stroke_color = None + self.badge_name = None + self.width = None + self.height = None + self.cache = False + self.scale = 1.0 + + def _get_cache_key(self, sensitive): + if self.background_color is None: + color = None + else: + color = (self.background_color.red, self.background_color.green, + self.background_color.blue) + return (self.icon_name, self.file_name, self.fill_color, + self.stroke_color, self.badge_name, self.width, self.height, + color, sensitive) + + def _load_svg(self, file_name): + entities = {} + if self.fill_color: + entities['fill_color'] = self.fill_color + if self.stroke_color: + entities['stroke_color'] = self.stroke_color + + return self._loader.load(file_name, entities, self.cache) + + def _get_attach_points(self, info, size_request): + attach_points = info.get_attach_points() + + if attach_points: + attach_x = float(attach_points[0][0]) / size_request + attach_y = float(attach_points[0][1]) / size_request + else: + attach_x = attach_y = 0 + + return attach_x, attach_y + + def _get_icon_info(self): + icon_info = _IconInfo() + + if self.file_name: + icon_info.file_name = self.file_name + elif self.icon_name: + theme = gtk.icon_theme_get_default() + + size = 50 + if self.width != None: + size = self.width + + info = theme.lookup_icon(self.icon_name, size, 0) + if info: + attach_x, attach_y = self._get_attach_points(info, size) + + icon_info.file_name = info.get_filename() + icon_info.attach_x = attach_x + icon_info.attach_y = attach_y + + del info + else: + logging.warning('No icon with the name %s was found in the ' + 'theme.', self.icon_name) + + return icon_info + + def _draw_badge(self, context, size, sensitive, widget): + theme = gtk.icon_theme_get_default() + badge_info = theme.lookup_icon(self.badge_name, size, 0) + if badge_info: + badge_file_name = badge_info.get_filename() + if badge_file_name.endswith('.svg'): + handle = self._loader.load(badge_file_name, {}, self.cache) + + dimensions = handle.get_dimension_data() + icon_width = int(dimensions[0]) + icon_height = int(dimensions[1]) + + pixbuf = handle.get_pixbuf() + else: + pixbuf = gtk.gdk.pixbuf_new_from_file(badge_file_name) + + icon_width = pixbuf.get_width() + icon_height = pixbuf.get_height() + + context.scale(float(size) / icon_width, + float(size) / icon_height) + + if not sensitive: + pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) + surface = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) + context.set_source_surface(surface, 0, 0) + context.paint() + + def _get_size(self, icon_width, icon_height, padding): + if self.width is not None and self.height is not None: + width = self.width + padding + height = self.height + padding + else: + width = icon_width + padding + height = icon_height + padding + + return width, height + + def _get_badge_info(self, icon_info, icon_width, icon_height): + info = _BadgeInfo() + if self.badge_name is None: + return info + + info.size = int(_BADGE_SIZE * icon_width) + info.attach_x = int(icon_info.attach_x * icon_width - info.size / 2) + info.attach_y = int(icon_info.attach_y * icon_height - info.size / 2) + + if info.attach_x < 0 or info.attach_y < 0: + info.icon_padding = max(-info.attach_x, -info.attach_y) + elif info.attach_x + info.size > icon_width or \ + info.attach_y + info.size > icon_height: + x_padding = info.attach_x + info.size - icon_width + y_padding = info.attach_y + info.size - icon_height + info.icon_padding = max(x_padding, y_padding) + + return info + + def _get_xo_color(self): + if self.stroke_color and self.fill_color: + return XoColor('%s,%s' % (self.stroke_color, self.fill_color)) + else: + return None + + def _set_xo_color(self, xo_color): + if xo_color: + self.stroke_color = xo_color.get_stroke_color() + self.fill_color = xo_color.get_fill_color() + else: + self.stroke_color = None + self.fill_color = None + + def _get_insensitive_pixbuf(self, pixbuf, widget): + if not (widget and widget.style): + return pixbuf + + icon_source = gtk.IconSource() + # Special size meaning "don't touch" + icon_source.set_size(-1) + icon_source.set_pixbuf(pixbuf) + icon_source.set_state(gtk.STATE_INSENSITIVE) + icon_source.set_direction_wildcarded(False) + icon_source.set_size_wildcarded(False) + + # Please note that the pixbuf returned by this function is leaked + # with current stable versions of pygtk. The relevant bug is + # http://bugzilla.gnome.org/show_bug.cgi?id=502871 + # -- 2007-12-14 Benjamin Berg + pixbuf = widget.style.render_icon(icon_source, widget.get_direction(), + gtk.STATE_INSENSITIVE, -1, widget, + "sugar-icon") + + return pixbuf + + def get_surface(self, sensitive=True, widget=None): + cache_key = self._get_cache_key(sensitive) + if cache_key in self._surface_cache: + return self._surface_cache[cache_key] + + icon_info = self._get_icon_info() + if icon_info.file_name is None: + return None + + is_svg = icon_info.file_name.endswith('.svg') + + if is_svg: + handle = self._load_svg(icon_info.file_name) + dimensions = handle.get_dimension_data() + icon_width = int(dimensions[0]) + icon_height = int(dimensions[1]) + else: + pixbuf = gtk.gdk.pixbuf_new_from_file(icon_info.file_name) + icon_width = pixbuf.get_width() + icon_height = pixbuf.get_height() + + badge_info = self._get_badge_info(icon_info, icon_width, icon_height) + + padding = badge_info.icon_padding + width, height = self._get_size(icon_width, icon_height, padding) + if self.background_color is None: + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + context = cairo.Context(surface) + else: + surface = cairo.ImageSurface(cairo.FORMAT_RGB24, width, height) + context = cairo.Context(surface) + context = gtk.gdk.CairoContext(context) + context.set_source_color(self.background_color) + context.paint() + + context.scale(float(width) / (icon_width + padding * 2), + float(height) / (icon_height + padding * 2)) + context.save() + + context.translate(padding, padding) + if is_svg: + if sensitive: + handle.render_cairo(context) + else: + pixbuf = handle.get_pixbuf() + pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) + + pixbuf_surface = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) + context.set_source_surface(pixbuf_surface, 0, 0) + context.paint() + else: + if not sensitive: + pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) + pixbuf_surface = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) + context.set_source_surface(pixbuf_surface, 0, 0) + context.paint() + + if self.badge_name: + context.restore() + context.translate(badge_info.attach_x, badge_info.attach_y) + self._draw_badge(context, badge_info.size, sensitive, widget) + + self._surface_cache[cache_key] = surface + + return surface + + xo_color = property(_get_xo_color, _set_xo_color) + + +class Icon(gtk.Image): + + __gtype_name__ = 'SugarIcon' + + def __init__(self, **kwargs): + self._buffer = _IconBuffer() + # HACK: need to keep a reference to the path so it doesn't get garbage + # collected while it's still used if it's a sugar.util.TempFilePath. + # See #1175 + self._file = None + + gobject.GObject.__init__(self, **kwargs) + + def get_file(self): + return self._file + + def set_file(self, file_name): + self._file = file_name + self._buffer.file_name = file_name + + file = gobject.property(type=object, setter=set_file, getter=get_file) + + def _sync_image_properties(self): + if self._buffer.icon_name != self.props.icon_name: + self._buffer.icon_name = self.props.icon_name + + if self._buffer.file_name != self.props.file: + self._buffer.file_name = self.props.file + + if self.props.pixel_size == -1: + width, height = gtk.icon_size_lookup(self.props.icon_size) + else: + width = height = self.props.pixel_size + if self._buffer.width != width or self._buffer.height != height: + self._buffer.width = width + self._buffer.height = height + + def _icon_size_changed_cb(self, image, pspec): + self._buffer.icon_size = self.props.icon_size + + def _icon_name_changed_cb(self, image, pspec): + self._buffer.icon_name = self.props.icon_name + + def _file_changed_cb(self, image, pspec): + self._buffer.file_name = self.props.file + + def do_size_request(self, requisition): + """ + Parameters + ---------- + requisition : + + Returns + ------- + None + + """ + self._sync_image_properties() + surface = self._buffer.get_surface() + if surface: + requisition[0] = surface.get_width() + requisition[1] = surface.get_height() + elif self._buffer.width and self._buffer.height: + requisition[0] = self._buffer.width + requisition[1] = self._buffer.width + else: + requisition[0] = requisition[1] = 0 + + def do_expose_event(self, event): + """ + Parameters + ---------- + event : + + Returns: + -------- + None + + """ + self._sync_image_properties() + sensitive = (self.state != gtk.STATE_INSENSITIVE) + surface = self._buffer.get_surface(sensitive, self) + if surface is None: + return + + xpad, ypad = self.get_padding() + xalign, yalign = self.get_alignment() + requisition = self.get_child_requisition() + if self.get_direction() != gtk.TEXT_DIR_LTR: + xalign = 1.0 - xalign + + allocation = self.get_allocation() + x = math.floor(allocation.x + xpad + + (allocation.width - requisition[0]) * xalign) + y = math.floor(allocation.y + ypad + + (allocation.height - requisition[1]) * yalign) + + cr = self.window.cairo_create() + cr.set_source_surface(surface, x, y) + cr.paint() + + def set_xo_color(self, value): + """ + Parameters + ---------- + value : + + Returns + ------- + None + + """ + if self._buffer.xo_color != value: + self._buffer.xo_color = value + self.queue_draw() + + xo_color = gobject.property( + type=object, getter=None, setter=set_xo_color) + + def set_fill_color(self, value): + """ + Parameters + ---------- + value : + + Returns + ------- + None + + """ + if self._buffer.fill_color != value: + self._buffer.fill_color = value + self.queue_draw() + + def get_fill_color(self): + """ + Parameters + ---------- + None + + Returns + ------- + fill_color : + + """ + return self._buffer.fill_color + + fill_color = gobject.property( + type=object, getter=get_fill_color, setter=set_fill_color) + + def set_stroke_color(self, value): + """ + Parameters + ---------- + value : + + Returns + ------- + None + + """ + if self._buffer.stroke_color != value: + self._buffer.stroke_color = value + self.queue_draw() + + def get_stroke_color(self): + """ + Parameters + ---------- + None + + Returns + ------- + stroke_color : + + """ + return self._buffer.stroke_color + + stroke_color = gobject.property( + type=object, getter=get_stroke_color, setter=set_stroke_color) + + def set_badge_name(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + """ + if self._buffer.badge_name != value: + self._buffer.badge_name = value + self.queue_resize() + + def get_badge_name(self): + return self._buffer.badge_name + + badge_name = gobject.property( + type=str, getter=get_badge_name, setter=set_badge_name) + + +class CanvasIcon(hippo.CanvasBox, hippo.CanvasItem): + + __gtype_name__ = 'CanvasIcon' + + def __init__(self, **kwargs): + from sugar.graphics.palette import CanvasInvoker + + self._buffer = _IconBuffer() + self._palette_invoker = CanvasInvoker() + + hippo.CanvasBox.__init__(self, **kwargs) + + self._palette_invoker.attach(self) + + self.connect('destroy', self.__destroy_cb) + + def _emit_paint_needed_icon_area(self): + surface = self._buffer.get_surface() + if surface: + width, height = self.get_allocation() + s_width = surface.get_width() + s_height = surface.get_height() + + x = (width - s_width) / 2 + y = (height - s_height) / 2 + + self.emit_paint_needed(x, y, s_width, s_height) + + def __destroy_cb(self, icon): + if self._palette_invoker is not None: + self._palette_invoker.detach() + + def set_file_name(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + \"\"\" + + """ + if self._buffer.file_name != value: + self._buffer.file_name = value + self.emit_paint_needed(0, 0, -1, -1) + + def get_file_name(self): + """ + Parameters + ---------- + None + + Returns + ------- + file name : + + """ + return self._buffer.file_name + + file_name = gobject.property( + type=object, getter=get_file_name, setter=set_file_name) + + def set_icon_name(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + """ + if self._buffer.icon_name != value: + self._buffer.icon_name = value + self.emit_paint_needed(0, 0, -1, -1) + + def get_icon_name(self): + """ + Parameters + ---------- + None + + Returns + ------- + icon name : + + """ + return self._buffer.icon_name + + icon_name = gobject.property( + type=object, getter=get_icon_name, setter=set_icon_name) + + def set_xo_color(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + """ + if self._buffer.xo_color != value: + self._buffer.xo_color = value + self._emit_paint_needed_icon_area() + + xo_color = gobject.property( + type=object, getter=None, setter=set_xo_color) + + def set_fill_color(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + """ + if self._buffer.fill_color != value: + self._buffer.fill_color = value + self._emit_paint_needed_icon_area() + + def get_fill_color(self): + """ + Parameters + ---------- + None + + Returns + ------- + fill color : + + """ + return self._buffer.fill_color + + fill_color = gobject.property( + type=object, getter=get_fill_color, setter=set_fill_color) + + def set_stroke_color(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + """ + if self._buffer.stroke_color != value: + self._buffer.stroke_color = value + self._emit_paint_needed_icon_area() + + def get_stroke_color(self): + """ + Parameters + ---------- + None + + Returns + ------- + stroke color : + + """ + return self._buffer.stroke_color + + stroke_color = gobject.property( + type=object, getter=get_stroke_color, setter=set_stroke_color) + + def set_background_color(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + """ + if self._buffer.background_color != value: + self._buffer.background_color = value + self.emit_paint_needed(0, 0, -1, -1) + + def get_background_color(self): + """ + Parameters + ---------- + None + + Returns + ------- + fill color : + + """ + return self._buffer.background_color + + background_color = gobject.property( + type=object, getter=get_background_color, setter=set_background_color) + + def set_size(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + """ + if self._buffer.width != value: + self._buffer.width = value + self._buffer.height = value + self.emit_request_changed() + + def get_size(self): + """ + Parameters + ---------- + None + + Returns + ------- + size : + + """ + return self._buffer.width + + size = gobject.property( + type=object, getter=get_size, setter=set_size) + + def set_scale(self, value): + """ + Parameters + ---------- + value: + + Returns + ------- + None + + """ + logging.warning( + 'CanvasIcon: the scale parameter is currently unsupported') + if self._buffer.scale != value: + self._buffer.scale = value + self.emit_request_changed() + + def get_scale(self): + """ + Parameters + ---------- + None + + Returns + ------- + scale : + + """ + return self._buffer.scale + + scale = gobject.property( + type=float, getter=get_scale, setter=set_scale) + + def set_cache(self, value): + """ + Parameters + ---------- + cache + + Returns + ------- + None + + """ + self._buffer.cache = value + + def get_cache(self): + """ + Parameters + ---------- + None + + Returns + ------- + cache : + + """ + return self._buffer.cache + + cache = gobject.property( + type=bool, default=False, getter=get_cache, setter=set_cache) + + def set_badge_name(self, value): + """ + Parameters + ---------- + value : + + Returns + ------- + None + + """ + if self._buffer.badge_name != value: + self._buffer.badge_name = value + self.emit_paint_needed(0, 0, -1, -1) + + def get_badge_name(self): + """ + Parameters + ---------- + None + + Returns + ------- + badge name : + + """ + return self._buffer.badge_name + + badge_name = gobject.property( + type=object, getter=get_badge_name, setter=set_badge_name) + + def do_paint_below_children(self, cr, damaged_box): + """ + Parameters + ---------- + cr : + + damaged_box : + + Returns + ------- + None + + """ + surface = self._buffer.get_surface() + if surface: + width, height = self.get_allocation() + + x = (width - surface.get_width()) / 2 + y = (height - surface.get_height()) / 2 + + cr.set_source_surface(surface, x, y) + cr.paint() + + def do_get_content_width_request(self): + """ + Parameters + ---------- + None + + Returns + ------- + width : + + """ + surface = self._buffer.get_surface() + if surface: + size = surface.get_width() + elif self._buffer.width: + size = self._buffer.width + else: + size = 0 + + return size, size + + def do_get_content_height_request(self, for_width): + surface = self._buffer.get_surface() + if surface: + size = surface.get_height() + elif self._buffer.height: + size = self._buffer.height + else: + size = 0 + + return size, size + + def do_button_press_event(self, event): + if event.button == 1: + self.emit_activated() + return True + else: + return False + + def create_palette(self): + return None + + def get_palette(self): + return self._palette_invoker.palette + + def set_palette(self, palette): + self._palette_invoker.palette = palette + + palette = gobject.property( + type=object, setter=set_palette, getter=get_palette) + + def get_palette_invoker(self): + return self._palette_invoker + + def set_palette_invoker(self, palette_invoker): + self._palette_invoker.detach() + self._palette_invoker = palette_invoker + + palette_invoker = gobject.property( + type=object, setter=set_palette_invoker, getter=get_palette_invoker) + + def set_tooltip(self, text): + from sugar.graphics.palette import Palette + + self.set_palette(Palette(text)) + + +class CellRendererIcon(gtk.GenericCellRenderer): + + __gtype_name__ = 'SugarCellRendererIcon' + + __gsignals__ = { + 'clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, [object]), + } + + def __init__(self, tree_view): + from sugar.graphics.palette import CellRendererInvoker + + self._buffer = _IconBuffer() + self._buffer.cache = True + self._xo_color = None + self._fill_color = None + self._stroke_color = None + self._prelit_fill_color = None + self._prelit_stroke_color = None + self._palette_invoker = CellRendererInvoker() + + gobject.GObject.__init__(self) + + self._palette_invoker.attach_cell_renderer(tree_view, self) + + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, icon): + self._palette_invoker.detach() + + def create_palette(self): + return None + + def get_palette_invoker(self): + return self._palette_invoker + + palette_invoker = gobject.property(type=object, getter=get_palette_invoker) + + def set_file_name(self, value): + if self._buffer.file_name != value: + self._buffer.file_name = value + + file_name = gobject.property(type=str, setter=set_file_name) + + def set_icon_name(self, value): + if self._buffer.icon_name != value: + self._buffer.icon_name = value + + icon_name = gobject.property(type=str, setter=set_icon_name) + + def get_xo_color(self): + return self._xo_color + + def set_xo_color(self, value): + self._xo_color = value + + xo_color = gobject.property(type=object, + getter=get_xo_color, setter=set_xo_color) + + def set_fill_color(self, value): + if self._fill_color != value: + self._fill_color = value + + fill_color = gobject.property(type=object, setter=set_fill_color) + + def set_stroke_color(self, value): + if self._stroke_color != value: + self._stroke_color = value + + stroke_color = gobject.property(type=object, setter=set_stroke_color) + + def set_prelit_fill_color(self, value): + if self._prelit_fill_color != value: + self._prelit_fill_color = value + + prelit_fill_color = gobject.property(type=object, + setter=set_prelit_fill_color) + + def set_prelit_stroke_color(self, value): + if self._prelit_stroke_color != value: + self._prelit_stroke_color = value + + prelit_stroke_color = gobject.property(type=object, + setter=set_prelit_stroke_color) + + def set_background_color(self, value): + if self._buffer.background_color != value: + self._buffer.background_color = value + + background_color = gobject.property(type=object, + setter=set_background_color) + + def set_size(self, value): + if self._buffer.width != value: + self._buffer.width = value + self._buffer.height = value + + size = gobject.property(type=object, setter=set_size) + + def on_get_size(self, widget, cell_area): + width = self._buffer.width + self.props.xpad * 2 + height = self._buffer.height + self.props.ypad * 2 + xoffset = 0 + yoffset = 0 + + if width > 0 and height > 0 and cell_area is not None: + + if widget.get_direction() == gtk.TEXT_DIR_RTL: + xoffset = 1.0 - self.props.xalign + else: + xoffset = self.props.xalign + + xoffset = max(xoffset * (cell_area.width - width), 0) + yoffset = max(self.props.yalign * (cell_area.height - height), 0) + + return xoffset, yoffset, width, height + + def on_activate(self, event, widget, path, background_area, cell_area, + flags): + pass + + def on_start_editing(self, event, widget, path, background_area, cell_area, + flags): + pass + + def _is_prelit(self, tree_view): + x, y = tree_view.get_pointer() + x, y = tree_view.convert_widget_to_bin_window_coords(x, y) + pos = tree_view.get_path_at_pos(x, 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_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 on_render(self, window, widget, background_area, cell_area, + expose_area, flags): + if self._xo_color is not None: + stroke_color = self._xo_color.get_stroke_color() + fill_color = self._xo_color.get_fill_color() + prelit_fill_color = None + prelit_stroke_color = None + else: + stroke_color = self._stroke_color + fill_color = self._fill_color + prelit_fill_color = self._prelit_fill_color + prelit_stroke_color = self._prelit_stroke_color + + has_prelit_colors = None not in [prelit_fill_color, + prelit_stroke_color] + + if flags & gtk.CELL_RENDERER_PRELIT and has_prelit_colors and \ + self._is_prelit(widget): + + self._buffer.fill_color = prelit_fill_color + self._buffer.stroke_color = prelit_stroke_color + else: + self._buffer.fill_color = fill_color + self._buffer.stroke_color = stroke_color + + surface = self._buffer.get_surface() + if surface is None: + return + + xoffset, yoffset, width_, height_ = self.on_get_size(widget, cell_area) + + x = cell_area.x + xoffset + y = cell_area.y + yoffset + + cr = window.cairo_create() + cr.set_source_surface(surface, math.floor(x), math.floor(y)) + cr.rectangle(expose_area) + cr.paint() + + +def get_icon_state(base_name, perc, step=5): + strength = round(perc / step) * step + icon_theme = gtk.icon_theme_get_default() + + while strength <= 100 and strength >= 0: + icon_name = '%s-%03d' % (base_name, strength) + if icon_theme.has_icon(icon_name): + return icon_name + + strength = strength + step + + +def get_icon_file_name(icon_name): + icon_theme = gtk.icon_theme_get_default() + info = icon_theme.lookup_icon(icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR, 0) + if not info: + return None + filename = info.get_filename() + del info + return filename + + +def get_surface(**kwargs): + """Get cached cairo surface. + + Keyword arguments: + icon_name -- name of icon to load, default None + file_name -- path to image file, default None + fill_color -- for svg images, change default fill color + default None + stroke_color -- for svg images, change default stroke color + default None + background_color -- draw background or surface will be transparent + default None + badge_name -- name of icon which will be drawn on top of + original image, default None + width -- change image width, default None + height -- change image height, default None + cache -- if image is svg, keep svg file content for later + scale -- scale image, default 1.0 + + Return: cairo surface or None if image was not found + + """ + icon = _IconBuffer() + for key, value in kwargs.items(): + icon.__setattr__(key, value) + return icon.get_surface() diff --git a/toolkit/src/sugar/graphics/iconentry.py b/toolkit/src/sugar/graphics/iconentry.py new file mode 100644 index 0000000..4ce2c4f --- /dev/null +++ b/toolkit/src/sugar/graphics/iconentry.py @@ -0,0 +1,106 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# 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 import _sugarext + +from sugar.graphics import style +from sugar.graphics.icon import _SVGLoader + +ICON_ENTRY_PRIMARY = _sugarext.ICON_ENTRY_PRIMARY +ICON_ENTRY_SECONDARY = _sugarext.ICON_ENTRY_SECONDARY + + +class IconEntry(_sugarext.IconEntry): + + def __init__(self): + _sugarext.IconEntry.__init__(self) + + self._clear_icon = None + self._clear_shown = False + + self.connect('key_press_event', self._keypress_event_cb) + + def set_icon_from_name(self, position, name): + icon_theme = gtk.icon_theme_get_default() + icon_info = icon_theme.lookup_icon(name, + gtk.ICON_SIZE_SMALL_TOOLBAR, + 0) + + if icon_info.get_filename().endswith('.svg'): + loader = _SVGLoader() + entities = {'fill_color': style.COLOR_TOOLBAR_GREY.get_svg(), + 'stroke_color': style.COLOR_TOOLBAR_GREY.get_svg()} + handle = loader.load(icon_info.get_filename(), entities, None) + pixbuf = handle.get_pixbuf() + else: + pixbuf = gtk.gdk.pixbuf_new_from_file(icon_info.get_filename()) + del icon_info + + image = gtk.Image() + image.set_from_pixbuf(pixbuf) + image.show() + + self.set_icon(position, image) + + def set_icon(self, position, image): + if image.get_storage_type() not in [gtk.IMAGE_PIXBUF, gtk.IMAGE_STOCK]: + raise ValueError('Image must have a storage type of pixbuf or ' + + 'stock, not %r.' % image.get_storage_type()) + _sugarext.IconEntry.set_icon(self, position, image) + + def remove_icon(self, position): + _sugarext.IconEntry.set_icon(self, position, None) + + def add_clear_button(self): + if self.props.text != "": + self.show_clear_button() + else: + self.hide_clear_button() + + self.connect('icon-pressed', self._icon_pressed_cb) + self.connect('changed', self._changed_cb) + + def show_clear_button(self): + if not self._clear_shown: + self.set_icon_from_name(ICON_ENTRY_SECONDARY, + 'dialog-cancel') + self._clear_shown = True + + def hide_clear_button(self): + if self._clear_shown: + self.remove_icon(ICON_ENTRY_SECONDARY) + self._clear_shown = False + + def _keypress_event_cb(self, widget, event): + keyval = gtk.gdk.keyval_name(event.keyval) + if keyval == 'Escape': + self.props.text = '' + return True + return False + + def _icon_pressed_cb(self, entru, icon_pos, button): + if icon_pos == ICON_ENTRY_SECONDARY: + self.set_text('') + self.hide_clear_button() + + def _changed_cb(self, icon_entry): + if not self.props.text: + self.hide_clear_button() + else: + self.show_clear_button() diff --git a/toolkit/src/sugar/graphics/menuitem.py b/toolkit/src/sugar/graphics/menuitem.py new file mode 100644 index 0000000..655f7cc --- /dev/null +++ b/toolkit/src/sugar/graphics/menuitem.py @@ -0,0 +1,95 @@ +# Copyright (C) 2007, Eduardo Silva <edsiper@gmail.com> +# +# 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 gobject +import pango +import gtk + +from sugar.graphics.icon import Icon + + +class MenuItem(gtk.ImageMenuItem): + + def __init__(self, text_label=None, icon_name=None, text_maxlen=60, + xo_color=None, file_name=None): + gobject.GObject.__init__(self) + self._accelerator = None + + label = gtk.AccelLabel(text_label) + label.set_alignment(0.0, 0.5) + label.set_accel_widget(self) + if text_maxlen > 0: + label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) + label.set_max_width_chars(text_maxlen) + self.add(label) + label.show() + + if icon_name is not None: + icon = Icon(icon_name=icon_name, + icon_size=gtk.ICON_SIZE_SMALL_TOOLBAR) + if xo_color is not None: + icon.props.xo_color = xo_color + self.set_image(icon) + icon.show() + + elif file_name is not None: + icon = Icon(file=file_name, icon_size=gtk.ICON_SIZE_SMALL_TOOLBAR) + if xo_color is not None: + icon.props.xo_color = xo_color + self.set_image(icon) + icon.show() + + self.connect('can-activate-accel', self.__can_activate_accel_cb) + self.connect('hierarchy-changed', self.__hierarchy_changed_cb) + + def __hierarchy_changed_cb(self, widget, previous_toplevel): + self._add_accelerator() + + def __can_activate_accel_cb(self, widget, signal_id): + # Accept activation via accelerators regardless of this widget's state + return True + + def _add_accelerator(self): + if self._accelerator is None or self.get_toplevel() is None: + return + + # TODO: should we remove the accelerator from the prev top level? + + accel_group = self.get_toplevel().get_data('sugar-accel-group') + if not accel_group: + logging.warning('No gtk.AccelGroup in the top level window.') + return + + keyval, mask = gtk.accelerator_parse(self._accelerator) + self.add_accelerator('activate', accel_group, keyval, mask, + gtk.ACCEL_LOCKED | gtk.ACCEL_VISIBLE) + + def set_accelerator(self, accelerator): + self._accelerator = accelerator + self._add_accelerator() + + def get_accelerator(self): + return self._accelerator + + accelerator = gobject.property(type=str, setter=set_accelerator, + getter=get_accelerator) diff --git a/toolkit/src/sugar/graphics/notebook.py b/toolkit/src/sugar/graphics/notebook.py new file mode 100644 index 0000000..603b7c9 --- /dev/null +++ b/toolkit/src/sugar/graphics/notebook.py @@ -0,0 +1,151 @@ +# Copyright (C) 2007, Eduardo Silva (edsiper@gmail.com) +# +# 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. + +"""Notebook class + +This class create a gtk.Notebook() widget supporting +a close button in every tab when the 'can-close-tabs' gproperty +is enabled (True) + +STABLE. +""" + +import gtk +import gobject + + +class Notebook(gtk.Notebook): + + __gtype_name__ = 'SugarNotebook' + + __gproperties__ = { + 'can-close-tabs': (bool, None, None, False, + gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY), + } + + def __init__(self, **kwargs): + # Initialise the Widget + + # Side effects: + # Set the 'can-close-tabs' property using **kwargs + # Set True the scrollable notebook property + + gobject.GObject.__init__(self, **kwargs) + + self._can_close_tabs = None + + self.set_scrollable(True) + self.show() + + def do_set_property(self, pspec, value): + """ + Set notebook property + + Parameters + ---------- + pspec : + property for which the value will be set + + Returns + ------- + None + + Raises + ------ + AssertionError + + """ + if pspec.name == 'can-close-tabs': + self._can_close_tabs = value + else: + raise AssertionError + + def _add_icon_to_button(self, button): + icon_box = gtk.HBox() + image = gtk.Image() + image.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) + gtk.Button.set_relief(button, gtk.RELIEF_NONE) + + settings = gtk.Widget.get_settings(button) + w, h = gtk.icon_size_lookup_for_settings(settings, gtk.ICON_SIZE_MENU) + gtk.Widget.set_size_request(button, w + 4, h + 4) + image.show() + icon_box.pack_start(image, True, False, 0) + button.add(icon_box) + icon_box.show() + + def _create_custom_tab(self, text, child): + event_box = gtk.EventBox() + + tab_box = gtk.HBox(False, 2) + tab_label = gtk.Label(text) + + tab_button = gtk.Button() + tab_button.connect('clicked', self._close_page, child) + + # Add a picture on a button + self._add_icon_to_button(tab_button) + + event_box.show() + tab_button.show() + tab_label.show() + + tab_box.pack_start(tab_label, True) + tab_box.pack_start(tab_button, True) + + tab_box.show_all() + event_box.add(tab_box) + + return event_box + + def add_page(self, text_label, widget): + """ + Adds a page to the notebook. + + Parameters + ---------- + text_label : + + widget : + + Returns + ------- + Boolean + Returns TRUE if the page is successfully added to th notebook. + + """ + # Add a new page to the notebook + if self._can_close_tabs: + eventbox = self._create_custom_tab(text_label, widget) + self.append_page(widget, eventbox) + else: + self.append_page(widget, gtk.Label(text_label)) + + pages = self.get_n_pages() + + # Set the new page + self.set_current_page(pages - 1) + self.show_all() + + return True + + def _close_page(self, button, child): + # Remove a page from the notebook + page = self.page_num(child) + + if page != -1: + self.remove_page(page) diff --git a/toolkit/src/sugar/graphics/objectchooser.py b/toolkit/src/sugar/graphics/objectchooser.py new file mode 100644 index 0000000..590e35d --- /dev/null +++ b/toolkit/src/sugar/graphics/objectchooser.py @@ -0,0 +1,132 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# 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 gobject +import gtk +import dbus + +from sugar.datastore import datastore + + +J_DBUS_SERVICE = 'org.laptop.Journal' +J_DBUS_INTERFACE = 'org.laptop.Journal' +J_DBUS_PATH = '/org/laptop/Journal' + + +class ObjectChooser(object): + + def __init__(self, title=None, parent=None, flags=None, buttons=None, + what_filter=None): + # For backwards compatibility: + # - We ignore title, flags and buttons. + # - 'parent' can be a xid or a gtk.Window + + if title is not None or flags is not None or buttons is not None: + logging.warning('Invocation of ObjectChooser() has deprecated ' + 'parameters.') + + if parent is None: + parent_xid = 0 + elif hasattr(parent, 'window') and hasattr(parent.window, 'xid'): + parent_xid = parent.window.xid + else: + parent_xid = parent + + self._parent_xid = parent_xid + self._main_loop = None + self._object_id = None + self._bus = None + self._chooser_id = None + self._response_code = gtk.RESPONSE_NONE + self._what_filter = what_filter + + def run(self): + self._object_id = None + + self._main_loop = gobject.MainLoop() + + self._bus = dbus.SessionBus(mainloop=self._main_loop) + self._bus.add_signal_receiver( + self.__name_owner_changed_cb, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus", + arg0=J_DBUS_SERVICE) + + obj = self._bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) + journal = dbus.Interface(obj, J_DBUS_INTERFACE) + journal.connect_to_signal('ObjectChooserResponse', + self.__chooser_response_cb) + journal.connect_to_signal('ObjectChooserCancelled', + self.__chooser_cancelled_cb) + + if self._what_filter is None: + what_filter = '' + else: + what_filter = self._what_filter + + self._chooser_id = journal.ChooseObject(self._parent_xid, what_filter) + + gtk.gdk.threads_leave() + try: + self._main_loop.run() + finally: + gtk.gdk.threads_enter() + self._main_loop = None + + return self._response_code + + def get_selected_object(self): + if self._object_id is None: + return None + else: + return datastore.get(self._object_id) + + def destroy(self): + self._cleanup() + + def _cleanup(self): + if self._main_loop is not None: + self._main_loop.quit() + self._main_loop = None + self._bus = None + + def __chooser_response_cb(self, chooser_id, object_id): + if chooser_id != self._chooser_id: + return + logging.debug('ObjectChooser.__chooser_response_cb: %r', object_id) + self._response_code = gtk.RESPONSE_ACCEPT + self._object_id = object_id + self._cleanup() + + def __chooser_cancelled_cb(self, chooser_id): + if chooser_id != self._chooser_id: + return + logging.debug('ObjectChooser.__chooser_cancelled_cb: %r', chooser_id) + self._response_code = gtk.RESPONSE_CANCEL + self._cleanup() + + def __name_owner_changed_cb(self, name, old, new): + logging.debug('ObjectChooser.__name_owner_changed_cb') + # Journal service disappeared from the bus + self._response_code = gtk.RESPONSE_CANCEL + self._cleanup() diff --git a/toolkit/src/sugar/graphics/palette.py b/toolkit/src/sugar/graphics/palette.py new file mode 100644 index 0000000..d4632eb --- /dev/null +++ b/toolkit/src/sugar/graphics/palette.py @@ -0,0 +1,446 @@ +# 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 pango + +from sugar.graphics import palettegroup +from sugar.graphics import animator +from sugar.graphics import style +from sugar.graphics.icon import Icon +from sugar.graphics.palettewindow import PaletteWindow +from sugar import _sugarext + +# DEPRECATED +# Import these for backwards compatibility +from sugar.graphics.palettewindow import MouseSpeedDetector, Invoker, \ + WidgetInvoker, CanvasInvoker, ToolInvoker, CellRendererInvoker + + +class Palette(PaletteWindow): + PRIMARY = 0 + SECONDARY = 1 + + __gtype_name__ = 'SugarPalette' + + def __init__(self, label=None, accel_path=None, menu_after_content=False, + text_maxlen=60, **kwargs): + # DEPRECATED: label is passed with the primary-text property, + # accel_path is set via the invoker property, and menu_after_content + # is not used + + self._primary_text = None + self._secondary_text = None + self._icon = None + self._icon_visible = True + self._palette_state = self.PRIMARY + + palette_box = gtk.VBox() + + primary_box = gtk.HBox() + palette_box.pack_start(primary_box, expand=False) + primary_box.show() + + self._icon_box = gtk.HBox() + self._icon_box.set_size_request(style.GRID_CELL_SIZE, -1) + primary_box.pack_start(self._icon_box, expand=False) + + labels_box = gtk.VBox() + self._label_alignment = gtk.Alignment(xalign=0, yalign=0.5, + xscale=1, yscale=0.33) + self._label_alignment.set_padding(0, 0, style.DEFAULT_SPACING, + style.DEFAULT_SPACING) + self._label_alignment.add(labels_box) + self._label_alignment.show() + primary_box.pack_start(self._label_alignment, expand=True) + labels_box.show() + + self._label = gtk.AccelLabel('') + self._label.set_alignment(0, 0.5) + + if text_maxlen > 0: + self._label.set_max_width_chars(text_maxlen) + self._label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) + labels_box.pack_start(self._label, expand=True) + + self._secondary_label = gtk.Label() + self._secondary_label.set_alignment(0, 0.5) + + if text_maxlen > 0: + self._secondary_label.set_max_width_chars(text_maxlen) + self._secondary_label.set_ellipsize(pango.ELLIPSIZE_END) + + labels_box.pack_start(self._secondary_label, expand=True) + + self._secondary_box = gtk.VBox() + palette_box.pack_start(self._secondary_box) + + self._separator = gtk.HSeparator() + self._secondary_box.pack_start(self._separator) + + self._menu_content_separator = gtk.HSeparator() + + self._secondary_anim = animator.Animator(2.0, 10) + self._secondary_anim.add(_SecondaryAnimation(self)) + + # we init after initializing all of our containers + PaletteWindow.__init__(self, **kwargs) + + primary_box.set_size_request(-1, style.GRID_CELL_SIZE + - 2 * self.get_border_width()) + + self._full_request = [0, 0] + self._menu_box = None + self._content = None + + # we set these for backward compatibility + if label is not None: + self.props.primary_text = label + + self._add_menu() + self._secondary_box.pack_start(self._menu_content_separator) + self._add_content() + + self.action_bar = PaletteActionBar() + self._secondary_box.pack_start(self.action_bar) + self.action_bar.show() + + self.add(palette_box) + palette_box.show() + + # The menu is not shown here until an item is added + self.menu = _Menu(self) + self.menu.connect('item-inserted', self.__menu_item_inserted_cb) + + self.connect('realize', self.__realize_cb) + self.connect('show', self.__show_cb) + self.connect('hide', self.__hide_cb) + self.connect('notify::invoker', self.__notify_invoker_cb) + self.connect('destroy', self.__destroy_cb) + + def _invoker_right_click_cb(self, invoker): + self.popup(immediate=True, state=self.SECONDARY) + + def do_style_set(self, previous_style): + # Prevent a warning from pygtk + if previous_style is not None: + gtk.Window.do_style_set(self, previous_style) + self.set_border_width(self.get_style().xthickness) + + def __menu_item_inserted_cb(self, menu): + self._update_separators() + + def __destroy_cb(self, palette): + self._secondary_anim.stop() + self.popdown(immediate=True) + # Break the reference cycle. It looks like the gc is not able to free + # it, possibly because gtk.Menu memory handling is very special. + self.menu = None + + def __show_cb(self, widget): + self.menu.set_active(True) + + def __hide_cb(self, widget): + self.menu.set_active(False) + self.menu.cancel() + self._secondary_anim.stop() + + def __notify_invoker_cb(self, palette, pspec): + invoker = self.props.invoker + if invoker is not None and hasattr(invoker.props, 'widget'): + self._update_accel_widget() + self._invoker.connect('notify::widget', + self.__invoker_widget_changed_cb) + + def __invoker_widget_changed_cb(self, invoker, spec): + self._update_accel_widget() + + def get_full_size_request(self): + return self._full_request + + def popup(self, immediate=False, state=None): + if self._invoker is not None: + self._update_full_request() + + PaletteWindow.popup(self, immediate) + + if state is None: + state = self.PRIMARY + self.set_palette_state(state) + + if state == self.PRIMARY: + self._secondary_anim.start() + else: + self._secondary_anim.stop() + + def popdown(self, immediate=False): + if immediate: + self._secondary_anim.stop() + self._popdown_submenus() + # to suppress glitches while later re-opening + self.set_palette_state(self.PRIMARY) + PaletteWindow.popdown(self, immediate) + + def _popdown_submenus(self): + # TODO explicit hiding of subitems + # should be removed after fixing #1301 + if self.menu is not None: + for menu_item in self.menu.get_children(): + if menu_item.props.submenu is not None: + menu_item.props.submenu.popdown() + + def on_enter(self, event): + PaletteWindow.on_enter(self, event) + self._secondary_anim.start() + + def _add_menu(self): + self._menu_box = gtk.VBox() + self._secondary_box.pack_start(self._menu_box) + self._menu_box.show() + + def _add_content(self): + # The content is not shown until a widget is added + self._content = gtk.VBox() + self._content.set_border_width(style.DEFAULT_SPACING) + self._secondary_box.pack_start(self._content) + + def _update_accel_widget(self): + assert self.props.invoker is not None + self._label.props.accel_widget = self.props.invoker.props.widget + + def set_primary_text(self, label, accel_path=None): + self._primary_text = label + + if label is not None: + self._label.set_markup('<b>%s</b>' % label) + self._label.show() + + def get_primary_text(self): + return self._primary_text + + primary_text = gobject.property(type=str, + getter=get_primary_text, + setter=set_primary_text) + + def set_secondary_text(self, label): + if label is not None: + label = label.split('\n', 1)[0] + self._secondary_text = label + + if label is None: + self._secondary_label.hide() + else: + self._secondary_label.set_text(label) + self._secondary_label.show() + + def get_secondary_text(self): + return self._secondary_text + + secondary_text = gobject.property(type=str, getter=get_secondary_text, + setter=set_secondary_text) + + def _show_icon(self): + self._label_alignment.set_padding(0, 0, 0, style.DEFAULT_SPACING) + self._icon_box.show() + + def _hide_icon(self): + self._icon_box.hide() + self._label_alignment.set_padding(0, 0, style.DEFAULT_SPACING, + style.DEFAULT_SPACING) + + def set_icon(self, icon): + if icon is None: + self._icon = None + self._hide_icon() + else: + if self._icon: + self._icon_box.remove(self._icon_box.get_children()[0]) + + event_box = gtk.EventBox() + event_box.connect('button-release-event', + self.__icon_button_release_event_cb) + self._icon_box.pack_start(event_box) + event_box.show() + + self._icon = icon + self._icon.props.icon_size = gtk.ICON_SIZE_LARGE_TOOLBAR + event_box.add(self._icon) + self._icon.show() + self._show_icon() + + def get_icon(self): + return self._icon + + icon = gobject.property(type=object, getter=get_icon, setter=set_icon) + + def __icon_button_release_event_cb(self, icon, event): + self.emit('activate') + + def set_icon_visible(self, visible): + self._icon_visible = visible + + if visible and self._icon is not None: + self._show_icon() + else: + self._hide_icon() + + def get_icon_visible(self): + return self._icon_visilbe + + icon_visible = gobject.property(type=bool, + default=True, + getter=get_icon_visible, + setter=set_icon_visible) + + def set_content(self, widget): + if len(self._content.get_children()) > 0: + self._content.remove(self._content.get_children()[0]) + + if widget is not None: + self._content.add(widget) + self._content.show() + else: + self._content.hide() + + self._update_accept_focus() + self._update_separators() + + def do_size_request(self, requisition): + PaletteWindow.do_size_request(self, requisition) + + # gtk.AccelLabel request doesn't include the accelerator. + label_width = self._label_alignment.size_request()[0] + \ + self._label.get_accel_width() + \ + 2 * self.get_border_width() + + requisition.width = max(requisition.width, + label_width, + self._full_request[0]) + + def _update_separators(self): + visible = len(self.menu.get_children()) > 0 or \ + len(self._content.get_children()) > 0 + self._separator.props.visible = visible + + visible = len(self.menu.get_children()) > 0 and \ + len(self._content.get_children()) > 0 + self._menu_content_separator.props.visible = visible + + def _update_accept_focus(self): + accept_focus = len(self._content.get_children()) + if self.window: + self.window.set_accept_focus(accept_focus) + + def __realize_cb(self, widget): + self._update_accept_focus() + + def _update_full_request(self): + if self._palette_state == self.PRIMARY: + self.menu.embed(self._menu_box) + self._secondary_box.show() + + self._full_request = self.size_request() + + if self._palette_state == self.PRIMARY: + self.menu.unembed() + self._secondary_box.hide() + + def _set_palette_state(self, state): + if self._palette_state == state: + return + + if state == self.PRIMARY: + self.menu.unembed() + self._secondary_box.hide() + elif state == self.SECONDARY: + self.menu.embed(self._menu_box) + self._secondary_box.show() + self.update_position() + + self._palette_state = state + + +class PaletteActionBar(gtk.HButtonBox): + + def add_action(self, label, icon_name=None): + button = gtk.Button(label) + + if icon_name: + icon = Icon(icon_name) + button.set_image(icon) + icon.show() + + self.pack_start(button) + button.show() + + +class _Menu(_sugarext.Menu): + + __gtype_name__ = 'SugarPaletteMenu' + + __gsignals__ = { + 'item-inserted': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, palette): + _sugarext.Menu.__init__(self) + self._palette = palette + + def do_insert(self, item, position): + _sugarext.Menu.do_insert(self, item, position) + self.emit('item-inserted') + self.show() + + def attach(self, child, left_attach, right_attach, + top_attach, bottom_attach): + _sugarext.Menu.attach(self, child, left_attach, right_attach, + top_attach, bottom_attach) + self.emit('item-inserted') + self.show() + + def do_expose_event(self, event): + # Ignore the Menu expose, just do the MenuShell expose to prevent any + # border from being drawn here. A border is drawn by the palette object + # around everything. + gtk.MenuShell.do_expose_event(self, event) + + def do_grab_notify(self, was_grabbed): + # Ignore grab_notify as the menu would close otherwise + pass + + def do_deactivate(self): + self._palette.hide() + + +class _SecondaryAnimation(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.set_palette_state(Palette.SECONDARY) diff --git a/toolkit/src/sugar/graphics/palettegroup.py b/toolkit/src/sugar/graphics/palettegroup.py new file mode 100644 index 0000000..05c713c --- /dev/null +++ b/toolkit/src/sugar/graphics/palettegroup.py @@ -0,0 +1,106 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# 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 + + +_groups = {} + + +def get_group(group_id): + if _groups.has_key(group_id): + group = _groups[group_id] + else: + group = Group() + _groups[group_id] = group + + return group + + +def popdown_all(): + for group in _groups.values(): + group.popdown() + + +class Group(gobject.GObject): + + __gsignals__ = { + 'popup': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'popdown': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self): + gobject.GObject.__init__(self) + self._up = False + self._palettes = [] + self._sig_ids = {} + + def is_up(self): + return self._up + + def get_state(self): + for palette in self._palettes: + if palette.is_up(): + return palette.palette_state + + return None + + def add(self, palette): + self._palettes.append(palette) + + self._sig_ids[palette] = [] + + sid = palette.connect('popup', self._palette_popup_cb) + self._sig_ids[palette].append(sid) + + sid = palette.connect('popdown', self._palette_popdown_cb) + self._sig_ids[palette].append(sid) + + def remove(self, palette): + sig_ids = self._sig_ids[palette] + for sid in sig_ids: + palette.disconnect(sid) + + self._palettes.remove(palette) + del self._sig_ids[palette] + + def popdown(self): + for palette in self._palettes: + if palette.is_up(): + palette.popdown(immediate=True) + + def _palette_popup_cb(self, palette): + for i in self._palettes: + if i != palette: + i.popdown(immediate=True) + if not self._up: + self.emit('popup') + self._up = True + + def _palette_popdown_cb(self, palette): + down = True + for palette in self._palettes: + if palette.is_up(): + down = False + + if down: + self._up = False + self.emit('popdown') diff --git a/toolkit/src/sugar/graphics/palettewindow.py b/toolkit/src/sugar/graphics/palettewindow.py new file mode 100644 index 0000000..22131c2 --- /dev/null +++ b/toolkit/src/sugar/graphics/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): + + __gtype_name__ = 'SugarPaletteWindow' + + __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): + 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): + 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): + + __gtype_name__ = 'SugarPaletteInvoker' + + __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/src/sugar/graphics/panel.py b/toolkit/src/sugar/graphics/panel.py new file mode 100644 index 0000000..441e7a9 --- /dev/null +++ b/toolkit/src/sugar/graphics/panel.py @@ -0,0 +1,30 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 gtk + + +class Panel(gtk.VBox): + + __gtype_name__ = 'SugarPanel' + + def __init__(self): + gtk.VBox.__init__(self) diff --git a/toolkit/src/sugar/graphics/radiopalette.py b/toolkit/src/sugar/graphics/radiopalette.py new file mode 100644 index 0000000..8199165 --- /dev/null +++ b/toolkit/src/sugar/graphics/radiopalette.py @@ -0,0 +1,104 @@ +# 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('notify::palette', self.__palette_cb) + + def __palette_cb(self, widget, pspec): + if not isinstance(self.props.palette, RadioPalette): + return + self.props.palette.update_button() + + 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, state=Palette.SECONDARY) + + +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.invoker is not None: + parent = self.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/src/sugar/graphics/radiotoolbutton.py b/toolkit/src/sugar/graphics/radiotoolbutton.py new file mode 100644 index 0000000..37267b4 --- /dev/null +++ b/toolkit/src/sugar/graphics/radiotoolbutton.py @@ -0,0 +1,182 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007-2008, One Laptop Per Child +# +# 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 gtk +import gobject + +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, ToolInvoker +from sugar.graphics import toolbutton + + +class RadioToolButton(gtk.RadioToolButton): + """ + An implementation of a "push" button. + + """ + + __gtype_name__ = 'SugarRadioToolButton' + + def __init__(self, **kwargs): + self._accelerator = None + self._tooltip = None + self._xo_color = None + self._palette_invoker = ToolInvoker() + + gobject.GObject.__init__(self, **kwargs) + + self._palette_invoker.attach_tool(self) + + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, icon): + if self._palette_invoker is not None: + self._palette_invoker.detach() + + def set_tooltip(self, tooltip): + """ + Set a simple palette with just a single label. + + Parameters + ---------- + tooltip: + + Returns + ------- + None + + """ + if self.palette is None or self._tooltip is None: + self.palette = Palette(tooltip) + elif self.palette is not None: + self.palette.set_primary_text(tooltip) + + self._tooltip = tooltip + + # Set label, shows up when toolbar overflows + gtk.RadioToolButton.set_label(self, tooltip) + + def get_tooltip(self): + return self._tooltip + + tooltip = gobject.property(type=str, setter=set_tooltip, + getter=get_tooltip) + + def set_accelerator(self, accelerator): + """ + Sets the accelerator. + + Parameters + ---------- + accelerator: + + Returns + ------- + None + + """ + self._accelerator = accelerator + toolbutton.setup_accelerator(self) + + def get_accelerator(self): + """ + Returns the accelerator for the button. + + Parameters + ---------- + None + + Returns + ------ + accelerator: + + """ + return self._accelerator + + accelerator = gobject.property(type=str, setter=set_accelerator, + getter=get_accelerator) + + def set_named_icon(self, named_icon): + icon = Icon(icon_name=named_icon, + xo_color=self._xo_color, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + self.set_icon_widget(icon) + icon.show() + + def get_named_icon(self): + if self.props.icon_widget is not None: + return self.props.icon_widget.props.icon_name + else: + return None + + named_icon = gobject.property(type=str, setter=set_named_icon, + getter=get_named_icon) + + def set_xo_color(self, xo_color): + if self._xo_color != xo_color: + self._xo_color = xo_color + if self.props.icon_widget is not None: + self.props.icon_widget.props.xo_color = xo_color + + def get_xo_color(self): + return self._xo_color + + xo_color = gobject.property(type=object, setter=set_xo_color, + getter=get_xo_color) + + def create_palette(self): + return None + + def get_palette(self): + return self._palette_invoker.palette + + def set_palette(self, palette): + self._palette_invoker.palette = palette + + palette = gobject.property( + type=object, setter=set_palette, getter=get_palette) + + def get_palette_invoker(self): + return self._palette_invoker + + def set_palette_invoker(self, palette_invoker): + self._palette_invoker.detach() + self._palette_invoker = palette_invoker + + palette_invoker = gobject.property( + type=object, setter=set_palette_invoker, getter=get_palette_invoker) + + def do_expose_event(self, event): + child = self.get_child() + allocation = self.get_allocation() + + if self.palette and self.palette.is_up(): + invoker = self.palette.props.invoker + invoker.draw_rectangle(event, self.palette) + elif child.state == gtk.STATE_PRELIGHT: + child.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_NONE, event.area, + child, "toolbutton-prelight", + allocation.x, allocation.y, + allocation.width, allocation.height) + + gtk.RadioToolButton.do_expose_event(self, event) diff --git a/toolkit/src/sugar/graphics/roundbox.py b/toolkit/src/sugar/graphics/roundbox.py new file mode 100644 index 0000000..75141f0 --- /dev/null +++ b/toolkit/src/sugar/graphics/roundbox.py @@ -0,0 +1,71 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 math + +import hippo + +from sugar.graphics import style + + +class CanvasRoundBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarRoundBox' + + _BORDER_DEFAULT = style.LINE_WIDTH + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + + # TODO: we should calculate radius depending on the height of the box. + self._radius = style.zoom(10) + + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + self.props.border = self._BORDER_DEFAULT + self.props.border_left = self._radius + self.props.border_right = self._radius + self.props.border_color = style.COLOR_BLACK.get_int() + + def do_paint_background(self, cr, damaged_box): + [width, height] = self.get_allocation() + + x = self._BORDER_DEFAULT / 2 + y = self._BORDER_DEFAULT / 2 + width -= self._BORDER_DEFAULT + height -= self._BORDER_DEFAULT + + cr.move_to(x + self._radius, y) + cr.arc(x + width - self._radius, y + self._radius, + self._radius, math.pi * 1.5, math.pi * 2) + cr.arc(x + width - self._radius, x + height - self._radius, + self._radius, 0, math.pi * 0.5) + cr.arc(x + self._radius, y + height - self._radius, + self._radius, math.pi * 0.5, math.pi) + cr.arc(x + self._radius, y + self._radius, self._radius, + math.pi, math.pi * 1.5) + + hippo.cairo_set_source_rgba32(cr, self.props.background_color) + cr.fill_preserve() + + # TODO: we should be more consistent here with the border properties. + if self.props.border_color: + hippo.cairo_set_source_rgba32(cr, self.props.border_color) + cr.set_line_width(self.props.border_top) + cr.stroke() diff --git a/toolkit/src/sugar/graphics/style.py b/toolkit/src/sugar/graphics/style.py new file mode 100644 index 0000000..2828b7f --- /dev/null +++ b/toolkit/src/sugar/graphics/style.py @@ -0,0 +1,148 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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. + +""" +All the constants are expressed in pixels. They are defined for the XO screen +and are usually adapted to different resolution by applying a zoom factor. + +STABLE. +""" + +import os +import logging + +import gtk +import pango +import gconf + + +FOCUS_LINE_WIDTH = 2 +_TAB_CURVATURE = 1 + + +def _compute_zoom_factor(): + if os.environ.has_key('SUGAR_SCALING'): + try: + scaling = int(os.environ['SUGAR_SCALING']) + return scaling / 100.0 + except ValueError: + logging.error('Invalid SUGAR_SCALING.') + + return 1.0 + + +class Font(object): + + def __init__(self, desc): + self._desc = desc + + def __str__(self): + return self._desc + + def get_pango_desc(self): + return pango.FontDescription(self._desc) + + +class Color(object): + + def __init__(self, color, alpha=1.0): + self._r, self._g, self._b = self._html_to_rgb(color) + self._a = alpha + + def get_rgba(self): + return (self._r, self._g, self._b, self._a) + + def get_int(self): + return int(self._a * 255) + (int(self._b * 255) << 8) + \ + (int(self._g * 255) << 16) + (int(self._r * 255) << 24) + + def get_gdk_color(self): + return gtk.gdk.Color(int(self._r * 65535), int(self._g * 65535), + int(self._b * 65535)) + + def get_html(self): + return '#%02x%02x%02x' % (self._r * 255, self._g * 255, self._b * 255) + + def _html_to_rgb(self, html_color): + """ #RRGGBB -> (r, g, b) tuple (in float format) """ + + html_color = html_color.strip() + if html_color[0] == '#': + html_color = html_color[1:] + if len(html_color) != 6: + raise ValueError, "input #%s is not in #RRGGBB format" % html_color + + r, g, b = html_color[:2], html_color[2:4], html_color[4:] + r, g, b = [int(n, 16) for n in (r, g, b)] + r, g, b = (r / 255.0, g / 255.0, b / 255.0) + + return (r, g, b) + + def get_svg(self): + if self._a == 0.0: + return 'none' + else: + return self.get_html() + + +def zoom(units): + return int(ZOOM_FACTOR * units) + + +ZOOM_FACTOR = _compute_zoom_factor() + +DEFAULT_SPACING = zoom(15) +DEFAULT_PADDING = zoom(6) +GRID_CELL_SIZE = zoom(75) +LINE_WIDTH = zoom(2) + +STANDARD_ICON_SIZE = zoom(55) +SMALL_ICON_SIZE = zoom(55 * 0.5) +MEDIUM_ICON_SIZE = zoom(55 * 1.5) +LARGE_ICON_SIZE = zoom(55 * 2.0) +XLARGE_ICON_SIZE = zoom(55 * 2.75) + +client = gconf.client_get_default() +FONT_SIZE = client.get_float('/desktop/sugar/font/default_size') +FONT_FACE = client.get_string('/desktop/sugar/font/default_face') + +FONT_NORMAL = Font('%s %f' % (FONT_FACE, FONT_SIZE)) +FONT_BOLD = Font('%s bold %f' % (FONT_FACE, FONT_SIZE)) +FONT_NORMAL_H = zoom(24) +FONT_BOLD_H = zoom(24) + +TOOLBOX_SEPARATOR_HEIGHT = zoom(9) +TOOLBOX_HORIZONTAL_PADDING = zoom(75) +TOOLBOX_TAB_VBORDER = int((zoom(36) - FONT_NORMAL_H - FOCUS_LINE_WIDTH) / 2) +TOOLBOX_TAB_HBORDER = zoom(15) - FOCUS_LINE_WIDTH - _TAB_CURVATURE +TOOLBOX_TAB_LABEL_WIDTH = zoom(150 - 15 * 2) + +COLOR_BLACK = Color('#000000') +COLOR_WHITE = Color('#FFFFFF') +COLOR_TRANSPARENT = Color('#FFFFFF', alpha=0.0) +COLOR_PANEL_GREY = Color('#C0C0C0') +COLOR_SELECTION_GREY = Color('#A6A6A6') +COLOR_TOOLBAR_GREY = Color('#282828') +COLOR_BUTTON_GREY = Color('#808080') +COLOR_INACTIVE_FILL = Color('#9D9FA1') +COLOR_INACTIVE_STROKE = Color('#757575') +COLOR_TEXT_FIELD_GREY = Color('#E5E5E5') +COLOR_HIGHLIGHT = Color('#E7E7E7') + +PALETTE_CURSOR_DISTANCE = zoom(10) + +TOOLBAR_ARROW_SIZE = zoom(24) diff --git a/toolkit/src/sugar/graphics/toggletoolbutton.py b/toolkit/src/sugar/graphics/toggletoolbutton.py new file mode 100644 index 0000000..cdaf2f0 --- /dev/null +++ b/toolkit/src/sugar/graphics/toggletoolbutton.py @@ -0,0 +1,91 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 + +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, ToolInvoker + + +class ToggleToolButton(gtk.ToggleToolButton): + + __gtype_name__ = "SugarToggleToolButton" + + def __init__(self, named_icon=None): + gtk.ToggleToolButton.__init__(self) + + self._palette_invoker = ToolInvoker(self) + self.set_named_icon(named_icon) + + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, icon): + if self._palette_invoker is not None: + self._palette_invoker.detach() + + def set_named_icon(self, named_icon): + icon = Icon(icon_name=named_icon) + self.set_icon_widget(icon) + icon.show() + + def create_palette(self): + return None + + def get_palette(self): + return self._palette_invoker.palette + + def set_palette(self, palette): + self._palette_invoker.palette = palette + + palette = gobject.property( + type=object, setter=set_palette, getter=get_palette) + + def get_palette_invoker(self): + return self._palette_invoker + + def set_palette_invoker(self, palette_invoker): + self._palette_invoker.detach() + self._palette_invoker = palette_invoker + + palette_invoker = gobject.property( + type=object, setter=set_palette_invoker, getter=get_palette_invoker) + + def set_tooltip(self, text): + self.set_palette(Palette(text)) + + def do_expose_event(self, event): + allocation = self.get_allocation() + child = self.get_child() + + if self.palette and self.palette.is_up(): + invoker = self.palette.props.invoker + invoker.draw_rectangle(event, self.palette) + elif child.state == gtk.STATE_PRELIGHT: + child.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_NONE, event.area, + child, "toolbutton-prelight", + allocation.x, allocation.y, + allocation.width, allocation.height) + + gtk.ToggleToolButton.do_expose_event(self, event) + + palette = property(get_palette, set_palette) diff --git a/toolkit/src/sugar/graphics/toolbarbox.py b/toolkit/src/sugar/graphics/toolbarbox.py new file mode 100644 index 0000000..b674e8d --- /dev/null +++ b/toolkit/src/sugar/graphics/toolbarbox.py @@ -0,0 +1,323 @@ +# 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.palette import PaletteWindow, ToolInvoker +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics import palettegroup + + +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())) + + 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) + self.page_widget.set_size_request(-1, style.GRID_CELL_SIZE) + 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 + style.FOCUS_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 + style.FOCUS_LINE_WIDTH, style.FOCUS_LINE_WIDTH, + alloc.width - style.FOCUS_LINE_WIDTH * 2, alloc.height) + + gtk.ToolButton.do_expose_event(self, event) + _paint_arrow(self, event, gtk.ARROW_UP) + + +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', -style.FOCUS_LINE_WIDTH, 0, + self.allocation.width + style.FOCUS_LINE_WIDTH * 2, + self.allocation.height + style.FOCUS_LINE_WIDTH) + self.get_style().paint_box(event.window, + gtk.STATE_NORMAL, gtk.SHADOW_NONE, event.area, self, None, + alloc.x + style.FOCUS_LINE_WIDTH, 0, + alloc.width - style.FOCUS_LINE_WIDTH * 2, + style.FOCUS_LINE_WIDTH) + + +def _setup_page(page_widget, color, hpad): + vpad = style.FOCUS_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 - style.TOOLBAR_ARROW_SIZE / 2 + y = alloc.y + alloc.height - int(style.TOOLBAR_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, style.TOOLBAR_ARROW_SIZE, style.TOOLBAR_ARROW_SIZE) diff --git a/toolkit/src/sugar/graphics/toolbox.py b/toolkit/src/sugar/graphics/toolbox.py new file mode 100644 index 0000000..9f29281 --- /dev/null +++ b/toolkit/src/sugar/graphics/toolbox.py @@ -0,0 +1,101 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 gtk +import gobject +import hippo + +from sugar.graphics import style + + +class Toolbox(gtk.VBox): + + __gtype_name__ = 'SugarToolbox' + + __gsignals__ = { + 'current-toolbar-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([int])), + } + + def __init__(self): + gtk.VBox.__init__(self) + + self._notebook = gtk.Notebook() + self._notebook.set_tab_pos(gtk.POS_BOTTOM) + self._notebook.set_show_border(False) + self._notebook.set_show_tabs(False) + self._notebook.props.tab_vborder = style.TOOLBOX_TAB_VBORDER + self._notebook.props.tab_hborder = style.TOOLBOX_TAB_HBORDER + self.pack_start(self._notebook) + self._notebook.show() + + # FIXME improve gtk.Notebook and do this in the theme + self._separator = hippo.Canvas() + box = hippo.CanvasBox( + border_color=style.COLOR_BUTTON_GREY.get_int(), + background_color=style.COLOR_PANEL_GREY.get_int(), + box_height=style.TOOLBOX_SEPARATOR_HEIGHT, + border_bottom=style.LINE_WIDTH) + self._separator.set_root(box) + self.pack_start(self._separator, False) + + self._notebook.connect('notify::page', self._notify_page_cb) + + def _notify_page_cb(self, notebook, pspec): + self.emit('current-toolbar-changed', notebook.props.page) + + def add_toolbar(self, name, toolbar): + label = gtk.Label(name) + width, height_ = label.size_request() + label.set_size_request(max(width, style.TOOLBOX_TAB_LABEL_WIDTH), -1) + label.set_alignment(0.0, 0.5) + + event_box = gtk.EventBox() + + alignment = gtk.Alignment(0.0, 0.0, 1.0, 1.0) + alignment.set_padding(0, 0, style.TOOLBOX_HORIZONTAL_PADDING, + style.TOOLBOX_HORIZONTAL_PADDING) + + alignment.add(toolbar) + event_box.add(alignment) + alignment.show() + event_box.show() + + self._notebook.append_page(event_box, label) + + if self._notebook.get_n_pages() > 1: + self._notebook.set_show_tabs(True) + self._separator.show() + + def remove_toolbar(self, index): + self._notebook.remove_page(index) + + if self._notebook.get_n_pages() < 2: + self._notebook.set_show_tabs(False) + self._separator.hide() + + def set_current_toolbar(self, index): + self._notebook.set_current_page(index) + + def get_current_toolbar(self): + return self._notebook.get_current_page() + + current_toolbar = property(get_current_toolbar, set_current_toolbar) diff --git a/toolkit/src/sugar/graphics/toolbutton.py b/toolkit/src/sugar/graphics/toolbutton.py new file mode 100644 index 0000000..f15e406 --- /dev/null +++ b/toolkit/src/sugar/graphics/toolbutton.py @@ -0,0 +1,162 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2008, One Laptop Per Child +# +# 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 + +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, ToolInvoker + + +def _add_accelerator(tool_button): + if not tool_button.props.accelerator or not tool_button.get_toplevel() or \ + not tool_button.child: + return + + # TODO: should we remove the accelerator from the prev top level? + + accel_group = tool_button.get_toplevel().get_data('sugar-accel-group') + if not accel_group: + logging.warning('No gtk.AccelGroup in the top level window.') + return + + keyval, mask = gtk.accelerator_parse(tool_button.props.accelerator) + # the accelerator needs to be set at the child, so the gtk.AccelLabel + # in the palette can pick it up. + tool_button.child.add_accelerator('clicked', accel_group, keyval, mask, + gtk.ACCEL_LOCKED | gtk.ACCEL_VISIBLE) + + +def _hierarchy_changed_cb(tool_button, previous_toplevel): + _add_accelerator(tool_button) + + +def setup_accelerator(tool_button): + _add_accelerator(tool_button) + tool_button.connect('hierarchy-changed', _hierarchy_changed_cb) + + +class ToolButton(gtk.ToolButton): + + __gtype_name__ = "SugarToolButton" + + def __init__(self, icon_name=None, **kwargs): + self._accelerator = None + self._tooltip = None + self._palette_invoker = ToolInvoker() + + gobject.GObject.__init__(self, **kwargs) + + self._palette_invoker.attach_tool(self) + + if icon_name: + self.set_icon(icon_name) + + self.get_child().connect('can-activate-accel', + self.__button_can_activate_accel_cb) + + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, icon): + if self._palette_invoker is not None: + self._palette_invoker.detach() + + def __button_can_activate_accel_cb(self, button, signal_id): + # Accept activation via accelerators regardless of this widget's state + return True + + def set_tooltip(self, tooltip): + """ Set a simple palette with just a single label. + """ + if self.palette is None or self._tooltip is None: + self.palette = Palette(tooltip) + elif self.palette is not None: + self.palette.set_primary_text(tooltip) + + self._tooltip = tooltip + + # Set label, shows up when toolbar overflows + gtk.ToolButton.set_label(self, tooltip) + + def get_tooltip(self): + return self._tooltip + + tooltip = gobject.property(type=str, setter=set_tooltip, + getter=get_tooltip) + + def set_accelerator(self, accelerator): + self._accelerator = accelerator + setup_accelerator(self) + + def get_accelerator(self): + return self._accelerator + + accelerator = gobject.property(type=str, setter=set_accelerator, + getter=get_accelerator) + + def set_icon(self, icon_name): + icon = Icon(icon_name=icon_name) + self.set_icon_widget(icon) + icon.show() + + def create_palette(self): + return None + + def get_palette(self): + return self._palette_invoker.palette + + def set_palette(self, palette): + self._palette_invoker.palette = palette + + palette = gobject.property( + type=object, setter=set_palette, getter=get_palette) + + def get_palette_invoker(self): + return self._palette_invoker + + def set_palette_invoker(self, palette_invoker): + self._palette_invoker.detach() + self._palette_invoker = palette_invoker + + palette_invoker = gobject.property( + type=object, setter=set_palette_invoker, getter=get_palette_invoker) + + def do_expose_event(self, event): + child = self.get_child() + allocation = self.get_allocation() + if self.palette and self.palette.is_up(): + invoker = self.palette.props.invoker + invoker.draw_rectangle(event, self.palette) + elif child.state == gtk.STATE_PRELIGHT: + child.style.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_NONE, event.area, + child, "toolbutton-prelight", + allocation.x, allocation.y, + allocation.width, allocation.height) + + gtk.ToolButton.do_expose_event(self, event) + + def do_clicked(self): + if self.palette: + self.palette.popdown(True) diff --git a/toolkit/src/sugar/graphics/toolcombobox.py b/toolkit/src/sugar/graphics/toolcombobox.py new file mode 100644 index 0000000..1b2fdb0 --- /dev/null +++ b/toolkit/src/sugar/graphics/toolcombobox.py @@ -0,0 +1,64 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 gtk +import gobject + +from sugar.graphics.combobox import ComboBox +from sugar.graphics import style + + +class ToolComboBox(gtk.ToolItem): + + __gproperties__ = { + 'label-text': (str, None, None, None, gobject.PARAM_WRITABLE), + } + + def __init__(self, combo=None, **kwargs): + self.label = None + self._label_text = '' + + gobject.GObject.__init__(self, **kwargs) + + self.set_border_width(style.DEFAULT_PADDING) + + hbox = gtk.HBox(False, style.DEFAULT_SPACING) + + self.label = gtk.Label(self._label_text) + hbox.pack_start(self.label, False) + self.label.show() + + if combo: + self.combo = combo + else: + self.combo = ComboBox() + + hbox.pack_start(self.combo) + self.combo.show() + + self.add(hbox) + hbox.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'label-text': + self._label_text = value + if self.label: + self.label.set_text(self._label_text) diff --git a/toolkit/src/sugar/graphics/tray.py b/toolkit/src/sugar/graphics/tray.py new file mode 100644 index 0000000..172123a --- /dev/null +++ b/toolkit/src/sugar/graphics/tray.py @@ -0,0 +1,468 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# 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 + +from sugar.graphics import style +from sugar.graphics.palette import ToolInvoker +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.icon import Icon + + +_PREVIOUS_PAGE = 0 +_NEXT_PAGE = 1 + + +class _TrayViewport(gtk.Viewport): + + __gproperties__ = { + 'scrollable': (bool, None, None, False, gobject.PARAM_READABLE), + 'can-scroll-prev': (bool, None, None, False, gobject.PARAM_READABLE), + 'can-scroll-next': (bool, None, None, False, gobject.PARAM_READABLE), + } + + def __init__(self, orientation): + self.orientation = orientation + self._scrollable = False + self._can_scroll_next = False + self._can_scroll_prev = False + + gobject.GObject.__init__(self) + + self.set_shadow_type(gtk.SHADOW_NONE) + + self.traybar = gtk.Toolbar() + self.traybar.set_orientation(orientation) + self.traybar.set_show_arrow(False) + self.add(self.traybar) + self.traybar.show() + + self.connect('size_allocate', self._size_allocate_cb) + + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + adj = self.get_hadjustment() + else: + adj = self.get_vadjustment() + adj.connect('changed', self._adjustment_changed_cb) + adj.connect('value-changed', self._adjustment_changed_cb) + + def scroll(self, direction): + if direction == _PREVIOUS_PAGE: + self._scroll_previous() + elif direction == _NEXT_PAGE: + self._scroll_next() + + def scroll_to_item(self, item): + """This function scrolls the viewport so that item will be visible.""" + assert item in self.traybar.get_children() + + # Get the allocation, and make sure that it is visible + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + adj = self.get_hadjustment() + start = item.allocation.x + stop = item.allocation.x + item.allocation.width + else: + adj = self.get_vadjustment() + start = item.allocation.y + stop = item.allocation.y + item.allocation.height + + if start < adj.value: + adj.value = start + elif stop > adj.value + adj.page_size: + adj.value = stop - adj.page_size + + def _scroll_next(self): + allocation = self.get_allocation() + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + adj = self.get_hadjustment() + new_value = adj.value + allocation.width + adj.value = min(new_value, adj.upper - allocation.width) + else: + adj = self.get_vadjustment() + new_value = adj.value + allocation.height + adj.value = min(new_value, adj.upper - allocation.height) + + def _scroll_previous(self): + allocation = self.get_allocation() + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + adj = self.get_hadjustment() + new_value = adj.value - allocation.width + adj.value = max(adj.lower, new_value) + else: + adj = self.get_vadjustment() + new_value = adj.value - allocation.height + adj.value = max(adj.lower, new_value) + + def do_size_request(self, requisition): + child_requisition = self.get_child().size_request() + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + requisition[0] = 0 + requisition[1] = child_requisition[1] + else: + requisition[0] = child_requisition[0] + requisition[1] = 0 + + def do_get_property(self, pspec): + if pspec.name == 'scrollable': + return self._scrollable + elif pspec.name == 'can-scroll-next': + return self._can_scroll_next + elif pspec.name == 'can-scroll-prev': + return self._can_scroll_prev + + def _size_allocate_cb(self, viewport, allocation): + bar_requisition = self.traybar.get_child_requisition() + if self.orientation == gtk.ORIENTATION_HORIZONTAL: + scrollable = bar_requisition[0] > allocation.width + else: + scrollable = bar_requisition[1] > allocation.height + + if scrollable != self._scrollable: + self._scrollable = scrollable + self.notify('scrollable') + + def _adjustment_changed_cb(self, adjustment): + if adjustment.value <= adjustment.lower: + can_scroll_prev = False + else: + can_scroll_prev = True + + if adjustment.value + adjustment.page_size >= adjustment.upper: + can_scroll_next = False + else: + can_scroll_next = True + + if can_scroll_prev != self._can_scroll_prev: + self._can_scroll_prev = can_scroll_prev + self.notify('can-scroll-prev') + + if can_scroll_next != self._can_scroll_next: + self._can_scroll_next = can_scroll_next + self.notify('can-scroll-next') + + +class _TrayScrollButton(ToolButton): + + def __init__(self, icon_name, scroll_direction): + ToolButton.__init__(self) + self._viewport = None + + self._scroll_direction = scroll_direction + + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + + 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(self.icon) + self.set_icon_widget(alignment) + alignment.show_all() + + self.connect('clicked', self._clicked_cb) + + def set_viewport(self, viewport): + self._viewport = viewport + self._viewport.connect('notify::scrollable', + self._viewport_scrollable_changed_cb) + + if self._scroll_direction == _PREVIOUS_PAGE: + self._viewport.connect('notify::can-scroll-prev', + self._viewport_can_scroll_dir_changed_cb) + self.set_sensitive(self._viewport.props.can_scroll_prev) + else: + self._viewport.connect('notify::can-scroll-next', + self._viewport_can_scroll_dir_changed_cb) + self.set_sensitive(self._viewport.props.can_scroll_next) + + def _viewport_scrollable_changed_cb(self, viewport, pspec): + self.props.visible = self._viewport.props.scrollable + + def _viewport_can_scroll_dir_changed_cb(self, viewport, pspec): + if self._scroll_direction == _PREVIOUS_PAGE: + sensitive = self._viewport.props.can_scroll_prev + else: + sensitive = self._viewport.props.can_scroll_next + + self.set_sensitive(sensitive) + + def _clicked_cb(self, button): + self._viewport.scroll(self._scroll_direction) + + viewport = property(fset=set_viewport) + + +ALIGN_TO_START = 0 +ALIGN_TO_END = 1 + + +class HTray(gtk.HBox): + + __gtype_name__ = 'SugarHTray' + + __gproperties__ = { + 'align': (int, None, None, 0, 1, ALIGN_TO_START, + gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY), + 'drag-active': (bool, None, None, False, gobject.PARAM_READWRITE), + } + + def __init__(self, **kwargs): + self._drag_active = False + self.align = ALIGN_TO_START + + gobject.GObject.__init__(self, **kwargs) + + scroll_left = _TrayScrollButton('go-left', _PREVIOUS_PAGE) + self.pack_start(scroll_left, False) + + self._viewport = _TrayViewport(gtk.ORIENTATION_HORIZONTAL) + self.pack_start(self._viewport) + self._viewport.show() + + scroll_right = _TrayScrollButton('go-right', _NEXT_PAGE) + self.pack_start(scroll_right, False) + + scroll_left.viewport = self._viewport + scroll_right.viewport = self._viewport + + if self.align == ALIGN_TO_END: + spacer = gtk.SeparatorToolItem() + spacer.set_size_request(0, 0) + spacer.props.draw = False + spacer.set_expand(True) + self._viewport.traybar.insert(spacer, 0) + spacer.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'align': + self.align = value + elif pspec.name == 'drag-active': + self._set_drag_active(value) + else: + raise AssertionError + + def do_get_property(self, pspec): + if pspec.name == 'align': + return self.align + elif pspec.name == 'drag-active': + return self._drag_active + else: + raise AssertionError + + def _set_drag_active(self, active): + if self._drag_active != active: + self._drag_active = active + if self._drag_active: + self._viewport.traybar.modify_bg(gtk.STATE_NORMAL, + style.COLOR_BLACK.get_gdk_color()) + else: + self._viewport.traybar.modify_bg(gtk.STATE_NORMAL, None) + + def get_children(self): + children = self._viewport.traybar.get_children()[:] + if self.align == ALIGN_TO_END: + children = children[1:] + return children + + def add_item(self, item, index=-1): + if self.align == ALIGN_TO_END and index > -1: + index += 1 + self._viewport.traybar.insert(item, index) + + def remove_item(self, item): + self._viewport.traybar.remove(item) + + def get_item_index(self, item): + index = self._viewport.traybar.get_item_index(item) + if self.align == ALIGN_TO_END: + index -= 1 + return index + + def scroll_to_item(self, item): + self._viewport.scroll_to_item(item) + + +class VTray(gtk.VBox): + + __gtype_name__ = 'SugarVTray' + + __gproperties__ = { + 'align': (int, None, None, 0, 1, ALIGN_TO_START, + gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY), + 'drag-active': (bool, None, None, False, gobject.PARAM_READWRITE), + } + + def __init__(self, **kwargs): + self._drag_active = False + self.align = ALIGN_TO_START + + gobject.GObject.__init__(self, **kwargs) + + scroll_up = _TrayScrollButton('go-up', _PREVIOUS_PAGE) + self.pack_start(scroll_up, False) + + self._viewport = _TrayViewport(gtk.ORIENTATION_VERTICAL) + self.pack_start(self._viewport) + self._viewport.show() + + scroll_down = _TrayScrollButton('go-down', _NEXT_PAGE) + self.pack_start(scroll_down, False) + + scroll_up.viewport = self._viewport + scroll_down.viewport = self._viewport + + if self.align == ALIGN_TO_END: + spacer = gtk.SeparatorToolItem() + spacer.set_size_request(0, 0) + spacer.props.draw = False + spacer.set_expand(True) + self._viewport.traybar.insert(spacer, 0) + spacer.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'align': + self.align = value + elif pspec.name == 'drag-active': + self._set_drag_active(value) + else: + raise AssertionError + + def do_get_property(self, pspec): + if pspec.name == 'align': + return self.align + elif pspec.name == 'drag-active': + return self._drag_active + else: + raise AssertionError + + def _set_drag_active(self, active): + if self._drag_active != active: + self._drag_active = active + if self._drag_active: + self._viewport.traybar.modify_bg(gtk.STATE_NORMAL, + style.COLOR_BLACK.get_gdk_color()) + else: + self._viewport.traybar.modify_bg(gtk.STATE_NORMAL, None) + + def get_children(self): + children = self._viewport.traybar.get_children()[:] + if self.align == ALIGN_TO_END: + children = children[1:] + return children + + def add_item(self, item, index=-1): + if self.align == ALIGN_TO_END and index > -1: + index += 1 + self._viewport.traybar.insert(item, index) + + def remove_item(self, item): + self._viewport.traybar.remove(item) + + def get_item_index(self, item): + index = self._viewport.traybar.get_item_index(item) + if self.align == ALIGN_TO_END: + index -= 1 + return index + + def scroll_to_item(self, item): + self._viewport.scroll_to_item(item) + + +class TrayButton(ToolButton): + + def __init__(self, **kwargs): + ToolButton.__init__(self, **kwargs) + + +class _IconWidget(gtk.EventBox): + + __gtype_name__ = "SugarTrayIconWidget" + + def __init__(self, icon_name=None, xo_color=None): + gtk.EventBox.__init__(self) + + self.set_app_paintable(True) + + self._icon = Icon(icon_name=icon_name, xo_color=xo_color, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + self.add(self._icon) + self._icon.show() + + def do_expose_event(self, event): + palette = self.parent.palette + if palette and palette.is_up(): + invoker = palette.props.invoker + invoker.draw_rectangle(event, palette) + + gtk.EventBox.do_expose_event(self, event) + + def get_icon(self): + return self._icon + + +class TrayIcon(gtk.ToolItem): + + __gtype_name__ = "SugarTrayIcon" + + def __init__(self, icon_name=None, xo_color=None): + gtk.ToolItem.__init__(self) + + self._icon_widget = _IconWidget(icon_name, xo_color) + self.add(self._icon_widget) + self._icon_widget.show() + + self._palette_invoker = ToolInvoker(self) + + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, icon): + if self._palette_invoker is not None: + self._palette_invoker.detach() + + def create_palette(self): + return None + + def get_palette(self): + return self._palette_invoker.palette + + def set_palette(self, palette): + self._palette_invoker.palette = palette + + palette = gobject.property( + type=object, setter=set_palette, getter=get_palette) + + def get_palette_invoker(self): + return self._palette_invoker + + def set_palette_invoker(self, palette_invoker): + self._palette_invoker.detach() + self._palette_invoker = palette_invoker + + palette_invoker = gobject.property( + type=object, setter=set_palette_invoker, getter=get_palette_invoker) + + def get_icon(self): + return self._icon_widget.get_icon() + icon = property(get_icon, None) diff --git a/toolkit/src/sugar/graphics/window.py b/toolkit/src/sugar/graphics/window.py new file mode 100644 index 0000000..e3bef6b --- /dev/null +++ b/toolkit/src/sugar/graphics/window.py @@ -0,0 +1,297 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2009, Aleksey Lim, Sayamindu Dasgupta +# +# 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 +import warnings + +from sugar.graphics.icon import Icon +from sugar.graphics import palettegroup + + +_UNFULLSCREEN_BUTTON_VISIBILITY_TIMEOUT = 2 + + +class UnfullscreenButton(gtk.Window): + + def __init__(self): + gtk.Window.__init__(self) + + self.set_decorated(False) + self.set_resizable(False) + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + + self.set_border_width(0) + + self.props.accept_focus = False + + #Setup estimate of width, height + w, h = gtk.icon_size_lookup(gtk.ICON_SIZE_LARGE_TOOLBAR) + self._width = w + self._height = h + + self.connect('size-request', self._size_request_cb) + + screen = self.get_screen() + screen.connect('size-changed', self._screen_size_changed_cb) + + self._button = gtk.Button() + self._button.set_relief(gtk.RELIEF_NONE) + + self._icon = Icon(icon_name='view-return', + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + self._icon.show() + self._button.add(self._icon) + + self._button.show() + self.add(self._button) + + def connect_button_press(self, cb): + self._button.connect('button-press-event', cb) + + def _reposition(self): + x = gtk.gdk.screen_width() - self._width + self.move(x, 0) + + def _size_request_cb(self, widget, req): + self._width = req.width + self._height = req.height + self._reposition() + + def _screen_size_changed_cb(self, screen): + self._reposition() + + +class Window(gtk.Window): + + def __init__(self, **args): + self._enable_fullscreen_mode = True + + gtk.Window.__init__(self, **args) + + self.connect('realize', self.__window_realize_cb) + self.connect('key-press-event', self.__key_press_cb) + + self._toolbar_box = None + self._alerts = [] + self._canvas = None + self.tray = None + + self.__vbox = gtk.VBox() + self.__hbox = gtk.HBox() + self.__vbox.pack_start(self.__hbox) + self.__hbox.show() + + self._event_box = gtk.EventBox() + self.__hbox.pack_start(self._event_box) + self._event_box.show() + self._event_box.add_events(gtk.gdk.POINTER_MOTION_HINT_MASK + | gtk.gdk.POINTER_MOTION_MASK) + self._event_box.connect('motion-notify-event', self.__motion_notify_cb) + + self.add(self.__vbox) + self.__vbox.show() + + self._is_fullscreen = False + self._unfullscreen_button = UnfullscreenButton() + self._unfullscreen_button.set_transient_for(self) + self._unfullscreen_button.connect_button_press( + self.__unfullscreen_button_pressed) + self._unfullscreen_button_timeout_id = None + + def reveal(self): + """ Make window active + + In contrast with present(), brings window to the top + even after invoking on response on non-gtk events. + See #1423. + """ + if self.window is None: + self.show() + return + timestamp = gtk.get_current_event_time() + if not timestamp: + timestamp = gtk.gdk.x11_get_server_time(self.window) + self.window.focus(timestamp) + + def fullscreen(self): + palettegroup.popdown_all() + if self._toolbar_box is not None: + self._toolbar_box.hide() + if self.tray is not None: + self.tray.hide() + + self._is_fullscreen = True + + if self.props.enable_fullscreen_mode: + self._unfullscreen_button.show() + + if self._unfullscreen_button_timeout_id is not None: + gobject.source_remove(self._unfullscreen_button_timeout_id) + self._unfullscreen_button_timeout_id = None + + self._unfullscreen_button_timeout_id = \ + gobject.timeout_add_seconds( \ + _UNFULLSCREEN_BUTTON_VISIBILITY_TIMEOUT, \ + self.__unfullscreen_button_timeout_cb) + + def unfullscreen(self): + if self._toolbar_box is not None: + self._toolbar_box.show() + if self.tray is not None: + self.tray.show() + + self._is_fullscreen = False + + if self.props.enable_fullscreen_mode: + self._unfullscreen_button.hide() + + if self._unfullscreen_button_timeout_id: + gobject.source_remove(self._unfullscreen_button_timeout_id) + self._unfullscreen_button_timeout_id = None + + def set_canvas(self, canvas): + if self._canvas: + self._event_box.remove(self._canvas) + + if canvas: + self._event_box.add(canvas) + + self._canvas = canvas + self.__vbox.set_focus_child(self._canvas) + + def get_canvas(self): + return self._canvas + + canvas = property(get_canvas, set_canvas) + + def get_toolbar_box(self): + return self._toolbar_box + + def set_toolbar_box(self, toolbar_box): + if self._toolbar_box: + self.__vbox.remove(self._toolbar_box) + + self.__vbox.pack_start(toolbar_box, False) + self.__vbox.reorder_child(toolbar_box, 0) + + self._toolbar_box = toolbar_box + + toolbar_box = property(get_toolbar_box, set_toolbar_box) + + def set_tray(self, tray, position): + if self.tray: + box = self.tray.get_parent() + box.remove(self.tray) + + if position == gtk.POS_LEFT: + self.__hbox.pack_start(tray, False) + elif position == gtk.POS_RIGHT: + self.__hbox.pack_end(tray, False) + elif position == gtk.POS_BOTTOM: + self.__vbox.pack_end(tray, False) + + self.tray = tray + + def add_alert(self, alert): + self._alerts.append(alert) + if len(self._alerts) == 1: + self.__vbox.pack_start(alert, False) + if self._toolbar_box is not None: + self.__vbox.reorder_child(alert, 1) + else: + self.__vbox.reorder_child(alert, 0) + + def remove_alert(self, alert): + if alert in self._alerts: + self._alerts.remove(alert) + # if the alert is the visible one on top of the queue + if alert.get_parent() is not None: + self.__vbox.remove(alert) + if len(self._alerts) >= 1: + self.__vbox.pack_start(self._alerts[0], False) + if self._toolbar_box is not None: + self.__vbox.reorder_child(self._alerts[0], 1) + else: + self.__vbox.reorder_child(self._alert[0], 0) + + def __window_realize_cb(self, window): + group = gtk.Window() + group.realize() + window.window.set_group(group.window) + + def __key_press_cb(self, widget, event): + key = gtk.gdk.keyval_name(event.keyval) + if event.state & gtk.gdk.MOD1_MASK: + if self.tray is not None and key == 'space': + self.tray.props.visible = not self.tray.props.visible + return True + elif key == 'Escape' and self._is_fullscreen and \ + self.props.enable_fullscreen_mode: + self.unfullscreen() + return True + return False + + def __unfullscreen_button_pressed(self, widget, event): + self.unfullscreen() + + def __motion_notify_cb(self, widget, event): + if self._is_fullscreen and self.props.enable_fullscreen_mode: + if not self._unfullscreen_button.props.visible: + self._unfullscreen_button.show() + else: + # Reset the timer + if self._unfullscreen_button_timeout_id is not None: + gobject.source_remove(self._unfullscreen_button_timeout_id) + self._unfullscreen_button_timeout_id = None + + self._unfullscreen_button_timeout_id = \ + gobject.timeout_add_seconds( \ + _UNFULLSCREEN_BUTTON_VISIBILITY_TIMEOUT, \ + self.__unfullscreen_button_timeout_cb) + return False + + def __unfullscreen_button_timeout_cb(self): + self._unfullscreen_button.hide() + self._unfullscreen_button_timeout_id = None + return False + + def set_enable_fullscreen_mode(self, enable_fullscreen_mode): + self._enable_fullscreen_mode = enable_fullscreen_mode + + def get_enable_fullscreen_mode(self): + return self._enable_fullscreen_mode + + enable_fullscreen_mode = gobject.property(type=object, + setter=set_enable_fullscreen_mode, getter=get_enable_fullscreen_mode) + + # DEPRECATED + + def set_toolbox(self, toolbar_box): + warnings.warn('use toolbar_box instead of toolbox', DeprecationWarning) + self.set_toolbar_box(toolbar_box) + + def get_toolbox(self): + warnings.warn('use toolbar_box instead of toolbox', DeprecationWarning) + return self._toolbar_box + + toolbox = property(get_toolbox, set_toolbox) diff --git a/toolkit/src/sugar/graphics/xocolor.py b/toolkit/src/sugar/graphics/xocolor.py new file mode 100644 index 0000000..fd329cb --- /dev/null +++ b/toolkit/src/sugar/graphics/xocolor.py @@ -0,0 +1,278 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 random +import logging + +import gconf + +colors = [ +['#B20008', '#FF2B34'], \ +['#FF2B34', '#B20008'], \ +['#E6000A', '#FF2B34'], \ +['#FF2B34', '#E6000A'], \ +['#FFADCE', '#FF2B34'], \ +['#9A5200', '#FF2B34'], \ +['#FF2B34', '#9A5200'], \ +['#FF8F00', '#FF2B34'], \ +['#FF2B34', '#FF8F00'], \ +['#FFC169', '#FF2B34'], \ +['#807500', '#FF2B34'], \ +['#FF2B34', '#807500'], \ +['#BE9E00', '#FF2B34'], \ +['#FF2B34', '#BE9E00'], \ +['#F8E800', '#FF2B34'], \ +['#008009', '#FF2B34'], \ +['#FF2B34', '#008009'], \ +['#00B20D', '#FF2B34'], \ +['#FF2B34', '#00B20D'], \ +['#8BFF7A', '#FF2B34'], \ +['#00588C', '#FF2B34'], \ +['#FF2B34', '#00588C'], \ +['#005FE4', '#FF2B34'], \ +['#FF2B34', '#005FE4'], \ +['#BCCDFF', '#FF2B34'], \ +['#5E008C', '#FF2B34'], \ +['#FF2B34', '#5E008C'], \ +['#7F00BF', '#FF2B34'], \ +['#FF2B34', '#7F00BF'], \ +['#D1A3FF', '#FF2B34'], \ +['#9A5200', '#FF8F00'], \ +['#FF8F00', '#9A5200'], \ +['#C97E00', '#FF8F00'], \ +['#FF8F00', '#C97E00'], \ +['#FFC169', '#FF8F00'], \ +['#807500', '#FF8F00'], \ +['#FF8F00', '#807500'], \ +['#BE9E00', '#FF8F00'], \ +['#FF8F00', '#BE9E00'], \ +['#F8E800', '#FF8F00'], \ +['#008009', '#FF8F00'], \ +['#FF8F00', '#008009'], \ +['#00B20D', '#FF8F00'], \ +['#FF8F00', '#00B20D'], \ +['#8BFF7A', '#FF8F00'], \ +['#00588C', '#FF8F00'], \ +['#FF8F00', '#00588C'], \ +['#005FE4', '#FF8F00'], \ +['#FF8F00', '#005FE4'], \ +['#BCCDFF', '#FF8F00'], \ +['#5E008C', '#FF8F00'], \ +['#FF8F00', '#5E008C'], \ +['#A700FF', '#FF8F00'], \ +['#FF8F00', '#A700FF'], \ +['#D1A3FF', '#FF8F00'], \ +['#B20008', '#FF8F00'], \ +['#FF8F00', '#B20008'], \ +['#FF2B34', '#FF8F00'], \ +['#FF8F00', '#FF2B34'], \ +['#FFADCE', '#FF8F00'], \ +['#807500', '#F8E800'], \ +['#F8E800', '#807500'], \ +['#BE9E00', '#F8E800'], \ +['#F8E800', '#BE9E00'], \ +['#FFFA00', '#EDDE00'], \ +['#008009', '#F8E800'], \ +['#F8E800', '#008009'], \ +['#00EA11', '#F8E800'], \ +['#F8E800', '#00EA11'], \ +['#8BFF7A', '#F8E800'], \ +['#00588C', '#F8E800'], \ +['#F8E800', '#00588C'], \ +['#00A0FF', '#F8E800'], \ +['#F8E800', '#00A0FF'], \ +['#BCCEFF', '#F8E800'], \ +['#5E008C', '#F8E800'], \ +['#F8E800', '#5E008C'], \ +['#AC32FF', '#F8E800'], \ +['#F8E800', '#AC32FF'], \ +['#D1A3FF', '#F8E800'], \ +['#B20008', '#F8E800'], \ +['#F8E800', '#B20008'], \ +['#FF2B34', '#F8E800'], \ +['#F8E800', '#FF2B34'], \ +['#FFADCE', '#F8E800'], \ +['#9A5200', '#F8E800'], \ +['#F8E800', '#9A5200'], \ +['#FF8F00', '#F8E800'], \ +['#F8E800', '#FF8F00'], \ +['#FFC169', '#F8E800'], \ +['#008009', '#00EA11'], \ +['#00EA11', '#008009'], \ +['#00B20D', '#00EA11'], \ +['#00EA11', '#00B20D'], \ +['#8BFF7A', '#00EA11'], \ +['#00588C', '#00EA11'], \ +['#00EA11', '#00588C'], \ +['#005FE4', '#00EA11'], \ +['#00EA11', '#005FE4'], \ +['#BCCDFF', '#00EA11'], \ +['#5E008C', '#00EA11'], \ +['#00EA11', '#5E008C'], \ +['#7F00BF', '#00EA11'], \ +['#00EA11', '#7F00BF'], \ +['#D1A3FF', '#00EA11'], \ +['#B20008', '#00EA11'], \ +['#00EA11', '#B20008'], \ +['#FF2B34', '#00EA11'], \ +['#00EA11', '#FF2B34'], \ +['#FFADCE', '#00EA11'], \ +['#9A5200', '#00EA11'], \ +['#00EA11', '#9A5200'], \ +['#FF8F00', '#00EA11'], \ +['#00EA11', '#FF8F00'], \ +['#FFC169', '#00EA11'], \ +['#807500', '#00EA11'], \ +['#00EA11', '#807500'], \ +['#BE9E00', '#00EA11'], \ +['#00EA11', '#BE9E00'], \ +['#F8E800', '#00EA11'], \ +['#00588C', '#00A0FF'], \ +['#00A0FF', '#00588C'], \ +['#005FE4', '#00A0FF'], \ +['#00A0FF', '#005FE4'], \ +['#BCCDFF', '#00A0FF'], \ +['#5E008C', '#00A0FF'], \ +['#00A0FF', '#5E008C'], \ +['#9900E6', '#00A0FF'], \ +['#00A0FF', '#9900E6'], \ +['#D1A3FF', '#00A0FF'], \ +['#B20008', '#00A0FF'], \ +['#00A0FF', '#B20008'], \ +['#FF2B34', '#00A0FF'], \ +['#00A0FF', '#FF2B34'], \ +['#FFADCE', '#00A0FF'], \ +['#9A5200', '#00A0FF'], \ +['#00A0FF', '#9A5200'], \ +['#FF8F00', '#00A0FF'], \ +['#00A0FF', '#FF8F00'], \ +['#FFC169', '#00A0FF'], \ +['#807500', '#00A0FF'], \ +['#00A0FF', '#807500'], \ +['#BE9E00', '#00A0FF'], \ +['#00A0FF', '#BE9E00'], \ +['#F8E800', '#00A0FF'], \ +['#008009', '#00A0FF'], \ +['#00A0FF', '#008009'], \ +['#00B20D', '#00A0FF'], \ +['#00A0FF', '#00B20D'], \ +['#8BFF7A', '#00A0FF'], \ +['#5E008C', '#AC32FF'], \ +['#AC32FF', '#5E008C'], \ +['#7F00BF', '#AC32FF'], \ +['#AC32FF', '#7F00BF'], \ +['#D1A3FF', '#AC32FF'], \ +['#B20008', '#AC32FF'], \ +['#AC32FF', '#B20008'], \ +['#FF2B34', '#AC32FF'], \ +['#AC32FF', '#FF2B34'], \ +['#FFADCE', '#AC32FF'], \ +['#9A5200', '#AC32FF'], \ +['#AC32FF', '#9A5200'], \ +['#FF8F00', '#AC32FF'], \ +['#AC32FF', '#FF8F00'], \ +['#FFC169', '#AC32FF'], \ +['#807500', '#AC32FF'], \ +['#AC32FF', '#807500'], \ +['#BE9E00', '#AC32FF'], \ +['#AC32FF', '#BE9E00'], \ +['#F8E800', '#AC32FF'], \ +['#008009', '#AC32FF'], \ +['#AC32FF', '#008009'], \ +['#00B20D', '#AC32FF'], \ +['#AC32FF', '#00B20D'], \ +['#8BFF7A', '#AC32FF'], \ +['#00588C', '#AC32FF'], \ +['#AC32FF', '#00588C'], \ +['#005FE4', '#AC32FF'], \ +['#AC32FF', '#005FE4'], \ +['#BCCDFF', '#AC32FF'], \ +] + + +def _parse_string(color_string): + if color_string == 'white': + return ['#ffffff', '#414141'] + elif color_string == 'insensitive': + return ['#ffffff', '#e2e2e2'] + + splitted = color_string.split(',') + if len(splitted) == 2: + return [splitted[0], splitted[1]] + else: + return None + + +def is_valid(color_string): + return (_parse_string(color_string) != None) + + +class XoColor: + + def __init__(self, color_string=None): + if color_string == None: + randomize = True + elif not is_valid(color_string): + logging.debug('Color string is not valid: %s, ' + 'fallback to default', color_string) + client = gconf.client_get_default() + color_string = client.get_string('/desktop/sugar/user/color') + randomize = False + else: + randomize = False + + if randomize: + n = int(random.random() * (len(colors) - 1)) + [self.stroke, self.fill] = colors[n] + else: + [self.stroke, self.fill] = _parse_string(color_string) + + def __cmp__(self, other): + if isinstance(other, XoColor): + if self.stroke == other.stroke and self.fill == other.fill: + return 0 + return -1 + + def get_stroke_color(self): + return self.stroke + + def get_fill_color(self): + return self.fill + + def to_string(self): + return '%s,%s' % (self.stroke, self.fill) + + +if __name__ == "__main__": + import sys + import re + + f = open(sys.argv[1], 'r') + + print 'colors = [' + + for line in f.readlines(): + match = re.match(r'fill: ([A-Z0-9]*) stroke: ([A-Z0-9]*)', line) + print "['#%s', '#%s'], \\" % (match.group(2), match.group(1)) + + print ']' + + f.close() |