From eaef567cabc5c0dda9706faa0e17e20909131ade Mon Sep 17 00:00:00 2001 From: simpoir Date: Fri, 30 Jan 2009 04:14:48 +0000 Subject: ajout initial du tree jhbuild [jhbuild base] --- (limited to 'src/sugar/graphics') 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("" + self._title + "") + 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 +# +# 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, value) + icon = re.sub('' % 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 +# +# 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 +# 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('%s' % 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() -- cgit v0.9.1