Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
sugar/src/jarabe/controlpanel/gui.py /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
577 lines (478 sloc)
20.9 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (C) 2008 One Laptop Per Child | |
| # | |
| # This program is free software: you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published by | |
| # the Free Software Foundation, either version 3 of the License, or | |
| # (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| import os | |
| import logging | |
| from gettext import gettext as _ | |
| from gi.repository import GObject | |
| from gi.repository import GLib | |
| from gi.repository import Gtk | |
| from gi.repository import Gdk | |
| from gi.repository import GdkX11 | |
| from sugar3.graphics.icon import Icon | |
| from sugar3.graphics import style | |
| from sugar3.graphics.alert import Alert, TimeoutAlert | |
| from jarabe.model.session import get_session_manager | |
| from jarabe.controlpanel.toolbar import MainToolbar | |
| from jarabe.controlpanel.toolbar import SectionToolbar | |
| from jarabe import config | |
| from jarabe.model import shell | |
| _logger = logging.getLogger('ControlPanel') | |
| class ControlPanel(Gtk.Window): | |
| __gtype_name__ = 'SugarControlPanel' | |
| def __init__(self, window_xid=0): | |
| self.parent_window_xid = window_xid | |
| Gtk.Window.__init__(self) | |
| self._calculate_max_columns() | |
| self.set_border_width(style.LINE_WIDTH) | |
| self.set_position(Gtk.WindowPosition.CENTER_ALWAYS) | |
| self.set_decorated(False) | |
| self.set_resizable(False) | |
| self.set_modal(True) | |
| self.set_can_focus(True) | |
| self.connect('key-press-event', self.__key_press_event_cb) | |
| self._toolbar = None | |
| self._canvas = None | |
| self._table = None | |
| self._scrolledwindow = None | |
| self._separator = None | |
| self._section_view = None | |
| self._section_toolbar = None | |
| self._main_toolbar = None | |
| self._vbox = Gtk.VBox() | |
| self._hbox = Gtk.HBox() | |
| self._vbox.pack_start(self._hbox, True, True, 0) | |
| self._hbox.show() | |
| self._main_view = Gtk.EventBox() | |
| self._hbox.pack_start(self._main_view, True, True, 0) | |
| self._main_view.modify_bg(Gtk.StateType.NORMAL, | |
| style.COLOR_BLACK.get_gdk_color()) | |
| self._main_view.show() | |
| self.add(self._vbox) | |
| self._vbox.show() | |
| self.connect('realize', self.__realize_cb) | |
| self._options = self._get_options() | |
| self._current_option = None | |
| self._setup_main() | |
| self._setup_section() | |
| self._show_main_view() | |
| Gdk.Screen.get_default().connect( | |
| 'size-changed', self.__size_changed_cb) | |
| self._busy_count = 0 | |
| self._selected = [] | |
| def __realize_cb(self, widget): | |
| self.set_type_hint(Gdk.WindowTypeHint.DIALOG) | |
| window = self.get_window() | |
| window.set_accept_focus(True) | |
| if self.parent_window_xid > 0: | |
| display = Gdk.Display.get_default() | |
| parent = GdkX11.X11Window.foreign_new_for_display( | |
| display, self.parent_window_xid) | |
| window.set_transient_for(parent) | |
| # the modal windows counter is updated to disable hot keys - SL#4601 | |
| shell.get_model().push_modal() | |
| def __size_changed_cb(self, event): | |
| self._calculate_max_columns() | |
| def busy(self): | |
| if self._busy_count == 0: | |
| self._old_cursor = self.get_window().get_cursor() | |
| self._set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) | |
| self._busy_count += 1 | |
| def unbusy(self): | |
| self._busy_count -= 1 | |
| if self._busy_count == 0: | |
| self._set_cursor(self._old_cursor) | |
| def _set_cursor(self, cursor): | |
| self.get_window().set_cursor(cursor) | |
| Gdk.flush() | |
| def add_alert(self, alert): | |
| self._vbox.pack_start(alert, False, False, 0) | |
| self._vbox.reorder_child(alert, 2) | |
| def remove_alert(self, alert): | |
| self._vbox.remove(alert) | |
| def grab_focus(self): | |
| # overwrite grab focus in order to grab focus on the view | |
| self._main_view.get_child().grab_focus() | |
| def _calculate_max_columns(self): | |
| self._max_columns = int(0.285 * (float(Gdk.Screen.width()) / | |
| style.GRID_CELL_SIZE - 3)) | |
| offset = style.GRID_CELL_SIZE | |
| width = Gdk.Screen.width() - offset * 2 | |
| height = Gdk.Screen.height() - offset * 2 | |
| self.set_size_request(width, height) | |
| if hasattr(self, '_table'): | |
| for child in self._table.get_children(): | |
| child.destroy() | |
| self._setup_options() | |
| def _set_canvas(self, canvas): | |
| if self._canvas in self._main_view: | |
| self._main_view.remove(self._canvas) | |
| if canvas: | |
| self._main_view.add(canvas) | |
| self._canvas = canvas | |
| def _set_toolbar(self, toolbar): | |
| if self._toolbar: | |
| self._vbox.remove(self._toolbar) | |
| self._vbox.pack_start(toolbar, False, False, 0) | |
| self._vbox.reorder_child(toolbar, 0) | |
| self._toolbar = toolbar | |
| if not self._separator: | |
| self._separator = Gtk.HSeparator() | |
| self._vbox.pack_start(self._separator, False, False, 0) | |
| self._vbox.reorder_child(self._separator, 1) | |
| self._separator.show() | |
| def _setup_main(self): | |
| self._main_toolbar = MainToolbar() | |
| self._table = Gtk.Table() | |
| self._table.set_col_spacings(style.GRID_CELL_SIZE) | |
| self._table.set_row_spacings(style.GRID_CELL_SIZE) | |
| self._table.set_border_width(style.GRID_CELL_SIZE) | |
| self._scrolledwindow = Gtk.ScrolledWindow() | |
| self._scrolledwindow.set_can_focus(False) | |
| self._scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC, | |
| Gtk.PolicyType.AUTOMATIC) | |
| self._scrolledwindow.add_with_viewport(self._table) | |
| child = self._scrolledwindow.get_child() | |
| child.modify_bg( | |
| Gtk.StateType.NORMAL, style.COLOR_BLACK.get_gdk_color()) | |
| self._setup_options() | |
| self._main_toolbar.connect('stop-clicked', | |
| self.__stop_clicked_cb) | |
| self._main_toolbar.connect('search-changed', | |
| self.__search_changed_cb) | |
| def _setup_options(self): | |
| # If the screen width only supports two columns, start | |
| # placing from the second row. | |
| if self._max_columns == 2: | |
| row = 1 | |
| column = 0 | |
| else: | |
| # About Me and About my computer are hardcoded below to use the | |
| # first two slots so we need to leave them free. | |
| row = 0 | |
| column = 2 | |
| options = list(self._options.keys()) | |
| options.sort() | |
| for option in options: | |
| sectionicon = _SectionIcon(icon_name=self._options[option]['icon'], | |
| title=self._options[option]['title'], | |
| xo_color=self._options[option]['color'], | |
| pixel_size=style.GRID_CELL_SIZE) | |
| sectionicon.connect('button-press-event', | |
| self.__select_option_cb, option) | |
| sectionicon.show() | |
| if option == 'aboutme': | |
| self._table.attach(sectionicon, 0, 1, 0, 1) | |
| elif option == 'aboutcomputer': | |
| self._table.attach(sectionicon, 1, 2, 0, 1) | |
| else: | |
| self._table.attach(sectionicon, | |
| column, column + 1, | |
| row, row + 1) | |
| column += 1 | |
| if column == self._max_columns: | |
| column = 0 | |
| row += 1 | |
| self._options[option]['button'] = sectionicon | |
| def _show_main_view(self): | |
| if self._section_view is not None: | |
| self._section_view.destroy() | |
| self._section_view = None | |
| self._set_toolbar(self._main_toolbar) | |
| self._main_toolbar.show() | |
| self._set_canvas(self._scrolledwindow) | |
| self._main_view.modify_bg(Gtk.StateType.NORMAL, | |
| style.COLOR_BLACK.get_gdk_color()) | |
| self._table.show() | |
| self._scrolledwindow.show() | |
| entry = self._main_toolbar.get_entry() | |
| entry.set_text('') | |
| entry.connect('icon-press', self.__clear_icon_pressed_cb) | |
| self.grab_focus() | |
| def __key_press_event_cb(self, window, event): | |
| if event.keyval == Gdk.KEY_Return: | |
| if len(self._selected) == 1: | |
| self.show_section_view(self._selected[0]) | |
| return True | |
| if event.keyval == Gdk.KEY_Escape: | |
| if self._toolbar == self._main_toolbar: | |
| self.__stop_clicked_cb(None) | |
| self.destroy() | |
| else: | |
| self.__cancel_clicked_cb(None) | |
| return True | |
| # if the user clicked out of the window - fix SL #3188 | |
| if not self.is_active(): | |
| self.present() | |
| entry = self._main_toolbar.get_entry() | |
| if not entry.has_focus(): | |
| entry.grab_focus() | |
| return False | |
| def __clear_icon_pressed_cb(self, entry, icon_pos, event): | |
| self.grab_focus() | |
| def _update(self, query): | |
| self._selected = [] | |
| for option in self._options: | |
| found = False | |
| for key in self._options[option]['keywords']: | |
| if query.lower() in key.lower(): | |
| self._options[option]['button'].set_sensitive(True) | |
| self._selected.append(option) | |
| found = True | |
| break | |
| if not found: | |
| self._options[option]['button'].set_sensitive(False) | |
| def _setup_section(self): | |
| self._section_toolbar = SectionToolbar() | |
| self._section_toolbar.connect('cancel-clicked', | |
| self.__cancel_clicked_cb) | |
| self._section_toolbar.connect('accept-clicked', | |
| self.__accept_clicked_cb) | |
| def show_section_view(self, option): | |
| self._set_toolbar(self._section_toolbar) | |
| icon = self._section_toolbar.get_icon() | |
| icon.set_from_icon_name(self._options[option]['icon'], | |
| Gtk.IconSize.LARGE_TOOLBAR) | |
| icon.props.xo_color = self._options[option]['color'] | |
| title = self._section_toolbar.get_title() | |
| title.set_text(self._options[option]['title']) | |
| self._section_toolbar.show() | |
| self._current_option = option | |
| mod = __import__('.'.join(('cpsection', option, 'view')), | |
| globals(), locals(), ['view']) | |
| view_class = getattr(mod, self._options[option]['view'], None) | |
| mod = __import__('.'.join(('cpsection', option, 'model')), | |
| globals(), locals(), ['model']) | |
| model = ModelWrapper(mod) | |
| try: | |
| self.busy() | |
| self._section_view = view_class(model, | |
| self._options[option]['alerts']) | |
| self._set_canvas(self._section_view) | |
| self._section_view.show() | |
| finally: | |
| self.unbusy() | |
| self._section_view.connect('notify::is-valid', | |
| self.__valid_section_cb) | |
| self._section_view.connect('notify::is-cancellable', | |
| self.__cancellable_section_cb) | |
| self._section_view.connect('request-close', | |
| self.__close_request_cb) | |
| self._section_view.connect('add-alert', | |
| self.__create_restart_alert_cb) | |
| self._section_view.connect('set-toolbar-sensitivity', | |
| self.__set_toolbar_sensitivity_cb) | |
| self._main_view.modify_bg(Gtk.StateType.NORMAL, | |
| style.COLOR_WHITE.get_gdk_color()) | |
| def set_section_view_auto_close(self): | |
| """Automatically close the control panel if there is "nothing to do" | |
| """ | |
| self._section_view.auto_close = True | |
| def _get_options(self): | |
| """Get the available option information from the extensions | |
| """ | |
| options = {} | |
| path = os.path.join(config.ext_path, 'cpsection') | |
| folder = os.listdir(path) | |
| for item in folder: | |
| if os.path.isdir(os.path.join(path, item)) and \ | |
| os.path.exists(os.path.join(path, item, '__init__.py')): | |
| try: | |
| mod = __import__('.'.join(('cpsection', item)), | |
| globals(), locals(), [item]) | |
| view_class = getattr(mod, 'CLASS', None) | |
| if view_class is not None: | |
| options[item] = {} | |
| options[item]['alerts'] = [] | |
| options[item]['view'] = view_class | |
| options[item]['icon'] = getattr(mod, 'ICON', item) | |
| options[item]['title'] = getattr(mod, 'TITLE', item) | |
| options[item]['color'] = getattr(mod, 'COLOR', None) | |
| keywords = getattr(mod, 'KEYWORDS', []) | |
| keywords.append(options[item]['title'].lower()) | |
| if item not in keywords: | |
| keywords.append(item) | |
| options[item]['keywords'] = keywords | |
| else: | |
| _logger.debug('no CLASS attribute in %r', item) | |
| except Exception: | |
| logging.exception('Exception while loading extension:') | |
| return options | |
| def __cancel_clicked_cb(self, widget): | |
| self._section_view.undo() | |
| self._options[self._current_option]['alerts'] = [] | |
| self._section_toolbar.accept_button.set_sensitive(True) | |
| self._show_main_view() | |
| def __accept_clicked_cb(self, widget): | |
| if hasattr(self._section_view, "apply"): | |
| self._section_view.apply() | |
| if self._section_view.needs_restart: | |
| self.__set_toolbar_sensitivity_cb(False) | |
| if self._section_view.show_restart_alert: | |
| self.__create_restart_alert_cb() | |
| else: | |
| self._show_main_view() | |
| def __set_toolbar_sensitivity_cb(self, value=True, | |
| widget=None, event=None): | |
| self._section_toolbar.accept_button.set_sensitive(value) | |
| self._section_toolbar.cancel_button.set_sensitive(value) | |
| def __create_restart_alert_cb(self, widget=None, event=None): | |
| alert = Alert() | |
| alert.props.title = _('Warning') | |
| alert.props.msg = self._section_view.restart_msg | |
| if self._section_view.props.is_cancellable: | |
| icon = Icon(icon_name='dialog-cancel') | |
| alert.add_button(Gtk.ResponseType.CANCEL, | |
| _('Cancel changes'), icon) | |
| icon.show() | |
| if self._section_view.props.is_deferrable: | |
| icon = Icon(icon_name='dialog-ok') | |
| alert.add_button(Gtk.ResponseType.ACCEPT, _('Later'), icon) | |
| icon.show() | |
| icon = Icon(icon_name='system-restart') | |
| alert.add_button(Gtk.ResponseType.APPLY, _('Restart now'), icon) | |
| icon.show() | |
| self.add_alert(alert) | |
| alert.connect('response', self.__response_cb) | |
| alert.show() | |
| def __response_cb(self, alert, response_id): | |
| self.remove_alert(alert) | |
| self._section_toolbar.accept_button.set_sensitive(True) | |
| self._section_toolbar.cancel_button.set_sensitive(True) | |
| if response_id is Gtk.ResponseType.CANCEL: | |
| self._section_view.undo() | |
| self._section_view.setup() | |
| self._options[self._current_option]['alerts'] = [] | |
| elif response_id is Gtk.ResponseType.ACCEPT: | |
| self._options[self._current_option]['alerts'] = \ | |
| self._section_view.restart_alerts | |
| self._show_main_view() | |
| elif response_id is Gtk.ResponseType.APPLY: | |
| self.busy() | |
| self._section_toolbar.accept_button.set_sensitive(False) | |
| self._section_toolbar.cancel_button.set_sensitive(False) | |
| get_session_manager().logout() | |
| GLib.timeout_add_seconds(4, self.__quit_timeout_cb) | |
| def __quit_timeout_cb(self): | |
| self.unbusy() | |
| alert = TimeoutAlert(30) | |
| alert.props.title = _('An activity is not responding.') | |
| alert.props.msg = _('You may lose unsaved work if you continue.') | |
| alert.connect('response', self.__quit_accept_cb) | |
| self.add_alert(alert) | |
| alert.show() | |
| def __quit_accept_cb(self, alert, response_id): | |
| self.remove_alert(alert) | |
| if response_id is Gtk.ResponseType.CANCEL: | |
| get_session_manager().cancel_shutdown() | |
| self._section_toolbar.accept_button.set_sensitive(True) | |
| self._section_toolbar.cancel_button.set_sensitive(True) | |
| else: | |
| self.busy() | |
| get_session_manager().shutdown_completed() | |
| def __select_option_cb(self, button, event, option): | |
| self.show_section_view(option) | |
| def __search_changed_cb(self, maintoolbar, query): | |
| self._update(query) | |
| def __stop_clicked_cb(self, widget): | |
| shell.get_model().pop_modal() | |
| self.destroy() | |
| def __close_request_cb(self, widget, event=None): | |
| self.destroy() | |
| def __valid_section_cb(self, section_view, pspec): | |
| section_is_valid = section_view.props.is_valid | |
| self._section_toolbar.accept_button.set_sensitive(section_is_valid) | |
| def __cancellable_section_cb(self, section_view, pspec): | |
| cancellable = section_view.props.is_cancellable | |
| self._section_toolbar.cancel_button.set_sensitive(cancellable) | |
| class ModelWrapper(object): | |
| def __init__(self, module): | |
| self._module = module | |
| self._options = {} | |
| self._setup() | |
| def _setup(self): | |
| methods = dir(self._module) | |
| for method in methods: | |
| if method.startswith('get_') and method[4:] != 'color': | |
| try: | |
| self._options[method[4:]] = getattr(self._module, method)() | |
| except Exception: | |
| self._options[method[4:]] = None | |
| def __getattr__(self, name): | |
| return getattr(self._module, name) | |
| def undo(self): | |
| for key in list(self._options.keys()): | |
| method = getattr(self._module, 'set_' + key, None) | |
| if method and self._options[key] is not None: | |
| try: | |
| method(self._options[key]) | |
| except Exception as detail: | |
| _logger.debug('Error undo option: %s', detail) | |
| if hasattr(ControlPanel, 'set_css_name'): | |
| ControlPanel.set_css_name('controlpanel') | |
| class _SectionIcon(Gtk.EventBox): | |
| __gtype_name__ = 'SugarSectionIcon' | |
| __gproperties__ = { | |
| 'icon-name': (str, None, None, None, GObject.ParamFlags.READWRITE), | |
| 'pixel-size': (object, None, None, GObject.ParamFlags.READWRITE), | |
| 'xo-color': (object, None, None, GObject.ParamFlags.READWRITE), | |
| 'title': (str, None, None, None, GObject.ParamFlags.READWRITE), | |
| } | |
| def __init__(self, **kwargs): | |
| self._icon_name = None | |
| self._pixel_size = style.GRID_CELL_SIZE | |
| self._xo_color = None | |
| self._title = 'No Title' | |
| Gtk.EventBox.__init__(self, **kwargs) | |
| self._vbox = Gtk.VBox() | |
| self._icon = Icon(icon_name=self._icon_name, | |
| pixel_size=self._pixel_size, | |
| xo_color=self._xo_color) | |
| self._vbox.pack_start(self._icon, expand=False, fill=False, padding=0) | |
| self._label = Gtk.Label(label=self._title) | |
| self._label.modify_fg(Gtk.StateType.NORMAL, | |
| style.COLOR_WHITE.get_gdk_color()) | |
| self._vbox.pack_start(self._label, expand=False, fill=False, padding=0) | |
| self._vbox.set_spacing(style.DEFAULT_SPACING) | |
| self.set_visible_window(False) | |
| self.set_app_paintable(True) | |
| self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) | |
| self.add(self._vbox) | |
| self._vbox.show() | |
| self._label.show() | |
| self._icon.show() | |
| def get_icon(self): | |
| return self._icon | |
| def do_set_property(self, pspec, value): | |
| if pspec.name == 'icon-name': | |
| if self._icon_name != value: | |
| self._icon_name = value | |
| elif pspec.name == 'pixel-size': | |
| if self._pixel_size != value: | |
| self._pixel_size = value | |
| elif pspec.name == 'xo-color': | |
| if self._xo_color != value: | |
| self._xo_color = value | |
| elif pspec.name == 'title': | |
| if self._title != value: | |
| self._title = value | |
| def do_get_property(self, pspec): | |
| if pspec.name == 'icon-name': | |
| return self._icon_name | |
| if pspec.name == 'pixel-size': | |
| return self._pixel_size | |
| if pspec.name == 'xo-color': | |
| return self._xo_color | |
| if pspec.name == 'title': | |
| return self._title |