Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/sugar/graphics
diff options
context:
space:
mode:
Diffstat (limited to 'src/sugar/graphics')
-rw-r--r--src/sugar/graphics/Makefile.am27
-rw-r--r--src/sugar/graphics/__init__.py18
-rw-r--r--src/sugar/graphics/alert.py436
-rw-r--r--src/sugar/graphics/animator.py148
-rw-r--r--src/sugar/graphics/canvastextview.py39
-rw-r--r--src/sugar/graphics/colorbutton.py526
-rw-r--r--src/sugar/graphics/combobox.py168
-rw-r--r--src/sugar/graphics/entry.py39
-rw-r--r--src/sugar/graphics/icon.py872
-rw-r--r--src/sugar/graphics/iconentry.py106
-rw-r--r--src/sugar/graphics/menuitem.py94
-rw-r--r--src/sugar/graphics/notebook.py150
-rw-r--r--src/sugar/graphics/objectchooser.py130
-rw-r--r--src/sugar/graphics/palette.py1124
-rw-r--r--src/sugar/graphics/palettegroup.py95
-rw-r--r--src/sugar/graphics/panel.py27
-rw-r--r--src/sugar/graphics/radiotoolbutton.py180
-rw-r--r--src/sugar/graphics/roundbox.py70
-rw-r--r--src/sugar/graphics/style.py133
-rw-r--r--src/sugar/graphics/toggletoolbutton.py89
-rw-r--r--src/sugar/graphics/toolbox.py101
-rw-r--r--src/sugar/graphics/toolbutton.py158
-rw-r--r--src/sugar/graphics/toolcombobox.py63
-rw-r--r--src/sugar/graphics/tray.py461
-rw-r--r--src/sugar/graphics/window.py216
-rw-r--r--src/sugar/graphics/xocolor.py259
26 files changed, 5729 insertions, 0 deletions
diff --git a/src/sugar/graphics/Makefile.am b/src/sugar/graphics/Makefile.am
new file mode 100644
index 0000000..c4d5e61
--- /dev/null
+++ b/src/sugar/graphics/Makefile.am
@@ -0,0 +1,27 @@
+sugardir = $(pythondir)/sugar/graphics
+sugar_PYTHON = \
+ __init__.py \
+ alert.py \
+ animator.py \
+ canvastextview.py \
+ combobox.py \
+ colorbutton.py \
+ entry.py \
+ icon.py \
+ iconentry.py \
+ menuitem.py \
+ notebook.py \
+ objectchooser.py \
+ radiotoolbutton.py \
+ palette.py \
+ palettegroup.py \
+ panel.py \
+ roundbox.py \
+ style.py \
+ toggletoolbutton.py \
+ toolbox.py \
+ toolbutton.py \
+ toolcombobox.py \
+ tray.py \
+ window.py \
+ xocolor.py
diff --git a/src/sugar/graphics/__init__.py b/src/sugar/graphics/__init__.py
new file mode 100644
index 0000000..1e7e0f9
--- /dev/null
+++ b/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/src/sugar/graphics/alert.py b/src/sugar/graphics/alert.py
new file mode 100644
index 0000000..2c3c4ca
--- /dev/null
+++ b/src/sugar/graphics/alert.py
@@ -0,0 +1,436 @@
+"""
+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
+#
+# 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 _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/src/sugar/graphics/animator.py b/src/sugar/graphics/animator.py
new file mode 100644
index 0000000..5d5b355
--- /dev/null
+++ b/src/sugar/graphics/animator.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.
+
+"""
+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/src/sugar/graphics/canvastextview.py b/src/sugar/graphics/canvastextview.py
new file mode 100644
index 0000000..481248d
--- /dev/null
+++ b/src/sugar/graphics/canvastextview.py
@@ -0,0 +1,39 @@
+# 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/src/sugar/graphics/colorbutton.py b/src/sugar/graphics/colorbutton.py
new file mode 100644
index 0000000..44f9f69
--- /dev/null
+++ b/src/sugar/graphics/colorbutton.py
@@ -0,0 +1,526 @@
+# 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:
+ self._palette.popup(immediate=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)
+
+ # Drag and Drop
+ def __drag_begin_cb(self, widget, context):
+ 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)
+
+# 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.)
+class ColorToolButton(gtk.ToolItem):
+ __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/src/sugar/graphics/combobox.py b/src/sugar/graphics/combobox.py
new file mode 100644
index 0000000..4e094ab
--- /dev/null
+++ b/src/sugar/graphics/combobox.py
@@ -0,0 +1,168 @@
+# 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/src/sugar/graphics/entry.py b/src/sugar/graphics/entry.py
new file mode 100644
index 0000000..62975da
--- /dev/null
+++ b/src/sugar/graphics/entry.py
@@ -0,0 +1,39 @@
+# 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/src/sugar/graphics/icon.py b/src/sugar/graphics/icon.py
new file mode 100644
index 0000000..1608bac
--- /dev/null
+++ b/src/sugar/graphics/icon.py
@@ -0,0 +1,872 @@
+# 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.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):
+ return (self.icon_name, self.file_name, self.fill_color,
+ self.stroke_color, self.badge_name, self.width, self.height,
+ 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)
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+
+ context = cairo.Context(surface)
+ 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()
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ 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 __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(0, 0, -1, -1)
+
+ 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(0, 0, -1, -1)
+
+ 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(0, 0, -1, -1)
+
+ 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_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=int, 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):
+ self.emit_activated()
+ return True
+
+ 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))
+
+ palette = property(get_palette, set_palette)
+
+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
diff --git a/src/sugar/graphics/iconentry.py b/src/sugar/graphics/iconentry.py
new file mode 100644
index 0000000..df38b9e
--- /dev/null
+++ b/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/src/sugar/graphics/menuitem.py b/src/sugar/graphics/menuitem.py
new file mode 100644
index 0000000..a357d78
--- /dev/null
+++ b/src/sugar/graphics/menuitem.py
@@ -0,0 +1,94 @@
+# 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=0,
+ 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/src/sugar/graphics/notebook.py b/src/sugar/graphics/notebook.py
new file mode 100644
index 0000000..4965b24
--- /dev/null
+++ b/src/sugar/graphics/notebook.py
@@ -0,0 +1,150 @@
+# 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/src/sugar/graphics/objectchooser.py b/src/sugar/graphics/objectchooser.py
new file mode 100644
index 0000000..fb3703d
--- /dev/null
+++ b/src/sugar/graphics/objectchooser.py
@@ -0,0 +1,130 @@
+# 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/src/sugar/graphics/palette.py b/src/sugar/graphics/palette.py
new file mode 100644
index 0000000..de5b8e0
--- /dev/null
+++ b/src/sugar/graphics/palette.py
@@ -0,0 +1,1124 @@
+# Copyright (C) 2007, Eduardo Silva <edsiper@gmail.com>
+# 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
+import hippo
+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 import _sugarext
+
+# Helper function to find the gap position and size of widget a
+def _calculate_gap(a, b):
+ # 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 Palette(gtk.Window):
+ PRIMARY = 0
+ SECONDARY = 1
+
+ __gtype_name__ = 'SugarPalette'
+
+ __gsignals__ = {
+ 'popup' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ 'popdown' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([]))
+ }
+
+ # DEPRECATED: label is passed with the primary-text property, accel_path
+ # is set via the invoker property, and menu_after_content is not used
+ def __init__(self, label=None, accel_path=None, menu_after_content=False,
+ text_maxlen=0, **kwargs):
+
+ self.palette_state = self.PRIMARY
+
+ self._primary_text = None
+ self._secondary_text = None
+ self._icon = None
+ self._icon_visible = True
+ self._group_id = None
+
+ 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.zoom(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_MIDDLE)
+
+ 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._popup_anim = animator.Animator(.5, 10)
+ self._popup_anim.add(_PopupAnimation(self))
+
+ self._secondary_anim = animator.Animator(2.0, 10)
+ self._secondary_anim.add(_SecondaryAnimation(self))
+
+ self._popdown_anim = animator.Animator(0.6, 10)
+ self._popdown_anim.add(_PopdownAnimation(self))
+
+ # we init after initializing all of our containers
+ 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)
+
+ primary_box.set_size_request(-1, style.zoom(style.GRID_CELL_SIZE)
+ - 2 * self.get_border_width())
+
+
+ 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._alignment = None
+ self._old_alloc = None
+ self._full_request = [0, 0]
+ self._cursor_x = 0
+ self._cursor_y = 0
+ self._invoker = None
+ self._group_id = None
+ self._up = False
+ self._menu_box = None
+ self._content = None
+ self._invoker_hids = []
+
+ self.set_group_id("default")
+
+ # 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('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 __menu_item_inserted_cb(self, menu):
+ self._update_separators()
+
+ def __destroy_cb(self, palette):
+ self.set_group_id(None)
+
+ # 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 _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 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 is_up(self):
+ return self._up
+
+ 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 = rectangle.width
+ height = rectangle.height
+
+ return gtk.gdk.Rectangle(x, y, width, height)
+
+ 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))
+ if hasattr(invoker.props, 'widget'):
+ self._update_accel_widget()
+ logging.debug(('Setup widget', invoker.props.widget))
+ self._invoker_hids.append(self._invoker.connect(
+ 'notify::widget', self._invoker_widget_changed_cb))
+
+ def get_invoker(self):
+ return self._invoker
+
+ invoker = gobject.property(type=object,
+ getter=get_invoker,
+ setter=set_invoker)
+
+ 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):
+ 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)
+
+ self._icon = icon
+ self._icon.props.icon_size = gtk.ICON_SIZE_LARGE_TOOLBAR
+ self._icon_box.pack_start(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 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 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)
+
+ # 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,
+ style.zoom(style.GRID_CELL_SIZE * 2),
+ label_width,
+ self._full_request[0])
+
+ 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_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.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ 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 _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 popup(self, immediate=False, state=None):
+ logging.debug('Palette.popup immediate %r' % immediate)
+
+ if state is None:
+ state = self.PRIMARY
+ self.set_state(state)
+
+ if self._invoker is not None:
+ self._update_full_request()
+ self._alignment = self._invoker.get_alignment(self._full_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.show()
+
+ self._secondary_anim.start()
+
+ def popdown(self, immediate=False):
+ logging.debug('Palette.popdown immediate %r' % immediate)
+ self._popup_anim.stop()
+
+ self._mouse_detector.stop()
+
+ if not immediate:
+ self._popdown_anim.start()
+ else:
+ self.hide()
+
+ def set_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
+
+ 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 _invoker_widget_changed_cb(self, invoker, spec):
+ self._update_accel_widget()
+
+ def _invoker_mouse_enter_cb(self, invoker):
+ self._mouse_detector.start()
+
+ def _invoker_mouse_leave_cb(self, invoker):
+ self._mouse_detector.stop()
+ self.popdown()
+
+ def _invoker_right_click_cb(self, invoker):
+ self.popup(immediate=True, state=self.SECONDARY)
+
+ def __enter_notify_event_cb(self, widget, event):
+ if event.detail != gtk.gdk.NOTIFY_INFERIOR and \
+ event.mode == gtk.gdk.CROSSING_NORMAL:
+ self._popdown_anim.stop()
+ self._secondary_anim.start()
+
+ def __leave_notify_event_cb(self, widget, event):
+ if event.detail != gtk.gdk.NOTIFY_INFERIOR and \
+ event.mode == gtk.gdk.CROSSING_NORMAL:
+ self.popdown()
+
+ def __show_cb(self, widget):
+ self.menu.set_active(True)
+
+ self._invoker.notify_popup()
+
+ self._up = True
+ self.emit('popup')
+
+ def __hide_cb(self, widget):
+ logging.debug('__hide_cb')
+ self.menu.set_active(False)
+
+ self._secondary_anim.stop()
+
+ if self._invoker:
+ self._invoker.notify_popdown()
+
+ self._up = False
+ self.emit('popdown')
+
+
+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 _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.show()
+
+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_state(Palette.SECONDARY)
+
+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.hide()
+
+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)
+ return self._get_position_for_alignment(alignment, palette_dim)
+
+ 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]
+
+ else:
+ 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:
+ 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:
+ 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
diff --git a/src/sugar/graphics/palettegroup.py b/src/sugar/graphics/palettegroup.py
new file mode 100644
index 0000000..e95b5aa
--- /dev/null
+++ b/src/sugar/graphics/palettegroup.py
@@ -0,0 +1,95 @@
+# 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
+
+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):
+ 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/src/sugar/graphics/panel.py b/src/sugar/graphics/panel.py
new file mode 100644
index 0000000..bc48db8
--- /dev/null
+++ b/src/sugar/graphics/panel.py
@@ -0,0 +1,27 @@
+# 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/src/sugar/graphics/radiotoolbutton.py b/src/sugar/graphics/radiotoolbutton.py
new file mode 100644
index 0000000..11f962d
--- /dev/null
+++ b/src/sugar/graphics/radiotoolbutton.py
@@ -0,0 +1,180 @@
+# 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/src/sugar/graphics/roundbox.py b/src/sugar/graphics/roundbox.py
new file mode 100644
index 0000000..28644bc
--- /dev/null
+++ b/src/sugar/graphics/roundbox.py
@@ -0,0 +1,70 @@
+# 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/src/sugar/graphics/style.py b/src/sugar/graphics/style.py
new file mode 100644
index 0000000..16b78ed
--- /dev/null
+++ b/src/sugar/graphics/style.py
@@ -0,0 +1,133 @@
+# 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
+
+_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)
+
+FONT_SIZE = zoom(7)
+FONT_NORMAL = Font('Bitstream Vera Sans %d' % FONT_SIZE)
+FONT_BOLD = Font('Bitstream Vera Sans bold %d' % 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')
+
+PALETTE_CURSOR_DISTANCE = zoom(10)
diff --git a/src/sugar/graphics/toggletoolbutton.py b/src/sugar/graphics/toggletoolbutton.py
new file mode 100644
index 0000000..9bb2f58
--- /dev/null
+++ b/src/sugar/graphics/toggletoolbutton.py
@@ -0,0 +1,89 @@
+# 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/src/sugar/graphics/toolbox.py b/src/sugar/graphics/toolbox.py
new file mode 100644
index 0000000..1cfbe93
--- /dev/null
+++ b/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/src/sugar/graphics/toolbutton.py b/src/sugar/graphics/toolbutton.py
new file mode 100644
index 0000000..6205b8a
--- /dev/null
+++ b/src/sugar/graphics/toolbutton.py
@@ -0,0 +1,158 @@
+# 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.connect('clicked', self.__button_clicked_cb)
+ 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 __button_clicked_cb(self, widget):
+ if self.palette:
+ self.palette.popdown(True)
+
diff --git a/src/sugar/graphics/toolcombobox.py b/src/sugar/graphics/toolcombobox.py
new file mode 100644
index 0000000..16e14bd
--- /dev/null
+++ b/src/sugar/graphics/toolcombobox.py
@@ -0,0 +1,63 @@
+# 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/src/sugar/graphics/tray.py b/src/sugar/graphics/tray.py
new file mode 100644
index 0000000..5df7170
--- /dev/null
+++ b/src/sugar/graphics/tray.py
@@ -0,0 +1,461 @@
+# 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/src/sugar/graphics/window.py b/src/sugar/graphics/window.py
new file mode 100644
index 0000000..1ad2bca
--- /dev/null
+++ b/src/sugar/graphics/window.py
@@ -0,0 +1,216 @@
+# 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
+
+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('window-state-event', self.__window_state_event_cb)
+ self.connect('key-press-event', self.__key_press_cb)
+
+ self.toolbox = 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.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)
+
+ def set_canvas(self, canvas):
+ if self.canvas:
+ self._event_box.remove(self.canvas)
+
+ if canvas:
+ self._event_box.add(canvas)
+
+ self.canvas = canvas
+
+ def set_toolbox(self, toolbox):
+ if self.toolbox:
+ self._vbox.remove(self.toolbox)
+
+ self._vbox.pack_start(toolbox, False)
+ self._vbox.reorder_child(toolbox, 0)
+
+ self.toolbox = toolbox
+
+ 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.toolbox 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.toolbox 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 __window_state_event_cb(self, window, event):
+ if not (event.changed_mask & gtk.gdk.WINDOW_STATE_FULLSCREEN):
+ return False
+
+ if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
+ if self.toolbox is not None:
+ self.toolbox.hide()
+ if self.tray is not None:
+ self.tray.hide()
+
+ self._is_fullscreen = True
+ if self.props.enable_fullscreen_mode:
+ self._unfullscreen_button.show()
+
+ else:
+ if self.toolbox is not None:
+ self.toolbox.show()
+ if self.tray is not None:
+ self.tray.show()
+
+ self._is_fullscreen = False
+ if self.props.enable_fullscreen_mode:
+ self._unfullscreen_button.hide()
+
+ def __key_press_cb(self, widget, event):
+ key = gtk.gdk.keyval_name(event.keyval)
+ if event.state & gtk.gdk.MOD1_MASK:
+ if 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 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)
+
diff --git a/src/sugar/graphics/xocolor.py b/src/sugar/graphics/xocolor.py
new file mode 100644
index 0000000..beb9565
--- /dev/null
+++ b/src/sugar/graphics/xocolor.py
@@ -0,0 +1,259 @@
+# 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
+
+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 or not is_valid(color_string):
+ 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()