diff --git a/examples/radiopalette.py b/examples/radiopalette.py new file mode 100644 index 00000000..85b43cef --- /dev/null +++ b/examples/radiopalette.py @@ -0,0 +1,74 @@ +import gtk + +from sugar.graphics.radiopalette import RadioPalette, RadioMenuButton, \ + RadioToolsButton +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics import style + +window = gtk.Window() + +box = gtk.VBox() +window.add(box) + +toolbar = gtk.Toolbar() +box.pack_start(toolbar, False) + +text_view = gtk.TextView() +box.pack_start(text_view) + +def echo(button, label): + if not button.props.active: + return + text_view.props.buffer.props.text += "\n" + label + +# RadioMenuButton + +palette = RadioPalette() + +group = RadioToolButton( + icon_name='document-open') +group.connect('clicked', lambda button: echo(button, 'document-open')) +palette.append(group, 'menu.document-open') + +button = RadioToolButton( + icon_name='document-save', + group=group) +button.connect('clicked', lambda button: echo(button, 'document-save')) +palette.append(button, 'menu.document-save') + +button = RadioToolButton( + icon_name='document-send', + group=group) +button.connect('clicked', lambda button: echo(button, 'document-send')) +palette.append(button, 'menu.document-send') + +button = RadioMenuButton(palette=palette) +toolbar.insert(button, -1) + +# RadioToolsButton + +palette = RadioPalette() + +group = RadioToolButton( + icon_name='document-open') +group.connect('clicked', lambda button: echo(button, 'document-open')) +palette.append(group, 'menu.document-open') + +button = RadioToolButton( + icon_name='document-save', + group=group) +button.connect('clicked', lambda button: echo(button, 'document-save')) +palette.append(button, 'menu.document-save') + +button = RadioToolButton( + icon_name='document-send', + group=group) +button.connect('clicked', lambda button: echo(button, 'document-send')) +palette.append(button, 'menu.document-send') + +button = RadioToolsButton(palette=palette) +toolbar.insert(button, -1) + +window.show_all() +gtk.main() diff --git a/examples/sugar b/examples/sugar new file mode 120000 index 00000000..12f3164b --- /dev/null +++ b/examples/sugar @@ -0,0 +1 @@ +../src/sugar/ \ No newline at end of file diff --git a/examples/toolbar.py b/examples/toolbar.py new file mode 100644 index 00000000..2faea1f8 --- /dev/null +++ b/examples/toolbar.py @@ -0,0 +1,50 @@ +import gtk + +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toolbarbox import ToolbarBox, ToolbarButton +from sugar.graphics import style + +window = gtk.Window() + +box = gtk.VBox() +window.add(box) + +toolbar = ToolbarBox() +box.pack_start(toolbar, False) + +tollbarbutton_1 = ToolbarButton( + page=gtk.Button('sub-widget #1'), + icon_name='computer-xo') +toolbar.toolbar.insert(tollbarbutton_1, -1) + +tollbarbutton_2 = ToolbarButton( + page=gtk.Button('sub-widget #2'), + icon_name='button_cancel', + tooltip='with custom palette instead of sub-widget') +toolbar.toolbar.insert(tollbarbutton_2, -1) + +toolbar.toolbar.insert(gtk.SeparatorToolItem(), -1) + +def del_cb(widget): + toolbar.toolbar.remove(tollbarbutton_3) +del_b = gtk.Button('delete sub-widget #3') +del_b.connect('clicked', del_cb) +tollbarbutton_3 = ToolbarButton( + page=del_b, + icon_name='activity-journal') +toolbar.toolbar.insert(tollbarbutton_3, -1) + +subbar = gtk.Toolbar() +subbutton = ToolButton( + icon_name='document-send', + tooltip='document-send') +subbar.insert(subbutton, -1) +subbar.show_all() + +tollbarbutton_4 = ToolbarButton( + page=subbar, + icon_name='document-save') +toolbar.toolbar.insert(tollbarbutton_4, -1) + +window.show_all() +gtk.main() diff --git a/m4/.gitignore b/m4/.gitignore index 9f841b0a..e08c7c8f 100644 --- a/m4/.gitignore +++ b/m4/.gitignore @@ -1 +1,3 @@ intltool.m4 +libtool.m4 +lt*.m4 diff --git a/src/sugar/__init__.py b/src/sugar/__init__.py new file mode 100644 index 00000000..44acb4d7 --- /dev/null +++ b/src/sugar/__init__.py @@ -0,0 +1,14 @@ +# 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/activity/Makefile.am b/src/sugar/activity/Makefile.am index 91f6ea8f..e2e6fdce 100644 --- a/src/sugar/activity/Makefile.am +++ b/src/sugar/activity/Makefile.am @@ -1,10 +1,11 @@ sugardir = $(pythondir)/sugar/activity -sugar_PYTHON = \ - __init__.py \ - activity.py \ - activityfactory.py \ - activityhandle.py \ - activityservice.py \ - bundlebuilder.py \ - main.py \ - namingalert.py \ No newline at end of file +sugar_PYTHON = \ + __init__.py \ + activity.py \ + activityfactory.py \ + activityhandle.py \ + activityservice.py \ + bundlebuilder.py \ + main.py \ + namingalert.py \ + widgets.py diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py index 9809928a..f20d402b 100644 --- a/src/sugar/activity/activity.py +++ b/src/sugar/activity/activity.py @@ -67,16 +67,15 @@ from sugar.activity.activityservice import ActivityService from sugar.activity.namingalert import NamingAlert from sugar.graphics import style from sugar.graphics.window import Window -from sugar.graphics.toolbox import Toolbox -from sugar.graphics.toolbutton import ToolButton -from sugar.graphics.toolcombobox import ToolComboBox from sugar.graphics.alert import Alert from sugar.graphics.icon import Icon -from sugar.graphics.xocolor import XoColor from sugar.datastore import datastore from sugar.session import XSMPClient from sugar import wm +# support deprecated imports +from sugar.activity.widgets import ActivityToolbar, EditToolbar, ActivityToolbox + _ = lambda msg: gettext.dgettext('sugar-toolkit', msg) SCOPE_PRIVATE = "private" @@ -87,226 +86,6 @@ J_DBUS_SERVICE = 'org.laptop.Journal' J_DBUS_PATH = '/org/laptop/Journal' J_DBUS_INTERFACE = 'org.laptop.Journal' -class ActivityToolbar(gtk.Toolbar): - """The Activity toolbar with the Journal entry title, sharing, - Keep and Stop buttons - - All activities should have this toolbar. It is easiest to add it to your - Activity by using the ActivityToolbox. - """ - def __init__(self, activity): - gtk.Toolbar.__init__(self) - - self._activity = activity - self._updating_share = False - - activity.connect('shared', self.__activity_shared_cb) - activity.connect('joined', self.__activity_shared_cb) - activity.connect('notify::max_participants', - self.__max_participants_changed_cb) - - if activity.metadata: - self.title = gtk.Entry() - self.title.set_size_request(int(gtk.gdk.screen_width() / 3), -1) - self.title.set_text(activity.metadata['title']) - self.title.connect('changed', self.__title_changed_cb) - self._add_widget(self.title) - - activity.metadata.connect('updated', self.__jobject_updated_cb) - - separator = gtk.SeparatorToolItem() - separator.props.draw = False - separator.set_expand(True) - self.insert(separator, -1) - separator.show() - - self.share = ToolComboBox(label_text=_('Share with:')) - self.share.combo.connect('changed', self.__share_changed_cb) - self.share.combo.append_item(SCOPE_PRIVATE, _('Private'), 'zoom-home') - self.share.combo.append_item(SCOPE_NEIGHBORHOOD, _('My Neighborhood'), - 'zoom-neighborhood') - self.insert(self.share, -1) - self.share.show() - - self._update_share() - - self.keep = ToolButton(tooltip=_('Keep')) - client = gconf.client_get_default() - color = XoColor(client.get_string('/desktop/sugar/user/color')) - keep_icon = Icon(icon_name='document-save', xo_color=color) - self.keep.set_icon_widget(keep_icon) - keep_icon.show() - self.keep.props.accelerator = 'S' - self.keep.connect('clicked', self.__keep_clicked_cb) - self.insert(self.keep, -1) - self.keep.show() - - self.stop = ToolButton('activity-stop', tooltip=_('Stop')) - self.stop.props.accelerator = 'Q' - self.stop.connect('clicked', self.__stop_clicked_cb) - self.insert(self.stop, -1) - self.stop.show() - - self._update_title_sid = None - - def _update_share(self): - self._updating_share = True - - if self._activity.props.max_participants == 1: - self.share.hide() - - if self._activity.get_shared(): - self.share.set_sensitive(False) - self.share.combo.set_active(1) - else: - self.share.set_sensitive(True) - self.share.combo.set_active(0) - - self._updating_share = False - - def __share_changed_cb(self, combo): - if self._updating_share: - return - - model = self.share.combo.get_model() - it = self.share.combo.get_active_iter() - (scope, ) = model.get(it, 0) - if scope == SCOPE_NEIGHBORHOOD: - self._activity.share() - - def __keep_clicked_cb(self, button): - self._activity.copy() - - def __stop_clicked_cb(self, button): - self._activity.close() - - def __jobject_updated_cb(self, jobject): - self.title.set_text(jobject['title']) - - def __title_changed_cb(self, entry): - if not self._update_title_sid: - self._update_title_sid = gobject.timeout_add_seconds( - 1, self.__update_title_cb) - - def __update_title_cb(self): - title = self.title.get_text() - - self._activity.metadata['title'] = title - self._activity.metadata['title_set_by_user'] = '1' - self._activity.save() - - shared_activity = self._activity.get_shared_activity() - if shared_activity: - shared_activity.props.name = title - - self._update_title_sid = None - return False - - def _add_widget(self, widget, expand=False): - tool_item = gtk.ToolItem() - tool_item.set_expand(expand) - - tool_item.add(widget) - widget.show() - - self.insert(tool_item, -1) - tool_item.show() - - def __activity_shared_cb(self, activity): - self._update_share() - - def __max_participants_changed_cb(self, activity, pspec): - self._update_share() - -class EditToolbar(gtk.Toolbar): - """Provides the standard edit toolbar for Activities. - - Members: - undo -- the undo button - redo -- the redo button - copy -- the copy button - paste -- the paste button - separator -- A separator between undo/redo and copy/paste - - This class only provides the 'edit' buttons in a standard layout, - your activity will need to either hide buttons which make no sense for your - Activity, or you need to connect the button events to your own callbacks: - - ## Example from Read.activity: - # Create the edit toolbar: - self._edit_toolbar = EditToolbar(self._view) - # Hide undo and redo, they're not needed - self._edit_toolbar.undo.props.visible = False - self._edit_toolbar.redo.props.visible = False - # Hide the separator too: - self._edit_toolbar.separator.props.visible = False - - # As long as nothing is selected, copy needs to be insensitive: - self._edit_toolbar.copy.set_sensitive(False) - # When the user clicks the button, call _edit_toolbar_copy_cb() - self._edit_toolbar.copy.connect('clicked', self._edit_toolbar_copy_cb) - - # Add the edit toolbar: - toolbox.add_toolbar(_('Edit'), self._edit_toolbar) - # And make it visible: - self._edit_toolbar.show() - """ - def __init__(self): - gtk.Toolbar.__init__(self) - - self.undo = ToolButton('edit-undo') - self.undo.set_tooltip(_('Undo')) - self.insert(self.undo, -1) - self.undo.show() - - self.redo = ToolButton('edit-redo') - self.redo.set_tooltip(_('Redo')) - self.insert(self.redo, -1) - self.redo.show() - - self.separator = gtk.SeparatorToolItem() - self.separator.set_draw(True) - self.insert(self.separator, -1) - self.separator.show() - - self.copy = ToolButton('edit-copy') - self.copy.set_tooltip(_('Copy')) - self.insert(self.copy, -1) - self.copy.show() - - self.paste = ToolButton('edit-paste') - self.paste.set_tooltip(_('Paste')) - self.insert(self.paste, -1) - self.paste.show() - -class ActivityToolbox(Toolbox): - """Creates the Toolbox for the Activity - - By default, the toolbox contains only the ActivityToolbar. After creating - the toolbox, you can add your activity specific toolbars, for example the - EditToolbar. - - To add the ActivityToolbox to your Activity in MyActivity.__init__() do: - - # Create the Toolbar with the ActivityToolbar: - toolbox = activity.ActivityToolbox(self) - ... your code, inserting all other toolbars you need, like EditToolbar - - # Add the toolbox to the activity frame: - self.set_toolbox(toolbox) - # And make it visible: - toolbox.show() - """ - def __init__(self, activity): - Toolbox.__init__(self) - - self._activity_toolbar = ActivityToolbar(activity) - self.add_toolbar(_('Activity'), self._activity_toolbar) - self._activity_toolbar.show() - - def get_activity_toolbar(self): - return self._activity_toolbar - class _ActivitySession(gobject.GObject): __gsignals__ = { 'quit-requested': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), diff --git a/src/sugar/activity/widgets.py b/src/sugar/activity/widgets.py new file mode 100644 index 00000000..e14c1f3f --- /dev/null +++ b/src/sugar/activity/widgets.py @@ -0,0 +1,297 @@ +# Copyright (C) 2009, Aleksey Lim +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import gobject +import gettext +import gconf + +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toolbarbox import ToolbarButton +from sugar.graphics.radiopalette import RadioPalette, RadioMenuButton +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.toolbox import Toolbox +from sugar.graphics.xocolor import XoColor +from sugar.graphics.icon import Icon +from sugar.bundle.activitybundle import ActivityBundle + +_ = lambda msg: gettext.dgettext('sugar-toolkit', msg) + +class ActivityToolbarButton(ToolbarButton): + def __init__(self, activity, **kwargs): + toolbar = ActivityToolbar(activity) + toolbar.stop.hide() + + ToolbarButton.__init__(self, page=toolbar, **kwargs) + + from sugar.activity.activity import get_bundle_path + bundle = ActivityBundle(get_bundle_path()) + + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + icon = Icon(file=bundle.get_icon(), xo_color=color) + icon.show() + self.set_icon_widget(icon) + +class StopButton(ToolButton): + def __init__(self, activity, **kwargs): + ToolButton.__init__(self, 'activity-stop', **kwargs) + self.props.tooltip = _('Stop') + self.props.accelerator = 'Q' + self.connect('clicked', self.__stop_button_clicked_cb, activity) + + def __stop_button_clicked_cb(self, button, activity): + activity.close() + +class UndoButton(ToolButton): + def __init__(self, **kwargs): + ToolButton.__init__(self, 'edit-undo', **kwargs) + self.props.tooltip = _('Undo') + self.props.accelerator = 'Q' + +class RedoButton(ToolButton): + def __init__(self, **kwargs): + ToolButton.__init__(self, 'edit-redo', **kwargs) + self.props.tooltip = _('Redo') + +class CopyButton(ToolButton): + def __init__(self, **kwargs): + ToolButton.__init__(self, 'edit-copy', **kwargs) + self.props.tooltip = _('Copy') + +class PasteButton(ToolButton): + def __init__(self, **kwargs): + ToolButton.__init__(self, 'edit-paste', **kwargs) + self.props.tooltip = _('Paste') + +class ShareButton(RadioMenuButton): + def __init__(self, activity, **kwargs): + palette = RadioPalette() + + self.private = RadioToolButton( + icon_name='zoom-home') + palette.append(self.private, _('Private')) + + self.neighborhood = RadioToolButton( + icon_name='zoom-neighborhood', + group=self.private) + self._neighborhood_handle = self.neighborhood.connect( + 'clicked', self.__neighborhood_clicked_cb, activity) + palette.append(self.neighborhood, _('My Neighborhood')) + + activity.connect('shared', self.__update_share_cb) + activity.connect('joined', self.__update_share_cb) + + RadioMenuButton.__init__(self, **kwargs) + self.props.palette = palette + + def __neighborhood_clicked_cb(self, button, activity): + activity.share() + + def __update_share_cb(self, activity): + self.neighborhood.handler_block(self._neighborhood_handle) + try: + if activity.get_shared(): + self.private.props.sensitive = False + self.neighborhood.props.sensitive = False + self.neighborhood.props.active = True + else: + self.private.props.sensitive = True + self.neighborhood.props.sensitive = True + self.private.props.active = True + finally: + self.neighborhood.handler_unblock(self._neighborhood_handle) + +class KeepButton(ToolButton): + def __init__(self, activity, **kwargs): + ToolButton.__init__(self, **kwargs) + self.props.tooltip = _('Keep') + self.props.accelerator = 'S' + + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + keep_icon = Icon(icon_name='document-save', xo_color=color) + keep_icon.show() + + self.set_icon_widget(keep_icon) + self.connect('clicked', self.__keep_button_clicked_cb, activity) + + def __keep_button_clicked_cb(self, button, activity): + activity.copy() + +class TitleEntry(gtk.ToolItem): + def __init__(self, activity, **kwargs): + gtk.ToolItem.__init__(self) + self.set_expand(False) + self._update_title_sid = None + + self.entry = gtk.Entry(**kwargs) + self.entry.set_size_request(int(gtk.gdk.screen_width() / 3), -1) + self.entry.set_text(activity.metadata['title']) + self.entry.connect('changed', self.__title_changed_cb, activity) + self.entry.show() + self.add(self.entry) + + activity.metadata.connect('updated', self.__jobject_updated_cb) + + def modify_bg(self, state, color): + gtk.ToolItem.modify_bg(self, state, color) + if state == gtk.STATE_NORMAL: + self.entry.modify_bg(gtk.STATE_INSENSITIVE, color) + + def __jobject_updated_cb(self, jobject): + self.entry.set_text(jobject['title']) + + def __title_changed_cb(self, entry, activity): + if not self._update_title_sid: + self._update_title_sid = gobject.timeout_add_seconds( + 1, self.__update_title_cb, activity) + + def __update_title_cb(self, activity): + title = self.entry.get_text() + + activity.metadata['title'] = title + activity.metadata['title_set_by_user'] = '1' + activity.save() + + shared_activity = activity.get_shared_activity() + if shared_activity is None: + shared_activity.props.name = title + + self._update_title_sid = None + return False + +class ActivityToolbar(gtk.Toolbar): + """The Activity toolbar with the Journal entry title, sharing, + Keep and Stop buttons + + All activities should have this toolbar. It is easiest to add it to your + Activity by using the ActivityToolbox. + """ + def __init__(self, activity): + gtk.Toolbar.__init__(self) + + self._activity = activity + + if activity.metadata: + title_button = TitleEntry(activity) + title_button.show() + self.insert(title_button, -1) + self.title = title_button.entry + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.share = ShareButton(activity) + self.share.show() + self.insert(self.share, -1) + + self.keep = KeepButton(activity) + self.insert(self.keep, -1) + self.keep.show() + + self.stop = StopButton(activity) + self.insert(self.stop, -1) + self.stop.show() + +class EditToolbar(gtk.Toolbar): + """Provides the standard edit toolbar for Activities. + + Members: + undo -- the undo button + redo -- the redo button + copy -- the copy button + paste -- the paste button + separator -- A separator between undo/redo and copy/paste + + This class only provides the 'edit' buttons in a standard layout, + your activity will need to either hide buttons which make no sense for your + Activity, or you need to connect the button events to your own callbacks: + + ## Example from Read.activity: + # Create the edit toolbar: + self._edit_toolbar = EditToolbar(self._view) + # Hide undo and redo, they're not needed + self._edit_toolbar.undo.props.visible = False + self._edit_toolbar.redo.props.visible = False + # Hide the separator too: + self._edit_toolbar.separator.props.visible = False + + # As long as nothing is selected, copy needs to be insensitive: + self._edit_toolbar.copy.set_sensitive(False) + # When the user clicks the button, call _edit_toolbar_copy_cb() + self._edit_toolbar.copy.connect('clicked', self._edit_toolbar_copy_cb) + + # Add the edit toolbar: + toolbox.add_toolbar(_('Edit'), self._edit_toolbar) + # And make it visible: + self._edit_toolbar.show() + """ + def __init__(self): + gtk.Toolbar.__init__(self) + + self.undo = UndoButton() + self.insert(self.undo, -1) + self.undo.show() + + self.redo = RedoButton() + self.insert(self.redo, -1) + self.redo.show() + + self.separator = gtk.SeparatorToolItem() + self.separator.set_draw(True) + self.insert(self.separator, -1) + self.separator.show() + + self.copy = CopyButton() + self.insert(self.copy, -1) + self.copy.show() + + self.paste = PasteButton() + self.insert(self.paste, -1) + self.paste.show() + +class ActivityToolbox(Toolbox): + """Creates the Toolbox for the Activity + + By default, the toolbox contains only the ActivityToolbar. After creating + the toolbox, you can add your activity specific toolbars, for example the + EditToolbar. + + To add the ActivityToolbox to your Activity in MyActivity.__init__() do: + + # Create the Toolbar with the ActivityToolbar: + toolbox = activity.ActivityToolbox(self) + ... your code, inserting all other toolbars you need, like EditToolbar + + # Add the toolbox to the activity frame: + self.set_toolbox(toolbox) + # And make it visible: + toolbox.show() + """ + def __init__(self, activity): + Toolbox.__init__(self) + + self._activity_toolbar = ActivityToolbar(activity) + self.add_toolbar(_('Activity'), self._activity_toolbar) + self._activity_toolbar.show() + + def get_activity_toolbar(self): + return self._activity_toolbar diff --git a/src/sugar/graphics/Makefile.am b/src/sugar/graphics/Makefile.am index c4d5e61a..a35fb4a0 100644 --- a/src/sugar/graphics/Makefile.am +++ b/src/sugar/graphics/Makefile.am @@ -1,27 +1,29 @@ 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 \ +sugar_PYTHON = \ + alert.py \ + animator.py \ + canvastextview.py \ + colorbutton.py \ + combobox.py \ + entry.py \ + iconentry.py \ + icon.py \ + __init__.py \ + menuitem.py \ + notebook.py \ + objectchooser.py \ + palettegroup.py \ + palette.py \ + panel.py \ + radiopalette.py \ + radiotoolbutton.py \ + roundbox.py \ + style.py \ + toggletoolbutton.py \ + toolbarbox.py \ + toolbox.py \ + toolbutton.py \ + toolcombobox.py \ + tray.py \ + window.py \ xocolor.py diff --git a/src/sugar/graphics/palette.py b/src/sugar/graphics/palette.py index 88145d24..92b04b65 100644 --- a/src/sugar/graphics/palette.py +++ b/src/sugar/graphics/palette.py @@ -127,11 +127,11 @@ class MouseSpeedDetector(gobject.GObject): return True -class Palette(gtk.Window): +class PaletteWindow(gtk.Window): PRIMARY = 0 SECONDARY = 1 - __gtype_name__ = 'SugarPalette' + __gtype_name__ = 'SugarPaletteWindow' __gsignals__ = { 'popup' : (gobject.SIGNAL_RUN_FIRST, @@ -142,18 +142,291 @@ class Palette(gtk.Window): gobject.TYPE_NONE, ([])) } + def __init__(self, **kwargs): + self._group_id = None + self._invoker = None + self._invoker_hids = [] + self._cursor_x = 0 + self._cursor_y = 0 + self._alignment = None + self._up = False + self._old_alloc = None + self._palette_state = self.PRIMARY + + 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)) + + gobject.GObject.__init__(self, **kwargs) + + self.set_decorated(False) + self.set_resizable(False) + # Just assume xthickness and ythickness are the same + self.set_border_width(self.get_style().xthickness) + + accel_group = gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.add_accel_group(accel_group) + + self.set_group_id("default") + + self.connect('show', self.__show_cb) + self.connect('hide', self.__hide_cb) + self.connect('realize', self.__realize_cb) + self.connect('destroy', self.__destroy_cb) + self.connect('enter-notify-event', self.__enter_notify_event_cb) + self.connect('leave-notify-event', self.__leave_notify_event_cb) + + self._mouse_detector = MouseSpeedDetector(self, 200, 5) + self._mouse_detector.connect('motion-slow', self._mouse_slow_cb) + + def __destroy_cb(self, palette): + self.set_group_id(None) + + def set_invoker(self, invoker): + for hid in self._invoker_hids[:]: + self._invoker.disconnect(hid) + self._invoker_hids.remove(hid) + + self._invoker = invoker + if invoker is not None: + self._invoker_hids.append(self._invoker.connect( + 'mouse-enter', self._invoker_mouse_enter_cb)) + self._invoker_hids.append(self._invoker.connect( + 'mouse-leave', self._invoker_mouse_leave_cb)) + self._invoker_hids.append(self._invoker.connect( + 'right-click', self._invoker_right_click_cb)) + + logging.debug(' Invoker set to %r' % self._invoker) + + def get_invoker(self): + return self._invoker + + invoker = gobject.property(type=object, + getter=get_invoker, + setter=set_invoker) + + def __realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + + def _mouse_slow_cb(self, widget): + self._mouse_detector.stop() + self._palette_do_popup() + + def _palette_do_popup(self): + immediate = False + + if self.is_up(): + self._popdown_anim.stop() + return + + if self._group_id: + group = palettegroup.get_group(self._group_id) + if group and group.is_up(): + immediate = True + group.popdown() + + self.popup(immediate=immediate) + + def is_up(self): + return self._up + + def set_group_id(self, group_id): + if self._group_id: + group = palettegroup.get_group(self._group_id) + group.remove(self) + if group_id: + self._group_id = group_id + group = palettegroup.get_group(group_id) + group.add(self) + + def get_group_id(self): + return self._group_id + + group_id = gobject.property(type=str, + getter=get_group_id, + setter=set_group_id) + + def do_size_request(self, requisition): + gtk.Window.do_size_request(self, requisition) + requisition.width = max(requisition.width, style.GRID_CELL_SIZE * 2) + + def do_size_allocate(self, allocation): + gtk.Window.do_size_allocate(self, allocation) + + if self._old_alloc is None or \ + self._old_alloc.x != allocation.x or \ + self._old_alloc.y != allocation.y or \ + self._old_alloc.width != allocation.width or \ + self._old_alloc.height != allocation.height: + self.queue_draw() + + # We need to store old allocation because when size_allocate + # is called widget.allocation is already updated. + # gtk.Window resizing is different from normal containers: + # the X window is resized, widget.allocation is updated from + # the configure request handler and finally size_allocate is called. + self._old_alloc = allocation + + def do_expose_event(self, event): + # We want to draw a border with a beautiful gap + if self._invoker is not None and self._invoker.has_rectangle_gap(): + invoker = self._invoker.get_rect() + palette = self.get_rect() + + gap = _calculate_gap(palette, invoker) + else: + gap = False + + allocation = self.get_allocation() + wstyle = self.get_style() + + if gap: + wstyle.paint_box_gap(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self, "palette", + 0, 0, allocation.width, allocation.height, + gap[0], gap[1], gap[2]) + else: + wstyle.paint_box(event.window, gtk.STATE_PRELIGHT, + gtk.SHADOW_IN, event.area, self, "palette", + 0, 0, allocation.width, allocation.height) + + # Fall trough to the container expose handler. + # (Leaving out the window expose handler which redraws everything) + gtk.Bin.do_expose_event(self, event) + + def update_position(self): + logging.debug(' update_position 1 %r %r' % (self._invoker, self._alignment)) + 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) + + logging.debug(' update_position %r %r' % (position.x, position.y)) + self.move(position.x, position.y) + + def get_full_size_request(self): + return self.size_request() + + def popup(self, immediate=False): + if self._invoker is not None: + full_size_request = self.get_full_size_request() + self._alignment = self._invoker.get_alignment(full_size_request) + + self.update_position() + self.set_transient_for(self._invoker.get_toplevel()) + + self._popdown_anim.stop() + + if not immediate: + self._popup_anim.start() + else: + self.show() + + 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 on_invoker_enter(self): + self._mouse_detector.start() + + def on_invoker_leave(self): + self._mouse_detector.stop() + self.popdown() + + def on_enter(self, event): + self._popdown_anim.stop() + self._secondary_anim.start() + + def on_leave(self, event): + self.popdown() + + def _invoker_mouse_enter_cb(self, invoker): + self.on_invoker_enter() + + def _invoker_mouse_leave_cb(self, invoker): + self.on_invoker_leave() + + def _invoker_right_click_cb(self, invoker): + self.popup(immediate=True) + + def __enter_notify_event_cb(self, widget, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR and \ + event.mode == gtk.gdk.CROSSING_NORMAL: + self.on_enter(event) + + def __leave_notify_event_cb(self, widget, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR and \ + event.mode == gtk.gdk.CROSSING_NORMAL: + self.on_leave(event) + + def __show_cb(self, widget): + self._invoker.notify_popup() + + self._up = True + self.emit('popup') + + def __hide_cb(self, widget): + logging.debug('__hide_cb') + self._secondary_anim.stop() + + if self._invoker: + self._invoker.notify_popdown() + + self._up = False + self.emit('popdown') + + def get_rect(self): + win_x, win_y = self.window.get_origin() + rectangle = self.get_allocation() + + x = win_x + rectangle.x + y = win_y + rectangle.y + width = rectangle.width + height = rectangle.height + + return gtk.gdk.Rectangle(x, y, width, height) + + def get_palette_state(self): + return self._palette_state + + def _set_palette_state(self, state): + self._palette_state = state + + def set_palette_state(self, state): + self._set_palette_state(state) + + palette_state = property(get_palette_state) + +class Palette(PaletteWindow): + __gtype_name__ = 'SugarPalette' + # 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=60, **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() @@ -200,49 +473,18 @@ class Palette(gtk.Window): 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) + PaletteWindow.__init__(self, **kwargs) primary_box.set_size_request(-1, 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: @@ -263,21 +505,63 @@ class Palette(gtk.Window): 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.connect('realize', self.__realize_cb) + self.connect('show', self.__show_cb) + self.connect('hide', self.__hide_cb) + self.connect('notify::invoker', self.__notify_invoker_cb) + self.connect('destroy', self.__destroy_cb) - self._mouse_detector = MouseSpeedDetector(self, 200, 5) - self._mouse_detector.connect('motion-slow', self._mouse_slow_cb) + def _invoker_right_click_cb(self, invoker): + self.popup(immediate=True, state=self.SECONDARY) + + def do_style_set(self, previous_style): + # Prevent a warning from pygtk + if previous_style is not None: + gtk.Window.do_style_set(self, previous_style) + self.set_border_width(self.get_style().xthickness) def __menu_item_inserted_cb(self, menu): self._update_separators() def __destroy_cb(self, palette): - self.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 __show_cb(self, widget): + self.menu.set_active(True) + + def __hide_cb(self, widget): + logging.debug('__hide_cb') + self.menu.set_active(False) + + def __notify_invoker_cb(self, palette, pspec): + invoker = self.props.invoker + if invoker is not None and hasattr(invoker.props, 'widget'): + logging.debug(('Setup widget', invoker.props.widget)) + self._update_accel_widget() + self._invoker.connect('notify::widget', + self.__invoker_widget_changed_cb) + + def __invoker_widget_changed_cb(self, invoker, spec): + self._update_accel_widget() + + def get_full_size_request(self): + return self._full_request + + def popup(self, immediate=False, state=None): + logging.debug('Palette.popup immediate %r' % immediate) + + if self._invoker is not None: + self._update_full_request() + + PaletteWindow.popup(self, immediate) + + if state is None: + state = self.PRIMARY + self.set_palette_state(state) + + self._secondary_anim.start() def _add_menu(self): self._menu_box = gtk.VBox() @@ -290,52 +574,6 @@ class Palette(gtk.Window): 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 @@ -438,24 +676,8 @@ class Palette(gtk.Window): 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) + PaletteWindow.do_size_request(self, requisition) # gtk.AccelLabel request doesn't include the accelerator. label_width = self._label_alignment.size_request()[0] + \ @@ -463,54 +685,9 @@ class Palette(gtk.Window): 2 * self.get_border_width() requisition.width = max(requisition.width, - 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 @@ -526,68 +703,21 @@ class Palette(gtk.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: + 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: + 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: + def _set_palette_state(self, state): + if self._palette_state == state: return if state == self.PRIMARY: @@ -596,73 +726,9 @@ class Palette(gtk.Window): 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') + self.update_position() + self._palette_state = state class PaletteActionBar(gtk.HButtonBox): def add_action(self, label, icon_name=None): @@ -728,7 +794,7 @@ class _SecondaryAnimation(animator.Animation): def next_frame(self, current): if current == 1.0: - self._palette.set_state(Palette.SECONDARY) + self._palette.set_palette_state(Palette.SECONDARY) class _PopdownAnimation(animator.Animation): def __init__(self, palette): diff --git a/src/sugar/graphics/radiopalette.py b/src/sugar/graphics/radiopalette.py new file mode 100644 index 00000000..9a9476dc --- /dev/null +++ b/src/sugar/graphics/radiopalette.py @@ -0,0 +1,98 @@ +# Copyright (C) 2009, Aleksey Lim +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk + +from sugar.graphics import style +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.palette import Palette + +class RadioMenuButton(ToolButton): + def __init__(self, **kwargs): + ToolButton.__init__(self, **kwargs) + self.selected_button = None + + if self.props.palette: + self.__palette_cb(None, None) + + self.connect('notify::palette', self.__palette_cb) + + def __palette_cb(self, widget, pspec): + if not isinstance(self.props.palette, RadioPalette): + return + self.props.palette.update_button() + + def do_clicked(self): + if self.palette is None: + return + if self.palette.is_up() and \ + self.palette.palette_state == Palette.SECONDARY: + self.palette.popdown(immediate=True) + else: + self.palette.popup(immediate=True, state=Palette.SECONDARY) + +class RadioToolsButton(RadioMenuButton): + def __init__(self, **kwargs): + RadioMenuButton.__init__(self, **kwargs) + + def do_clicked(self): + if not self.selected_button: + return + self.selected_button.emit('clicked') + +class RadioPalette(Palette): + def __init__(self, **kwargs): + Palette.__init__(self, **kwargs) + + self.button_box = gtk.HBox() + self.button_box.show() + self.set_content(self.button_box) + + def append(self, button, label): + children = self.button_box.get_children() + + if button.palette is not None: + raise RuntimeError("Palette's button should not have sub-palettes") + + button.show() + button.connect('clicked', self.__clicked_cb) + self.button_box.pack_start(button, fill=False) + button.palette_label = label + + if not children: + self.__clicked_cb(button) + + def update_button(self): + for i in self.button_box.get_children(): + self.__clicked_cb(i) + + def __clicked_cb(self, button): + if not button.get_active(): + return + + self.set_primary_text(button.palette_label) + self.popdown(immediate=True) + + if self.invoker is not None: + parent = self.invoker.parent + else: + parent = None + if not isinstance(parent, RadioMenuButton): + return + + parent.set_icon(button.props.icon_name) + parent.selected_button = button diff --git a/src/sugar/graphics/style.py b/src/sugar/graphics/style.py index 591957bf..e63e5caa 100644 --- a/src/sugar/graphics/style.py +++ b/src/sugar/graphics/style.py @@ -28,7 +28,7 @@ import logging import gtk import pango -_FOCUS_LINE_WIDTH = 2 +FOCUS_LINE_WIDTH = 2 _TAB_CURVATURE = 1 def _compute_zoom_factor(): @@ -115,8 +115,8 @@ 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_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') @@ -131,3 +131,5 @@ COLOR_INACTIVE_STROKE = Color('#757575') COLOR_TEXT_FIELD_GREY = Color('#E5E5E5') PALETTE_CURSOR_DISTANCE = zoom(10) + +TOOLBAR_ARROW_SIZE = zoom(24) diff --git a/src/sugar/graphics/toolbarbox.py b/src/sugar/graphics/toolbarbox.py new file mode 100644 index 00000000..54de2bd4 --- /dev/null +++ b/src/sugar/graphics/toolbarbox.py @@ -0,0 +1,284 @@ +# Copyright (C) 2009, Aleksey Lim +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import gobject +import logging + +from sugar.graphics import style +from sugar.graphics.palette import PaletteWindow, ToolInvoker +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics import palettegroup + +class ToolbarButton(ToolButton): + def __init__(self, page=None, **kwargs): + ToolButton.__init__(self, **kwargs) + + self.set_page(page) + + self.connect('clicked', + lambda widget: self.set_expanded(not self.is_expanded())) + + def get_toolbar_box(self): + if not hasattr(self.parent, 'owner'): + return None + return self.parent.owner + + toolbar_box = property(get_toolbar_box) + + def get_page(self): + if self.page_widget is None: + return None + return self.page_widget.child.child + + def set_page(self, page): + if page is None: + self.page_widget = None + return + self.page_widget = _embody_page(_Box, page) + self.page_widget.toolbar_button = self + page.show() + if self.props.palette is None: + self.props.palette = _ToolbarPalette(invoker=ToolInvoker(self)) + self.props.palette.toolbar_button = self + self._move_page_to_palette() + + page = gobject.property(type=object, getter=get_page, setter=set_page) + + def _move_page_to_palette(self): + if self.page_widget is None: + return + + if self.toolbar_box is not None and \ + self.page_widget in self.toolbar_box.get_children(): + self.toolbar_box.remove(self.page_widget) + + if isinstance(self.props.palette, _ToolbarPalette): + self.props.palette.add(self.page_widget) + + def is_expanded(self): + return self.toolbar_box is not None and self.page_widget is not None \ + and self.toolbar_box.expanded_button == self + + def popdown(self): + self.props.palette.popdown(immediate=True) + + def set_expanded(self, expanded): + self.popdown() + + box = self.toolbar_box + + if box is None or self.page_widget is None or \ + self.is_expanded() == expanded: + return + + if not expanded: + box.remove(self.page_widget) + box.expanded_button = None + self._move_page_to_palette() + return + + if box.expanded_button is not None: + # need to redraw it to erase arrow + expanded_toolitem = box.expanded_button.page_widget.toolbar_button + if expanded_toolitem.window is not None: + expanded_toolitem.window.invalidate_rect(None, True) + box.expanded_button.set_expanded(False) + + if self.page_widget.parent is not None: + self.props.palette.remove(self.page_widget) + + self.modify_bg(gtk.STATE_NORMAL, box.background) + _setup_page(self.page_widget, box.background, box.props.padding) + box.pack_start(self.page_widget) + box.expanded_button = self + + def do_expose_event(self, event): + if not self.is_expanded() or self.props.palette is not None and \ + self.props.palette.is_up(): + ToolButton.do_expose_event(self, event) + _paint_arrow(self, event, gtk.ARROW_DOWN) + return + + alloc = self.allocation + + self.get_style().paint_box(event.window, + gtk.STATE_NORMAL, gtk.SHADOW_IN, event.area, self, + 'palette-invoker', alloc.x, 0, + alloc.width, alloc.height + style.FOCUS_LINE_WIDTH) + + if self.child.state != gtk.STATE_PRELIGHT: + self.get_style().paint_box(event.window, + gtk.STATE_NORMAL, gtk.SHADOW_NONE, event.area, self, None, + alloc.x + style.FOCUS_LINE_WIDTH, style.FOCUS_LINE_WIDTH, + alloc.width - style.FOCUS_LINE_WIDTH * 2, alloc.height) + + gtk.ToolButton.do_expose_event(self, event) + _paint_arrow(self, event, gtk.ARROW_UP) + +class ToolbarBox(gtk.VBox): + def __init__(self, padding=style.TOOLBOX_HORIZONTAL_PADDING): + gtk.VBox.__init__(self) + self.expanded_button = None + self.background = None + + self._toolbar = gtk.Toolbar() + self._toolbar.owner = self + self._toolbar.connect('remove', self.__remove_cb) + + top_widget = _embody_page(gtk.EventBox, self._toolbar) + self.pack_start(top_widget) + + self.props.padding = padding + self.modify_bg(gtk.STATE_NORMAL, + style.COLOR_TOOLBAR_GREY.get_gdk_color()) + + def __remove_cb(self, sender, button): + if not isinstance(button, ToolbarButton): + return + button.popdown() + if self.expanded_button == button: + self.remove(button.page_widget) + self.expanded_button = None + + def get_toolbar(self): + return self._toolbar + + toolbar = property(get_toolbar) + + def get_padding(self): + return self.toolbar.parent.props.left_padding + + def set_padding(self, pad): + self.toolbar.parent.set_padding(0, 0, pad, pad) + + padding = gobject.property(type=object, + getter=get_padding, setter=set_padding) + + def modify_bg(self, state, color): + if state == gtk.STATE_NORMAL: + self.background = color + self.toolbar.parent.parent.modify_bg(state, color) + self.toolbar.modify_bg(state, color) + +class _ToolbarPalette(PaletteWindow): + def __init__(self, **kwargs): + PaletteWindow.__init__(self, **kwargs) + self.toolbar_box = None + self.set_border_width(0) + self._focus = 0 + + group = palettegroup.get_group('default') + group.connect('popdown', self.__group_popdown_cb) + self.set_group_id('toolbar_box') + + def on_invoker_enter(self): + PaletteWindow.on_invoker_enter(self) + self._handle_focus(+1) + + def on_invoker_leave(self): + PaletteWindow.on_invoker_leave(self) + self._handle_focus(-1) + + def on_enter(self, event): + PaletteWindow.on_enter(self, event) + self._handle_focus(+1) + + def on_leave(self, event): + PaletteWindow.on_enter(self, event) + self._handle_focus(-1) + + def _handle_focus(self, delta): + self._focus += delta + if self._focus not in (0, 1): + logging.error('_Palette._focus=%s not in (0, 1)' % self._focus) + + if self._focus == 0: + group = palettegroup.get_group('default') + if not group.is_up(): + self.popdown() + + def do_size_request(self, requisition): + gtk.Window.do_size_request(self, requisition) + requisition.width = max(requisition.width, + gtk.gdk.screen_width()) + + def popup(self, immediate=False): + button = self.toolbar_button + if button.is_expanded(): + return + box = button.toolbar_box + _setup_page(button.page_widget, style.COLOR_BLACK.get_gdk_color(), + box.props.padding) + PaletteWindow.popup(self, immediate) + + def __group_popdown_cb(self, group): + if self._focus == 0: + self.popdown(immediate=True) + +class _Box(gtk.EventBox): + def __init__(self): + gtk.EventBox.__init__(self) + self.connect('expose-event', self.do_expose_event) + self.set_app_paintable(True) + + def do_expose_event(self, widget, event): + alloc = self.toolbar_button.allocation + self.get_style().paint_box(event.window, + gtk.STATE_NORMAL, gtk.SHADOW_IN, event.area, self, + 'palette-invoker', -style.FOCUS_LINE_WIDTH, 0, + self.allocation.width + style.FOCUS_LINE_WIDTH * 2, + self.allocation.height + style.FOCUS_LINE_WIDTH) + self.get_style().paint_box(event.window, + gtk.STATE_NORMAL, gtk.SHADOW_NONE, event.area, self, None, + alloc.x + style.FOCUS_LINE_WIDTH, 0, + alloc.width - style.FOCUS_LINE_WIDTH * 2, + style.FOCUS_LINE_WIDTH) + +def _setup_page(page_widget, color, hpad): + vpad = style.FOCUS_LINE_WIDTH + page_widget.child.set_padding(vpad, vpad, hpad, hpad) + + page = page_widget.child.child + page.modify_bg(gtk.STATE_NORMAL, color) + if isinstance(page, gtk.Container): + for i in page.get_children(): + i.modify_bg(gtk.STATE_NORMAL, color) + + page_widget.modify_bg(gtk.STATE_NORMAL, color) + page_widget.modify_bg(gtk.STATE_PRELIGHT, color) + +def _embody_page(box_class, widget): + widget.show() + alignment = gtk.Alignment(0.0, 0.0, 1.0, 1.0) + alignment.add(widget) + alignment.show() + box = box_class() + box.modify_bg(gtk.STATE_ACTIVE, style.COLOR_BUTTON_GREY.get_gdk_color()) + box.add(alignment) + box.show() + return box + +def _paint_arrow(widget, event, arrow_type): + alloc = widget.allocation + x = alloc.x + alloc.width / 2 - style.TOOLBAR_ARROW_SIZE / 2 + y = alloc.y + alloc.height - int(style.TOOLBAR_ARROW_SIZE * .85) + + widget.get_style().paint_arrow(event.window, + gtk.STATE_NORMAL, gtk.SHADOW_NONE, event.area, widget, + None, arrow_type, True, + x, y, style.TOOLBAR_ARROW_SIZE, style.TOOLBAR_ARROW_SIZE) diff --git a/src/sugar/graphics/toolbutton.py b/src/sugar/graphics/toolbutton.py index 6205b8a4..047df067 100644 --- a/src/sugar/graphics/toolbutton.py +++ b/src/sugar/graphics/toolbutton.py @@ -68,7 +68,6 @@ class ToolButton(gtk.ToolButton): 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) @@ -151,8 +150,7 @@ class ToolButton(gtk.ToolButton): allocation.width, allocation.height) gtk.ToolButton.do_expose_event(self, event) - - def __button_clicked_cb(self, widget): + + def do_clicked(self): if self.palette: self.palette.popdown(True) - diff --git a/src/sugar/graphics/window.py b/src/sugar/graphics/window.py index 9d6a6aa4..d1158a7c 100644 --- a/src/sugar/graphics/window.py +++ b/src/sugar/graphics/window.py @@ -21,6 +21,8 @@ STABLE. import gobject import gtk +import logging +import warnings from sugar.graphics.icon import Icon @@ -84,8 +86,8 @@ class Window(gtk.Window): 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.__toolbar_box = None self._alerts = [] self._canvas = None self.tray = None @@ -125,14 +127,19 @@ class Window(gtk.Window): canvas = property(get_canvas, set_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 get_toolbar_box(self): + return self.__toolbar_box + + def set_toolbar_box(self, toolbar_box): + if self.__toolbar_box: + self._vbox.remove(self.__toolbar_box) + + self._vbox.pack_start(toolbar_box, False) + self._vbox.reorder_child(toolbar_box, 0) + + self.__toolbar_box = toolbar_box + + toolbar_box = property(get_toolbar_box, set_toolbar_box) def set_tray(self, tray, position): if self.tray: @@ -152,7 +159,7 @@ class Window(gtk.Window): self._alerts.append(alert) if len(self._alerts) == 1: self._vbox.pack_start(alert, False) - if self.toolbox is not None: + if self.__toolbar_box is not None: self._vbox.reorder_child(alert, 1) else: self._vbox.reorder_child(alert, 0) @@ -165,7 +172,7 @@ class Window(gtk.Window): self._vbox.remove(alert) if len(self._alerts) >= 1: self._vbox.pack_start(self._alerts[0], False) - if self.toolbox is not None: + if self.__toolbar_box is not None: self._vbox.reorder_child(self._alerts[0], 1) else: self._vbox.reorder_child(self._alert[0], 0) @@ -180,8 +187,8 @@ class Window(gtk.Window): return False if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: - if self.toolbox is not None: - self.toolbox.hide() + if self.__toolbar_box is not None: + self.__toolbar_box.hide() if self.tray is not None: self.tray.hide() @@ -199,8 +206,8 @@ class Window(gtk.Window): self.__unfullscreen_button_timeout_cb) else: - if self.toolbox is not None: - self.toolbox.show() + if self.__toolbar_box is not None: + self.__toolbar_box.show() if self.tray is not None: self.tray.show() @@ -258,3 +265,14 @@ class Window(gtk.Window): setter=set_enable_fullscreen_mode, getter=get_enable_fullscreen_mode) + # DEPRECATED + + def set_toolbox(self, toolbar_box): + warnings.warn('use toolbar_box instead of toolbox', DeprecationWarning) + self.set_toolbar_box(toolbar_box) + + def get_toolbox(self): + warnings.warn('use toolbar_box instead of toolbox', DeprecationWarning) + return self.__toolbar_box + + toolbox = property(get_toolbox, set_toolbox)