diff options
author | Marco Pesenti Gritti <mpgritti@gmail.com> | 2008-09-28 10:27:47 (GMT) |
---|---|---|
committer | Marco Pesenti Gritti <mpgritti@gmail.com> | 2008-09-28 10:27:47 (GMT) |
commit | 091adf5ee3ae797507328ab72225133039dfdebb (patch) | |
tree | 27400fde442e59914b2227fa3dbee54e997591c9 /src/jarabe | |
parent | 28c225bcbbc76f666bbdcd6c8f31232fc2720947 (diff) |
Move the shell code into site-packages.
Diffstat (limited to 'src/jarabe')
151 files changed, 19674 insertions, 0 deletions
diff --git a/src/jarabe/.gitignore b/src/jarabe/.gitignore new file mode 100644 index 0000000..4acd06b --- /dev/null +++ b/src/jarabe/.gitignore @@ -0,0 +1 @@ +config.py diff --git a/src/jarabe/Makefile.am b/src/jarabe/Makefile.am new file mode 100644 index 0000000..83571a4 --- /dev/null +++ b/src/jarabe/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = jarabe diff --git a/src/jarabe/__init__.py b/src/jarabe/__init__.py new file mode 100644 index 0000000..41b4b1c --- /dev/null +++ b/src/jarabe/__init__.py @@ -0,0 +1,26 @@ +"""OLPC Sugar Graphical "Shell" Interface + +Provides the shell-level operations for managing +the OLPC laptop computers. It interacts heavily +with (and depends upon) the Sugar UI libraries. + +This is a "graphical" shell, the name does not +refer to a command-line "shell" interface. +""" + +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/config.py.in b/src/jarabe/config.py.in new file mode 100644 index 0000000..7469b00 --- /dev/null +++ b/src/jarabe/config.py.in @@ -0,0 +1,23 @@ +# Copyright (C) 2008 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# pylint: disable-msg=C0301 + +prefix = '@prefix@' +data_path = '@prefix@/share/sugar/data' +shell_path = '@prefix@/share/sugar/shell' +version = '@SUCROSE_VERSION@' + diff --git a/src/jarabe/controlpanel/Makefile.am b/src/jarabe/controlpanel/Makefile.am new file mode 100644 index 0000000..7d17b4f --- /dev/null +++ b/src/jarabe/controlpanel/Makefile.am @@ -0,0 +1,12 @@ +SUBDIRS = aboutme aboutxo datetime frame language network power + +sugardir = $(pythondir)/jarabe/controlpanel +sugar_PYTHON = \ + __init__.py \ + cmd.py \ + gui.py \ + inlinealert.py \ + sectionview.py \ + toolbar.py + + diff --git a/src/jarabe/controlpanel/__init__.py b/src/jarabe/controlpanel/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/controlpanel/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/controlpanel/aboutme/Makefile.am b/src/jarabe/controlpanel/aboutme/Makefile.am new file mode 100644 index 0000000..da77593 --- /dev/null +++ b/src/jarabe/controlpanel/aboutme/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/jarabe/controlpanel/aboutme + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py diff --git a/src/jarabe/controlpanel/aboutme/__init__.py b/src/jarabe/controlpanel/aboutme/__init__.py new file mode 100644 index 0000000..b683e28 --- /dev/null +++ b/src/jarabe/controlpanel/aboutme/__init__.py @@ -0,0 +1,25 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +from sugar import profile + +CLASS = 'AboutMe' +ICON = 'module-about_me' +TITLE = _('About Me') +COLOR = profile.get_color() + + diff --git a/src/jarabe/controlpanel/aboutme/model.py b/src/jarabe/controlpanel/aboutme/model.py new file mode 100644 index 0000000..3818792 --- /dev/null +++ b/src/jarabe/controlpanel/aboutme/model.py @@ -0,0 +1,116 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +from gettext import gettext as _ + +from sugar import profile +from sugar.graphics.xocolor import XoColor + +_COLORS = {'red': {'dark':'#b20008', 'medium':'#e6000a', 'light':'#ffadce'}, + 'orange': {'dark':'#9a5200', 'medium':'#c97e00', 'light':'#ffc169'}, + 'yellow': {'dark':'#807500', 'medium':'#be9e00', 'light':'#fffa00'}, + 'green': {'dark':'#008009', 'medium':'#00b20d', 'light':'#8bff7a'}, + 'blue': {'dark':'#00588c', 'medium':'#005fe4', 'light':'#bccdff'}, + 'purple': {'dark':'#5e008c', 'medium':'#7f00bf', 'light':'#d1a3ff'} + } + +_MODIFIERS = ('dark', 'medium', 'light') + +def get_nick(): + return profile.get_nick_name() + +def print_nick(): + print get_nick() + +def set_nick(nick): + """Set the nickname. + nick : e.g. 'walter' + """ + if not nick: + raise ValueError(_("You must enter a name.")) + pro = profile.get_profile() + if not isinstance(nick, unicode): + nick = unicode(nick, 'utf-8') + pro.nick_name = nick + pro.save() + return 1 + +def get_color(): + return profile.get_color() + +def print_color(): + color_string = get_color().to_string() + tmp = color_string.split(',') + + stroke_tuple = None + fill_tuple = None + for color in _COLORS: + for hue in _COLORS[color]: + if _COLORS[color][hue] == tmp[0]: + stroke_tuple = (color, hue) + if _COLORS[color][hue] == tmp[1]: + fill_tuple = (color, hue) + + if stroke_tuple is not None: + print _('stroke: color=%s hue=%s') % (stroke_tuple[0], + stroke_tuple[1]) + else: + print _('stroke: %s') % (tmp[0]) + if fill_tuple is not None: + print _('fill: color=%s hue=%s') % (fill_tuple[0], fill_tuple[1]) + else: + print _('fill: %s') % (tmp[1]) + +def set_color(stroke, fill, stroke_modifier='medium', fill_modifier='medium'): + """Set the system color by setting a fill and stroke color. + fill : [red, orange, yellow, blue, green, purple] + stroke : [red, orange, yellow, blue, green, purple] + hue stroke : [dark, medium, light] (optional) + hue fill : [dark, medium, light] (optional) + """ + + if stroke_modifier not in _MODIFIERS or fill_modifier not in _MODIFIERS: + print (_("Error in specified color modifiers.")) + return + if stroke not in _COLORS or fill not in _COLORS: + print (_("Error in specified colors.")) + return + + if stroke_modifier == fill_modifier: + if fill_modifier == 'medium': + fill_modifier = 'light' + else: + fill_modifier = 'medium' + + color = _COLORS[stroke][stroke_modifier] + ',' \ + + _COLORS[fill][fill_modifier] + pro = profile.get_profile() + pro.color = XoColor(color) + pro.save() + return 1 + +def get_color_xo(): + return profile.get_color() + +def set_color_xo(color): + """Set a color with an XoColor + This method is used by the graphical user interface + """ + pro = profile.get_profile() + pro.color = color + pro.save() + return 1 diff --git a/src/jarabe/controlpanel/aboutme/view.py b/src/jarabe/controlpanel/aboutme/view.py new file mode 100644 index 0000000..fc4f351 --- /dev/null +++ b/src/jarabe/controlpanel/aboutme/view.py @@ -0,0 +1,226 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +from gettext import gettext as _ + +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor +from sugar import profile + +from jarabe.controlpanel.sectionview import SectionView +from jarabe.controlpanel.inlinealert import InlineAlert + +CLASS = 'AboutMe' +ICON = 'module-about_me' +COLOR = profile.get_color() +TITLE = _('About Me') + +class EventIcon(gtk.EventBox): + __gtype_name__ = "SugarEventIcon" + def __init__(self, **kwargs): + gtk.EventBox.__init__(self) + + self.icon = Icon(pixel_size = style.XLARGE_ICON_SIZE, **kwargs) + + self.set_visible_window(False) + self.set_app_paintable(True) + self.set_events(gtk.gdk.BUTTON_PRESS_MASK) + + self.add(self.icon) + self.icon.show() + +class ColorPicker(EventIcon): + __gsignals__ = { + 'color-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])) + } + def __init__(self, xocolor=None): + EventIcon.__init__(self) + self.icon.props.xo_color = xocolor + self.icon.props.icon_name = 'computer-xo' + self.icon.props.pixel_size = style.XLARGE_ICON_SIZE + self.connect('button_press_event', self.__pressed_cb) + + def __pressed_cb(self, button, event): + self._set_random_colors() + + def _set_random_colors(self): + xocolor = XoColor() + self.icon.props.xo_color = xocolor + self.emit('color-changed', xocolor) + +class AboutMe(SectionView): + def __init__(self, model, alerts): + SectionView.__init__(self) + + self._model = model + self.restart_alerts = alerts + self._nick_sid = 0 + self._color_valid = True + self._nick_valid = True + self._color_change_handler = None + self._nick_change_handler = None + + self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_spacing(style.DEFAULT_SPACING) + self._group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + + self._nick_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + self._nick_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + self._nick_entry = None + self._nick_alert = None + self._setup_nick() + + self._color_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + self._color_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + self._color_picker = None + self._color_alert = None + self._setup_color() + + self.setup() + + def _setup_nick(self): + label_entry = gtk.Label(_('Name:')) + label_entry.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + self._group.add_widget(label_entry) + label_entry.set_alignment(1, 0.5) + self._nick_box.pack_start(label_entry, expand=False) + label_entry.show() + + self._nick_entry = gtk.Entry() + self._nick_entry.modify_bg(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self._nick_entry.modify_base(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self._nick_entry.set_width_chars(25) + self._nick_box.pack_start(self._nick_entry, expand=False) + self._nick_entry.show() + + label_entry_error = gtk.Label() + self._group.add_widget(label_entry_error) + self._nick_alert_box.pack_start(label_entry_error, expand=False) + label_entry_error.show() + + self._nick_alert = InlineAlert() + self._nick_alert_box.pack_start(self._nick_alert) + if 'nick' in self.restart_alerts: + self._nick_alert.props.msg = self.restart_msg + self._nick_alert.show() + + self.pack_start(self._nick_box, False) + self.pack_start(self._nick_alert_box, False) + self._nick_box.show() + self._nick_alert_box.show() + + def _setup_color(self): + label_color = gtk.Label(_('Click to change your color:')) + label_color.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + self._group.add_widget(label_color) + self._color_box.pack_start(label_color, expand=False) + label_color.show() + + self._color_picker = ColorPicker() + self._color_box.pack_start(self._color_picker, expand=False) + self._color_picker.show() + + label_color_error = gtk.Label() + self._group.add_widget(label_color_error) + self._color_alert_box.pack_start(label_color_error, expand=False) + label_color_error.show() + + self._color_alert = InlineAlert() + self._color_alert_box.pack_start(self._color_alert) + if 'color' in self.restart_alerts: + self._color_alert.props.msg = self.restart_msg + self._color_alert.show() + + self.pack_start(self._color_box, False) + self.pack_start(self._color_alert_box, False) + self._color_box.show() + self._color_alert_box.show() + + def setup(self): + self._nick_entry.set_text(self._model.get_nick()) + self._color_picker.icon.props.xo_color = self._model.get_color_xo() + + self._color_valid = True + self._nick_valid = True + self.needs_restart = False + self._nick_change_handler = self._nick_entry.connect( \ + 'changed', self.__nick_changed_cb) + self._color_change_handler = self._color_picker.connect( \ + 'color-changed', self.__color_changed_cb) + + def undo(self): + self._color_picker.disconnect(self._color_change_handler) + self._nick_entry.disconnect(self._nick_change_handler) + self._model.undo() + self._nick_alert.hide() + self._color_alert.hide() + + def _validate(self): + if self._nick_valid and self._color_valid: + self.props.is_valid = True + else: + self.props.is_valid = False + + def __nick_changed_cb(self, widget, data=None): + if self._nick_sid: + gobject.source_remove(self._nick_sid) + self._nick_sid = gobject.timeout_add(self._APPLY_TIMEOUT, + self.__nick_timeout_cb, widget) + + def __nick_timeout_cb(self, widget): + self._nick_sid = 0 + + if widget.get_text() == self._model.get_nick(): + return False + try: + self._model.set_nick(widget.get_text()) + except ValueError, detail: + self._nick_alert.props.msg = detail + self._nick_valid = False + else: + self._nick_alert.props.msg = self.restart_msg + self._nick_valid = True + self.needs_restart = True + self.restart_alerts.append('nick') + + self._validate() + self._nick_alert.show() + return False + + def __color_changed_cb(self, colorpicker, xocolor): + self._model.set_color_xo(xocolor) + self.needs_restart = True + self._color_alert.props.msg = self.restart_msg + self._color_valid = True + self.restart_alerts.append('color') + + self._validate() + self._color_alert.show() + + + + + + diff --git a/src/jarabe/controlpanel/aboutxo/Makefile.am b/src/jarabe/controlpanel/aboutxo/Makefile.am new file mode 100644 index 0000000..77cb5d8 --- /dev/null +++ b/src/jarabe/controlpanel/aboutxo/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/jarabe/controlpanel/aboutxo + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py diff --git a/src/jarabe/controlpanel/aboutxo/__init__.py b/src/jarabe/controlpanel/aboutxo/__init__.py new file mode 100644 index 0000000..4dc2cb2 --- /dev/null +++ b/src/jarabe/controlpanel/aboutxo/__init__.py @@ -0,0 +1,22 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +CLASS = 'AboutXO' +ICON = 'module-about_my_xo' +TITLE = _('About my XO') + diff --git a/src/jarabe/controlpanel/aboutxo/model.py b/src/jarabe/controlpanel/aboutxo/model.py new file mode 100644 index 0000000..ebbcca7 --- /dev/null +++ b/src/jarabe/controlpanel/aboutxo/model.py @@ -0,0 +1,101 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import os +import logging +import re +from gettext import gettext as _ + +_logger = logging.getLogger('ControlPanel - AboutXO') +_not_available = _('Not available') + +def get_aboutxo(): + msg = 'Serial Number: %s \nBuild Number: %s \nFirmware Number: %s \n' \ + % (get_serial_number(), get_build_number(), get_firmware_number()) + return msg + +def print_aboutxo(): + print get_aboutxo() + +def get_serial_number(): + serial_no = _read_file('/ofw/serial-number') + if serial_no is None: + serial_no = _not_available + return serial_no + +def print_serial_number(): + print get_serial_number() + +def get_build_number(): + build_no = _read_file('/boot/olpc_build') + if build_no is None: + build_no = _not_available + return build_no + +def print_build_number(): + print get_build_number() + +def get_firmware_number(): + firmware_no = _read_file('/ofw/openprom/model') + if firmware_no is None: + firmware_no = _not_available + else: + firmware_no = re.split(" +", firmware_no) + if len(firmware_no) == 3: + firmware_no = firmware_no[1] + return firmware_no + +def print_firmware_number(): + print get_firmware_number() + +def _read_file(path): + if os.access(path, os.R_OK) == 0: + return None + + fd = open(path, 'r') + value = fd.read() + fd.close() + if value: + value = value.strip('\n') + return value + else: + _logger.debug('No information in file or directory: %s' % path) + return None + +def get_license(): + license_file = "/usr/share/licenses/common-licenses/GPLv2" + lang = os.environ['LANG'] + if lang.endswith("UTF-8"): + lang = lang[:-6] + + try_file = license_file + "." + lang + if os.path.isfile(try_file): + license_file = try_file + else: + try_file = license_file + "." + lang.split("_")[0] + if os.path.isfile(try_file): + license_file = try_file + + try: + fd = open(license_file) + # remove 0x0c page breaks which can't be rendered in text views + license_text = fd.read().replace('\x0c', '') + fd.close() + except IOError: + license_text = _not_available + return license_text + diff --git a/src/jarabe/controlpanel/aboutxo/view.py b/src/jarabe/controlpanel/aboutxo/view.py new file mode 100644 index 0000000..e18d38d --- /dev/null +++ b/src/jarabe/controlpanel/aboutxo/view.py @@ -0,0 +1,193 @@ +# coding=utf-8 +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +from gettext import gettext as _ + +import config +from sugar.graphics import style + +from jarabe.controlpanel.sectionview import SectionView + +class AboutXO(SectionView): + def __init__(self, model, alerts=None): + SectionView.__init__(self) + + self._model = model + + self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_spacing(style.DEFAULT_SPACING) + + self._group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + + scrollwindow = gtk.ScrolledWindow() + scrollwindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + self.pack_start(scrollwindow, expand=True) + scrollwindow.show() + + self._vbox = gtk.VBox() + scrollwindow.add_with_viewport(self._vbox) + self._vbox.show() + + self._setup_identity() + self._setup_software() + self._setup_copyright() + + def _setup_identity(self): + separator_identity = gtk.HSeparator() + self._vbox.pack_start(separator_identity, expand=False) + separator_identity.show() + + label_identity = gtk.Label(_('Identity')) + label_identity.set_alignment(0, 0) + self._vbox.pack_start(label_identity, expand=False) + label_identity.show() + vbox_identity = gtk.VBox() + vbox_identity.set_border_width(style.DEFAULT_SPACING * 2) + vbox_identity.set_spacing(style.DEFAULT_SPACING) + + box_identity = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_serial = gtk.Label(_('Serial Number:')) + label_serial.set_alignment(1, 0) + label_serial.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + box_identity.pack_start(label_serial, expand=False) + self._group.add_widget(label_serial) + label_serial.show() + label_serial_no = gtk.Label(self._model.get_serial_number()) + label_serial_no.set_alignment(0, 0) + box_identity.pack_start(label_serial_no, expand=False) + label_serial_no.show() + vbox_identity.pack_start(box_identity, expand=False) + box_identity.show() + + self._vbox.pack_start(vbox_identity, expand=False) + vbox_identity.show() + + + def _setup_software(self): + separator_software = gtk.HSeparator() + self._vbox.pack_start(separator_software, expand=False) + separator_software.show() + + label_software = gtk.Label(_('Software')) + label_software.set_alignment(0, 0) + self._vbox.pack_start(label_software, expand=False) + label_software.show() + box_software = gtk.VBox() + box_software.set_border_width(style.DEFAULT_SPACING * 2) + box_software.set_spacing(style.DEFAULT_SPACING) + + box_build = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_build = gtk.Label(_('Build:')) + label_build.set_alignment(1, 0) + label_build.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + box_build.pack_start(label_build, expand=False) + self._group.add_widget(label_build) + label_build.show() + label_build_no = gtk.Label(self._model.get_build_number()) + label_build_no.set_alignment(0, 0) + box_build.pack_start(label_build_no, expand=False) + label_build_no.show() + box_software.pack_start(box_build, expand=False) + box_build.show() + + box_sugar = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_sugar = gtk.Label(_('Sugar:')) + label_sugar.set_alignment(1, 0) + label_sugar.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + box_sugar.pack_start(label_sugar, expand=False) + self._group.add_widget(label_sugar) + label_sugar.show() + label_sugar_ver = gtk.Label(config.version) + label_sugar_ver.set_alignment(0, 0) + box_sugar.pack_start(label_sugar_ver, expand=False) + label_sugar_ver.show() + box_software.pack_start(box_sugar, expand=False) + box_sugar.show() + + box_firmware = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_firmware = gtk.Label(_('Firmware:')) + label_firmware.set_alignment(1, 0) + label_firmware.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + box_firmware.pack_start(label_firmware, expand=False) + self._group.add_widget(label_firmware) + label_firmware.show() + label_firmware_no = gtk.Label(self._model.get_firmware_number()) + label_firmware_no.set_alignment(0, 0) + box_firmware.pack_start(label_firmware_no, expand=False) + label_firmware_no.show() + box_software.pack_start(box_firmware, expand=False) + box_firmware.show() + + self._vbox.pack_start(box_software, expand=False) + box_software.show() + + def _setup_copyright(self): + separator_copyright = gtk.HSeparator() + self._vbox.pack_start(separator_copyright, expand=False) + separator_copyright.show() + + label_copyright = gtk.Label(_('Copyright and License')) + label_copyright.set_alignment(0, 0) + self._vbox.pack_start(label_copyright, expand=False) + label_copyright.show() + vbox_copyright = gtk.VBox() + vbox_copyright.set_border_width(style.DEFAULT_SPACING * 2) + vbox_copyright.set_spacing(style.DEFAULT_SPACING) + + label_copyright = gtk.Label(_("© 2008 One Laptop per Child " + "Association Inc; Red Hat Inc; " + "and Contributors.")) + label_copyright.set_alignment(0, 0) + label_copyright.show() + vbox_copyright.pack_start(label_copyright, expand=False) + + label_info = gtk.Label(_("Sugar is the graphical user interface that " + "you are looking at. Sugar is free software, " + "covered by the GNU General Public License, " + "and you are welcome to change it and/or " + "distribute copies of it under certain " + "conditions described therein.")) + label_info.set_alignment(0, 0) + label_info.set_line_wrap(True) + label_info.set_size_request(gtk.gdk.screen_width() / 2, -1) + label_info.show() + vbox_copyright.pack_start(label_info, expand=False) + + expander = gtk.Expander(_("Full license:")) + expander.connect("notify::expanded", self.license_expander_cb) + expander.show() + vbox_copyright.pack_start(expander, expand=True) + + self._vbox.pack_start(vbox_copyright, expand=True) + vbox_copyright.show() + + def license_expander_cb(self, expander, param_spec): + # load/destroy the license viewer on-demand, to avoid storing the + # GPL in memory at all times + if expander.get_expanded(): + view_license = gtk.TextView() + view_license.set_editable(False) + view_license.get_buffer().set_text(self._model.get_license()) + view_license.show() + expander.add(view_license) + else: + expander.get_child().destroy() diff --git a/src/jarabe/controlpanel/cmd.py b/src/jarabe/controlpanel/cmd.py new file mode 100644 index 0000000..8e41af5 --- /dev/null +++ b/src/jarabe/controlpanel/cmd.py @@ -0,0 +1,148 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import sys +import getopt +import os +from gettext import gettext as _ + +import config + +_RESTART = 1 + +_same_option_warning = _("sugar-control-panel: WARNING, found more than" + " one option with the same name: %s module: %r") +_no_option_error = _("sugar-control-panel: key=%s not an available option") +_general_error = _("sugar-control-panel: %s") + +def cmd_help(): + '''Print the help to the screen''' + # TRANS: Translators, there's a empty line at the end of this string, + # which must appear in the translated string (msgstr) as well. + print _('Usage: sugar-control-panel [ option ] key [ args ... ] \n\ + Control for the sugar environment. \n\ + Options: \n\ + -h show this help message and exit \n\ + -l list all the available options \n\ + -h key show information about this key \n\ + -g key get the current value of the key \n\ + -s key set the current value for the key \n\ + -c key clear the current value for the key \n\ + ') + +def note_restart(): + '''Instructions how to restart sugar''' + print _('To apply your changes you have to restart sugar.\n' + + 'Hit ctrl+alt+erase on the keyboard to trigger a restart.') + +def load_modules(): + '''Build a list of pointers to available modules and import them. + ''' + modules = [] + + path = os.path.join(config.shell_path, 'controlpanel') + 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, 'model.py')): + module = __import__('.'.join(('controlpanel', item, 'model')), + globals(), locals(), ['model']) + modules.append(module) + + return modules + +def main(): + try: + options, args = getopt.getopt(sys.argv[1:], "h:s:g:c:l", []) + except getopt.GetoptError: + cmd_help() + sys.exit(2) + + if not options: + cmd_help() + sys.exit(2) + + modules = load_modules() + + for option, key in options: + found = 0 + if option in ("-h"): + for module in modules: + method = getattr(module, 'set_' + key, None) + if method: + found += 1 + if found == 1: + print method.__doc__ + else: + print _(_same_option_warning % (key, module)) + if found == 0: + print _(_no_option_error % key) + if option in ("-l"): + for module in modules: + methods = dir(module) + print '%s:' % module.__name__.split('.')[1] + for method in methods: + if method.startswith('get_'): + print ' %s' % method[4:] + if option in ("-g"): + for module in modules: + method = getattr(module, 'print_' + key, None) + if method: + found += 1 + if found == 1: + try: + method() + except Exception, detail: + print _(_general_error % detail) + else: + print _(_same_option_warning % (key, module)) + if found == 0: + print _(_no_option_error % key) + if option in ("-s"): + for module in modules: + method = getattr(module, 'set_' + key, None) + if method: + note = 0 + found += 1 + if found == 1: + try: + note = method(*args) + except Exception, detail: + print _(_general_error % detail) + if note == _RESTART: + note_restart() + else: + print _(_same_option_warning % (key, module)) + if found == 0: + print _(_no_option_error % key) + if option in ("-c"): + for module in modules: + method = getattr(module, 'clear_' + key, None) + if method: + note = 0 + found += 1 + if found == 1: + try: + note = method(*args) + except Exception, detail: + print _(_general_error % detail) + if note == _RESTART: + note_restart() + else: + print _(_same_option_warning % (key, module)) + if found == 0: + print _(_no_option_error % key) diff --git a/src/jarabe/controlpanel/datetime/Makefile.am b/src/jarabe/controlpanel/datetime/Makefile.am new file mode 100644 index 0000000..f522683 --- /dev/null +++ b/src/jarabe/controlpanel/datetime/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/jarabe/controlpanel/datetime + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py diff --git a/src/jarabe/controlpanel/datetime/__init__.py b/src/jarabe/controlpanel/datetime/__init__.py new file mode 100644 index 0000000..fc9be45 --- /dev/null +++ b/src/jarabe/controlpanel/datetime/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +CLASS = 'TimeZone' +ICON = 'module-date_and_time' +TITLE = _('Date & Time') diff --git a/src/jarabe/controlpanel/datetime/model.py b/src/jarabe/controlpanel/datetime/model.py new file mode 100644 index 0000000..4a4c560 --- /dev/null +++ b/src/jarabe/controlpanel/datetime/model.py @@ -0,0 +1,94 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +# +# The timezone config is based on the system-config-date +# (http://fedoraproject.org/wiki/SystemConfig/date) tool. +# Parts of the code were reused. +# + +import os +from gettext import gettext as _ + +from sugar import profile + +_zone_tab = '/usr/share/zoneinfo/zone.tab' + +def _initialize(): + '''Initialize the docstring of the set function''' + if set_timezone.__doc__ is None: + # when running under 'python -OO', all __doc__ fields are None, + # so += would fail -- and this function would be unnecessary anyway. + return + timezones = read_all_timezones() + for timezone in timezones: + set_timezone.__doc__ += timezone + '\n' + +def read_all_timezones(fn=_zone_tab): + fd = open (fn, 'r') + lines = fd.readlines() + fd.close() + timezones = [] + for line in lines: + if line.startswith('#'): + continue + line = line.split() + if len(line) > 1: + timezones.append(line[2]) + timezones.sort() + + for offset in xrange(-12, 13): + if offset < 0: + tz = 'GMT%d' % offset + elif offset > 0: + tz = 'GMT+%d' % offset + else: + tz = 'GMT' + timezones.append(tz) + for offset in xrange(-12, 13): + if offset < 0: + tz = 'UTC%d' % offset + elif offset > 0: + tz = 'UTC+%d' % offset + else: + tz = 'UTC' + timezones.append(tz) + return timezones + +def get_timezone(): + pro = profile.get_profile() + return pro.timezone + +def print_timezone(): + print get_timezone() + +def set_timezone(timezone): + """Set the system timezone + timezone : e.g. 'America/Los_Angeles' + """ + timezones = read_all_timezones() + if timezone in timezones: + os.environ['TZ'] = timezone + pro = profile.get_profile() + pro.timezone = timezone + pro.save() + else: + raise ValueError(_("Error timezone does not exist.")) + return 1 + +# inilialize the docstrings for the timezone +_initialize() + diff --git a/src/jarabe/controlpanel/datetime/view.py b/src/jarabe/controlpanel/datetime/view.py new file mode 100644 index 0000000..58719b4 --- /dev/null +++ b/src/jarabe/controlpanel/datetime/view.py @@ -0,0 +1,138 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +from gettext import gettext as _ + +from sugar.graphics import style +from sugar.graphics import iconentry + +from jarabe.controlpanel.sectionview import SectionView +from jarabe.controlpanel.inlinealert import InlineAlert + +class TimeZone(SectionView): + def __init__(self, model, alerts): + SectionView.__init__(self) + + self._model = model + self.restart_alerts = alerts + self._zone_sid = 0 + self._cursor_change_handler = None + + self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_spacing(style.DEFAULT_SPACING) + + self.connect("realize", self.__realize_cb) + + self._entry = iconentry.IconEntry() + self._entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self._entry.add_clear_button() + self._entry.modify_bg(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self._entry.modify_base(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self.pack_start(self._entry, False) + self._entry.show() + + self._scrolled_window = gtk.ScrolledWindow() + self._scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + self._scrolled_window.set_shadow_type(gtk.SHADOW_IN) + + self._store = gtk.ListStore(gobject.TYPE_STRING) + zones = model.read_all_timezones() + for zone in zones: + self._store.append([zone]) + + self._treeview = gtk.TreeView(self._store) + self._treeview.set_search_entry(self._entry) + self._treeview.set_search_equal_func(self._search) + self._treeview.set_search_column(0) + self._scrolled_window.add(self._treeview) + self._treeview.show() + + self._timezone_column = gtk.TreeViewColumn(_('Timezone')) + self._cell = gtk.CellRendererText() + self._timezone_column.pack_start(self._cell, True) + self._timezone_column.add_attribute(self._cell, 'text', 0) + self._timezone_column.set_sort_column_id(0) + self._treeview.append_column(self._timezone_column) + + self.pack_start(self._scrolled_window) + self._scrolled_window.show() + + self._zone_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + self.pack_start(self._zone_alert_box, False) + + self._zone_alert = InlineAlert() + self._zone_alert_box.pack_start(self._zone_alert) + if 'zone' in self.restart_alerts: + self._zone_alert.props.msg = self.restart_msg + self._zone_alert.show() + self._zone_alert_box.show() + + self.setup() + + def setup(self): + zone = self._model.get_timezone() + for row in self._store: + if zone == row[0]: + self._treeview.set_cursor(row.path, self._timezone_column, + False) + self._treeview.scroll_to_cell(row.path, self._timezone_column, + True, 0.5, 0.5) + break + + self.needs_restart = False + self._cursor_change_handler = self._treeview.connect( \ + "cursor-changed", self.__zone_changed_cd) + + def undo(self): + self._treeview.disconnect(self._cursor_change_handler) + self._model.undo() + self._zone_alert.hide() + + def __realize_cb(self, widget): + self._entry.grab_focus() + + def _search(self, model, column, key, iterator, data=None): + value = model.get_value(iterator, column) + if key.lower() in value.lower(): + return False + return True + + def __zone_changed_cd(self, treeview, data=None): + list_, row = treeview.get_selection().get_selected() + if not row: + return False + if self._model.get_timezone() == self._store.get_value(row, 0): + return False + + if self._zone_sid: + gobject.source_remove(self._zone_sid) + self._zone_sid = gobject.timeout_add(self._APPLY_TIMEOUT, + self.__zone_timeout_cb, row) + return True + + def __zone_timeout_cb(self, row): + self._zone_sid = 0 + self._model.set_timezone(self._store.get_value(row, 0)) + self.restart_alerts.append('zone') + self.needs_restart = True + self._zone_alert.props.msg = self.restart_msg + self._zone_alert.show() + return False diff --git a/src/jarabe/controlpanel/frame/Makefile.am b/src/jarabe/controlpanel/frame/Makefile.am new file mode 100644 index 0000000..10e2b40 --- /dev/null +++ b/src/jarabe/controlpanel/frame/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/jarabe/controlpanel/frame + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py diff --git a/src/jarabe/controlpanel/frame/__init__.py b/src/jarabe/controlpanel/frame/__init__.py new file mode 100644 index 0000000..a93f9c7 --- /dev/null +++ b/src/jarabe/controlpanel/frame/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +CLASS = 'Frame' +ICON = 'module-frame' +TITLE = _('Frame') diff --git a/src/jarabe/controlpanel/frame/model.py b/src/jarabe/controlpanel/frame/model.py new file mode 100644 index 0000000..0e19703 --- /dev/null +++ b/src/jarabe/controlpanel/frame/model.py @@ -0,0 +1,64 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +from gettext import gettext as _ + +from sugar import profile + +def get_corner_delay(): + pro = profile.get_profile() + return pro.hot_corners_delay + +def print_corner_delay(): + print get_corner_delay() + +def set_corner_delay(delay): + """Set a delay for the activation of the frame using hot corners. + instantaneous: 0 (0 milliseconds) + delay: 100 (100 milliseconds) + never: 1000 (disable activation) + """ + try: + int(delay) + except ValueError: + raise ValueError(_("Value must be an integer.")) + pro = profile.get_profile() + pro.hot_corners_delay = int(delay) + pro.save() + return 1 + +def get_edge_delay(): + pro = profile.get_profile() + return pro.warm_edges_delay + +def print_edge_delay(): + print get_edge_delay() + +def set_edge_delay(delay): + """Set a delay for the activation of the frame using warm edges. + instantaneous: 0 (0 milliseconds) + delay: 100 (100 milliseconds) + never: 1000 (disable activation) + """ + try: + int(delay) + except ValueError: + raise ValueError(_("Value must be an integer.")) + pro = profile.get_profile() + pro.warm_edges_delay = int(delay) + pro.save() + return 1 diff --git a/src/jarabe/controlpanel/frame/view.py b/src/jarabe/controlpanel/frame/view.py new file mode 100644 index 0000000..7ab7bd2 --- /dev/null +++ b/src/jarabe/controlpanel/frame/view.py @@ -0,0 +1,232 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +from gettext import gettext as _ + +from sugar.graphics import style + +from jarabe.controlpanel.sectionview import SectionView +from jarabe.controlpanel.inlinealert import InlineAlert + +_never = _('never') +_instantaneous = _('instantaneous') +_seconds_label = _('%s seconds') +_MAX_DELAY = 1000.0 + +class Frame(SectionView): + def __init__(self, model, alerts): + SectionView.__init__(self) + + self._model = model + self._corner_delay_sid = 0 + self._corner_delay_is_valid = True + self._corner_delay_change_handler = None + self._edge_delay_sid = 0 + self._edge_delay_is_valid = True + self._edge_delay_change_handler = None + self.restart_alerts = alerts + + self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_spacing(style.DEFAULT_SPACING) + self._group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + + separator = gtk.HSeparator() + self.pack_start(separator, expand=False) + separator.show() + + label_activation = gtk.Label(_('Activation Delay')) + label_activation.set_alignment(0, 0) + self.pack_start(label_activation, expand=False) + label_activation.show() + + self._box_sliders = gtk.VBox() + self._box_sliders.set_border_width(style.DEFAULT_SPACING * 2) + self._box_sliders.set_spacing(style.DEFAULT_SPACING) + + self._corner_delay_slider = None + self._corner_delay_alert = None + self._setup_corner() + + self._edge_delay_slider = None + self._edge_delay_alert = None + self._setup_edge() + + self.pack_start(self._box_sliders, expand=False) + self._box_sliders.show() + + self.setup() + + def _setup_corner(self): + box_delay = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_delay = gtk.Label(_('Corner')) + label_delay.set_alignment(1, 0.75) + label_delay.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + box_delay.pack_start(label_delay, expand=False) + self._group.add_widget(label_delay) + label_delay.show() + + adj = gtk.Adjustment(value=100, lower=0, upper=_MAX_DELAY, + step_incr=100, page_incr=100, page_size=0) + self._corner_delay_slider = gtk.HScale(adj) + self._corner_delay_slider.set_digits(0) + self._corner_delay_slider.connect('format-value', + self.__corner_delay_format_cb) + box_delay.pack_start(self._corner_delay_slider) + self._corner_delay_slider.show() + self._box_sliders.pack_start(box_delay, expand=False) + box_delay.show() + + self._corner_delay_alert = InlineAlert() + label_delay_error = gtk.Label() + self._group.add_widget(label_delay_error) + + delay_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + delay_alert_box.pack_start(label_delay_error, expand=False) + label_delay_error.show() + delay_alert_box.pack_start(self._corner_delay_alert, expand=False) + self._box_sliders.pack_start(delay_alert_box, expand=False) + delay_alert_box.show() + if 'corner_delay' in self.restart_alerts: + self._corner_delay_alert.props.msg = self.restart_msg + self._corner_delay_alert.show() + + def _setup_edge(self): + box_delay = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_delay = gtk.Label(_('Edge')) + label_delay.set_alignment(1, 0.75) + label_delay.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + box_delay.pack_start(label_delay, expand=False) + self._group.add_widget(label_delay) + label_delay.show() + + adj = gtk.Adjustment(value=100, lower=0, upper=_MAX_DELAY, + step_incr=100, page_incr=100, page_size=0) + self._edge_delay_slider = gtk.HScale(adj) + self._edge_delay_slider.set_digits(0) + self._edge_delay_slider.connect('format-value', + self.__edge_delay_format_cb) + box_delay.pack_start(self._edge_delay_slider) + self._edge_delay_slider.show() + self._box_sliders.pack_start(box_delay, expand=False) + box_delay.show() + + self._edge_delay_alert = InlineAlert() + label_delay_error = gtk.Label() + self._group.add_widget(label_delay_error) + + delay_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + delay_alert_box.pack_start(label_delay_error, expand=False) + label_delay_error.show() + delay_alert_box.pack_start(self._edge_delay_alert, expand=False) + self._box_sliders.pack_start(delay_alert_box, expand=False) + delay_alert_box.show() + if 'edge_delay' in self.restart_alerts: + self._edge_delay_alert.props.msg = self.restart_msg + self._edge_delay_alert.show() + + def setup(self): + self._corner_delay_slider.set_value(self._model.get_corner_delay()) + self._edge_delay_slider.set_value(self._model.get_edge_delay()) + self._corner_delay_is_valid = True + self._edge_delay_is_valid = True + self.needs_restart = False + self._corner_delay_change_handler = self._corner_delay_slider.connect( \ + 'value-changed', self.__corner_delay_changed_cb) + self._edge_delay_change_handler = self._edge_delay_slider.connect( \ + 'value-changed', self.__edge_delay_changed_cb) + + def undo(self): + self._corner_delay_slider.disconnect(self._corner_delay_change_handler) + self._edge_delay_slider.disconnect(self._edge_delay_change_handler) + self._model.undo() + self._corner_delay_alert.hide() + self._edge_delay_alert.hide() + + def _validate(self): + if self._edge_delay_is_valid and self._corner_delay_is_valid: + self.props.is_valid = True + else: + self.props.is_valid = False + + def __corner_delay_changed_cb(self, scale, data=None): + if self._corner_delay_sid: + gobject.source_remove(self._corner_delay_sid) + self._corner_delay_sid = gobject.timeout_add( \ + self._APPLY_TIMEOUT, self.__corner_delay_timeout_cb, scale) + + def __corner_delay_timeout_cb(self, scale): + self._corner_delay_sid = 0 + if scale.get_value() == self._model.get_corner_delay(): + return + try: + self._model.set_corner_delay(scale.get_value()) + except ValueError, detail: + self._corner_delay_alert.props.msg = detail + self._corner_delay_is_valid = False + else: + self._corner_delay_alert.props.msg = self.restart_msg + self._corner_delay_is_valid = True + self.needs_restart = True + self.restart_alerts.append('corner_delay') + + self._validate() + self._corner_delay_alert.show() + return False + + def __corner_delay_format_cb(self, scale, value): + if value == _MAX_DELAY: + return _never + elif value == 0.0: + return _instantaneous + else: + return _seconds_label % (value / _MAX_DELAY) + + def __edge_delay_changed_cb(self, scale, data=None): + if self._edge_delay_sid: + gobject.source_remove(self._edge_delay_sid) + self._edge_delay_sid = gobject.timeout_add( \ + self._APPLY_TIMEOUT, self.__edge_delay_timeout_cb, scale) + + def __edge_delay_timeout_cb(self, scale): + self._edge_delay_sid = 0 + if scale.get_value() == self._model.get_edge_delay(): + return + try: + self._model.set_edge_delay(scale.get_value()) + except ValueError, detail: + self._edge_delay_alert.props.msg = detail + self._edge_delay_is_valid = False + else: + self._edge_delay_alert.props.msg = self.restart_msg + self._edge_delay_is_valid = True + self.needs_restart = True + self.restart_alerts.append('edge_delay') + + self._validate() + self._edge_delay_alert.show() + return False + + def __edge_delay_format_cb(self, scale, value): + if value == _MAX_DELAY: + return _never + elif value == 0.0: + return _instantaneous + else: + return _seconds_label % (value / _MAX_DELAY) diff --git a/src/jarabe/controlpanel/gui.py b/src/jarabe/controlpanel/gui.py new file mode 100644 index 0000000..a0e4498 --- /dev/null +++ b/src/jarabe/controlpanel/gui.py @@ -0,0 +1,414 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import os +import gobject +import logging +from gettext import gettext as _ + +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar.graphics.alert import Alert +import config +from session import get_session_manager + +from jarabe.controlpanel.toolbar import MainToolbar +from jarabe.controlpanel.toolbar import SectionToolbar + +_logger = logging.getLogger('ControlPanel') +_MAX_COLUMNS = 5 + +class ControlPanel(gtk.Window): + __gtype_name__ = 'SugarControlPanel' + + def __init__(self): + gtk.Window.__init__(self) + + self.set_border_width(style.LINE_WIDTH) + offset = style.GRID_CELL_SIZE + width = gtk.gdk.screen_width() - offset * 2 + height = gtk.gdk.screen_height() - offset * 2 + self.set_size_request(width, height) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_decorated(False) + self.set_resizable(False) + self.set_modal(True) + + 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) + self._hbox.show() + + self._main_view = gtk.EventBox() + self._hbox.pack_start(self._main_view) + self._main_view.modify_bg(gtk.STATE_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() + + def __realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + + def _set_canvas(self, canvas): + if self._canvas: + 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) + self._vbox.reorder_child(toolbar, 0) + self._toolbar = toolbar + if not self._separator: + self._separator = gtk.HSeparator() + self._vbox.pack_start(self._separator, False) + 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_border_width(style.GRID_CELL_SIZE) + + self._scrolledwindow = gtk.ScrolledWindow() + self._scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, + gtk.POLICY_AUTOMATIC) + self._scrolledwindow.add_with_viewport(self._table) + child = self._scrolledwindow.get_child() + child.modify_bg(gtk.STATE_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): + row = 0 + column = 2 + options = 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 == 'aboutxo': + self._table.attach(sectionicon, 1, 2, 0, 1) + else: + self._table.attach(sectionicon, + column, column + 1, + row, row + 1) + column += 1 + if column == _MAX_COLUMNS: + column = 0 + row += 1 + + self._options[option]['button'] = sectionicon + + def _show_main_view(self): + self._set_toolbar(self._main_toolbar) + self._main_toolbar.show() + self._set_canvas(self._scrolledwindow) + self._main_view.modify_bg(gtk.STATE_NORMAL, + style.COLOR_BLACK.get_gdk_color()) + self._table.show() + self._scrolledwindow.show() + entry = self._main_toolbar.get_entry() + entry.grab_focus() + entry.set_text('') + + def _update(self, query): + 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) + 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.ICON_SIZE_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(('controlpanel', option, 'view')), + globals(), locals(), ['view']) + view_class = getattr(mod, self._options[option]['view'], None) + + mod = __import__('.'.join(('controlpanel', option, 'model')), + globals(), locals(), ['model']) + model = ModelWrapper(mod) + + self._section_view = view_class(model, + self._options[option]['alerts']) + + self._set_canvas(self._section_view) + self._section_view.show() + self._section_view.connect('notify::is-valid', + self.__valid_section_cb) + self._section_view.connect('request-close', + self.__close_request_cb) + self._main_view.modify_bg(gtk.STATE_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.shell_path, 'controlpanel') + 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')): + mod = __import__('.'.join(('controlpanel', 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) + else: + _logger.error('There is no CLASS constant specified in ' \ + 'the view file \'%s\'.' % item) + 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 self._section_view.needs_restart: + self._section_toolbar.accept_button.set_sensitive(False) + self._section_toolbar.cancel_button.set_sensitive(False) + alert = Alert() + alert.props.title = _('Warning') + alert.props.msg = _('Changes require restart') + + icon = Icon(icon_name='dialog-cancel') + alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel changes'), icon) + icon.show() + + if self._current_option != 'aboutme': + icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_ACCEPT, _('Later'), icon) + icon.show() + + icon = Icon(icon_name='system-restart') + alert.add_button(gtk.RESPONSE_APPLY, _('Restart now'), icon) + icon.show() + + self._vbox.pack_start(alert, False) + self._vbox.reorder_child(alert, 2) + alert.connect('response', self.__response_cb) + alert.show() + else: + self._show_main_view() + + def __response_cb(self, alert, response_id): + self._vbox.remove(alert) + self._section_toolbar.accept_button.set_sensitive(True) + self._section_toolbar.cancel_button.set_sensitive(True) + if response_id is gtk.RESPONSE_CANCEL: + self._section_view.undo() + self._section_view.setup() + self._options[self._current_option]['alerts'] = [] + elif response_id is gtk.RESPONSE_ACCEPT: + self._options[self._current_option]['alerts'] = \ + self._section_view.restart_alerts + self._show_main_view() + elif response_id is gtk.RESPONSE_APPLY: + session_manager = get_session_manager() + session_manager.logout() + + 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): + 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) + +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 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, detail: + _logger.debug('Error undo option: %s' % detail) + +class _SectionIcon(gtk.EventBox): + __gtype_name__ = "SugarSectionIcon" + + __gproperties__ = { + 'icon-name' : (str, None, None, None, + gobject.PARAM_READWRITE), + 'pixel-size' : (object, None, None, + gobject.PARAM_READWRITE), + 'xo-color' : (object, None, None, + gobject.PARAM_READWRITE), + 'title' : (str, None, None, None, + gobject.PARAM_READWRITE) + } + + def __init__(self, **kwargs): + self._icon_name = None + self._pixel_size = style.GRID_CELL_SIZE + self._xo_color = None + self._title = 'No Title' + + gobject.GObject.__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) + + self._label = gtk.Label(self._title) + self._label.modify_fg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + self._vbox.pack_start(self._label, expand=False, fill=False) + + self._vbox.set_spacing(style.DEFAULT_SPACING) + self.set_visible_window(False) + self.set_app_paintable(True) + self.set_events(gtk.gdk.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 + elif pspec.name == 'pixel-size': + return self._pixel_size + elif pspec.name == 'xo-color': + return self._xo_color + elif pspec.name == 'title': + return self._title diff --git a/src/jarabe/controlpanel/inlinealert.py b/src/jarabe/controlpanel/inlinealert.py new file mode 100644 index 0000000..619a379 --- /dev/null +++ b/src/jarabe/controlpanel/inlinealert.py @@ -0,0 +1,83 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +import pango + +from sugar.graphics import style +from sugar.graphics.icon import Icon + +class InlineAlert(gtk.HBox): + """UI interface for Inline alerts + + Inline alerts are different from the other alerts beause they are + no dialogs, they only inform about a current event. + + Properties: + 'msg': the message of the alert, + 'icon': the icon that appears at the far left + See __gproperties__ + """ + + __gtype_name__ = 'SugarInlineAlert' + + __gproperties__ = { + 'msg' : (str, None, None, None, + gobject.PARAM_READWRITE), + 'icon' : (object, None, None, + gobject.PARAM_WRITABLE) + } + + def __init__(self, **kwargs): + + self._msg = None + self._msg_color = None + self._icon = Icon(icon_name='emblem-warning', + fill_color=style.COLOR_SELECTION_GREY.get_svg(), + stroke_color=style.COLOR_WHITE.get_svg()) + + self._msg_label = gtk.Label() + self._msg_label.set_max_width_chars(50) + self._msg_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) + self._msg_label.set_alignment(0, 0.5) + self._msg_label.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + + gobject.GObject.__init__(self, **kwargs) + + self.set_spacing(style.DEFAULT_SPACING) + self.modify_bg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + + self.pack_start(self._icon, False) + self.pack_start(self._msg_label, False) + self._msg_label.show() + self._icon.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'msg': + if self._msg != value: + self._msg = value + self._msg_label.set_markup(self._msg) + elif pspec.name == 'icon': + if self._icon != value: + self._icon = value + + def do_get_property(self, pspec): + if pspec.name == 'msg': + return self._msg + diff --git a/src/jarabe/controlpanel/language/Makefile.am b/src/jarabe/controlpanel/language/Makefile.am new file mode 100644 index 0000000..ed8992c --- /dev/null +++ b/src/jarabe/controlpanel/language/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/jarabe/controlpanel/language + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py diff --git a/src/jarabe/controlpanel/language/__init__.py b/src/jarabe/controlpanel/language/__init__.py new file mode 100644 index 0000000..a8f9f08 --- /dev/null +++ b/src/jarabe/controlpanel/language/__init__.py @@ -0,0 +1,22 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +CLASS = 'Language' +ICON = 'module-language' +TITLE = _('Language') + diff --git a/src/jarabe/controlpanel/language/model.py b/src/jarabe/controlpanel/language/model.py new file mode 100644 index 0000000..404d9dd --- /dev/null +++ b/src/jarabe/controlpanel/language/model.py @@ -0,0 +1,135 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +# +# The language config is based on the system-config-language +# (http://fedoraproject.org/wiki/SystemConfig/language) tool +# Parts of the code were reused. +# + +import os +from gettext import gettext as _ +import subprocess + +_default_lang = 'en_US.utf8' +_standard_msg = _("Could not access ~/.i18n. Create standard settings.") + +def read_all_languages(): + fdp = subprocess.Popen(['locale', '-av'], stdout=subprocess.PIPE) + lines = fdp.stdout.read().split('\n') + locales = [] + + for line in lines: + if line.find('locale:') != -1: + locale = line.lstrip('locale:') + locale = locale.split('archive:')[0].strip() + elif line.find('language |') != -1: + lang = line.lstrip('language |') + elif line.find('territory |') != -1: + territory = line.lstrip('territory |') + if locale.endswith('utf8') and len(lang): + locales.append((lang, territory, locale)) + + #FIXME: This is a temporary workaround for locales that are essential to + # OLPC, but are not in Glibc yet. + locales.append(('Kreyol', 'Haiti', 'ht_HT.utf8')) + locales.append(('Dari', 'Afghanistan', 'fa_AF.utf8')) + locales.append(('Pashto', 'Afghanistan', 'ps_AF.utf8')) + + locales.sort() + return locales + +def _initialize(): + if set_language.__doc__ is None: + # when running under 'python -OO', all __doc__ fields are None, + # so += would fail -- and this function would be unnecessary anyway. + return + languages = read_all_languages() + set_language.__doc__ += '\n' + for lang in languages: + set_language.__doc__ += '%s \n' % (lang[0].replace(' ', '_') + '/' + + lang[1].replace(' ', '_')) + +def _write_i18n(lang): + path = os.path.join(os.environ.get("HOME"), '.i18n') + if os.access(path, os.W_OK) == 0: + print _standard_msg + fd = open(path, 'w') + fd.write('LANG="%s"\n' % _default_lang) + fd.close() + else: + fd = open(path, 'r') + lines = fd.readlines() + fd.close() + for i in range(len(lines)): + if lines[i][:5] == "LANG=": + lines[i] = 'LANG="%s"\n' % lang + fd = open(path, 'w') + fd.writelines(lines) + fd.close() + +def get_language(): + path = os.path.join(os.environ.get("HOME"), '.i18n') + if os.access(path, os.R_OK) == 0: + print _standard_msg + fd = open(path, 'w') + fd.write('LANG="%s"\n' % _default_lang) + fd.close() + return _default_lang + + fd = open(path, "r") + lines = fd.readlines() + fd.close() + + lang = None + + for line in lines: + if line[:5] == "LANG=": + lang = line[5:].replace('"', '') + lang = lang.strip() + + return lang + +def print_language(): + code = get_language() + + languages = read_all_languages() + for lang in languages: + if lang[2].split('.')[0] == code.split('.')[0]: + print lang[0].replace(' ', '_') + '/' + lang[1].replace(' ', '_') + return + print (_("Language for code=%s could not be determined.") % code) + +def set_language(language): + """Set the system language. + languages : + """ + if language.endswith('utf8'): + _write_i18n(language) + return 1 + else: + languages = read_all_languages() + for lang, territory, locale in languages: + code = lang.replace(' ', '_') + '/' \ + + territory.replace(' ', '_') + if code == language: + _write_i18n(locale) + return 1 + print (_("Sorry I do not speak \'%s\'.") % language) + +# inilialize the docstrings for the language +_initialize() + diff --git a/src/jarabe/controlpanel/language/view.py b/src/jarabe/controlpanel/language/view.py new file mode 100644 index 0000000..71a185f --- /dev/null +++ b/src/jarabe/controlpanel/language/view.py @@ -0,0 +1,142 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +from gettext import gettext as _ + +from sugar.graphics import style +from sugar.graphics import iconentry + +from jarabe.controlpanel.sectionview import SectionView +from jarabe.controlpanel.inlinealert import InlineAlert + +class Language(SectionView): + def __init__(self, model, alerts): + SectionView.__init__(self) + + self._model = model + self.restart_alerts = alerts + self._lang_sid = 0 + self._cursor_change_handler = None + + self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_spacing(style.DEFAULT_SPACING) + + self.connect("realize", self.__realize_cb) + + self._entry = iconentry.IconEntry() + self._entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self._entry.add_clear_button() + self._entry.modify_bg(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self._entry.modify_base(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self.pack_start(self._entry, False) + self._entry.show() + + self._scrolled_window = gtk.ScrolledWindow() + self._scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + self._scrolled_window.set_shadow_type(gtk.SHADOW_IN) + + self._store = gtk.ListStore(gobject.TYPE_STRING, + gobject.TYPE_STRING) + locales = model.read_all_languages() + for locale in locales: + self._store.append([locale[2], '%s (%s)' % + (locale[0], locale[1])]) + + self._treeview = gtk.TreeView(self._store) + self._treeview.set_search_entry(self._entry) + self._treeview.set_search_equal_func(self._search) + self._treeview.set_search_column(1) + self._scrolled_window.add(self._treeview) + self._treeview.show() + + self._language_column = gtk.TreeViewColumn(_('Language')) + self._cell = gtk.CellRendererText() + self._language_column.pack_start(self._cell, True) + self._language_column.add_attribute(self._cell, 'text', 1) + self._language_column.set_sort_column_id(1) + self._treeview.append_column(self._language_column) + + self.pack_start(self._scrolled_window) + self._scrolled_window.show() + + self._lang_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + self.pack_start(self._lang_alert_box, False) + + self._lang_alert = InlineAlert() + self._lang_alert_box.pack_start(self._lang_alert) + if 'lang' in self.restart_alerts: + self._lang_alert.props.msg = self.restart_msg + self._lang_alert.show() + self._lang_alert_box.show() + + self.setup() + + def setup(self): + lang_code = self._model.get_language() + for row in self._store: + lang = lang_code.split('.')[0] + lang_column = row[0].split('.')[0] + if lang in lang_column: + self._treeview.set_cursor(row.path, self._language_column, + False) + self._treeview.scroll_to_cell(row.path, self._language_column, + True, 0.5, 0.5) + break + + self.needs_restart = False + self._cursor_change_handler = self._treeview.connect( \ + "cursor-changed", self.__lang_changed_cd) + + def undo(self): + self._treeview.disconnect(self._cursor_change_handler) + self._model.undo() + self._lang_alert.hide() + + def __realize_cb(self, widget): + self._entry.grab_focus() + + def _search(self, model, column, key, iterator, data=None): + value = model.get_value(iterator, column) + if key.lower() in value.lower(): + return False + return True + + def __lang_changed_cd(self, treeview, data=None): + row = treeview.get_selection().get_selected() + if not row[1]: + return False + if self._model.get_language() == self._store.get_value(row[1], 0): + return False + + if self._lang_sid: + gobject.source_remove(self._lang_sid) + self._lang_sid = gobject.timeout_add(self._APPLY_TIMEOUT, + self.__lang_timeout_cb, + self._store.get_value(row[1], 0)) + + def __lang_timeout_cb(self, code): + self._lang_sid = 0 + self._model.set_language(code) + self.restart_alerts.append('lang') + self.needs_restart = True + self._lang_alert.props.msg = self.restart_msg + self._lang_alert.show() + return False diff --git a/src/jarabe/controlpanel/network/Makefile.am b/src/jarabe/controlpanel/network/Makefile.am new file mode 100644 index 0000000..e480aa3 --- /dev/null +++ b/src/jarabe/controlpanel/network/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/jarabe/controlpanel/network + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py diff --git a/src/jarabe/controlpanel/network/__init__.py b/src/jarabe/controlpanel/network/__init__.py new file mode 100644 index 0000000..8fea274 --- /dev/null +++ b/src/jarabe/controlpanel/network/__init__.py @@ -0,0 +1,25 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +CLASS = 'Network' +ICON = 'module-network' +TITLE = _('Network') +KEYWORDS = ['network', 'jabber', 'radio', 'server'] + + + diff --git a/src/jarabe/controlpanel/network/model.py b/src/jarabe/controlpanel/network/model.py new file mode 100644 index 0000000..b6f71a0 --- /dev/null +++ b/src/jarabe/controlpanel/network/model.py @@ -0,0 +1,101 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import dbus +from gettext import gettext as _ + +from sugar import profile +from jarabe.hardware import hardwaremanager + +NM_SERVICE_NAME = 'org.freedesktop.NetworkManager' +NM_SERVICE_PATH = '/org/freedesktop/NetworkManager' +NM_SERVICE_IFACE = 'org.freedesktop.NetworkManager' +NM_ASLEEP = 1 + +KEYWORDS = ['network', 'jabber', 'radio', 'server'] + +class ReadError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +def get_jabber(): + pro = profile.get_profile() + return pro.jabber_server + +def print_jabber(): + print get_jabber() + +def set_jabber(server): + """Set the jabber server + server : e.g. 'olpc.collabora.co.uk' + """ + pro = profile.get_profile() + pro.jabber_server = server + pro.jabber_registered = False + pro.save() + return 1 + +def get_radio(): + bus = dbus.SystemBus() + proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH) + nm = dbus.Interface(proxy, NM_SERVICE_IFACE) + state = nm.getWirelessEnabled() + if state in (0, 1): + return state + else: + raise ReadError(_('State is unknown.')) + +def print_radio(): + print ('off', 'on')[get_radio()] + +def set_radio(state): + """Turn Radio 'on' or 'off' + state : 'on/off' + """ + if state == 'on' or state == 1: + bus = dbus.SystemBus() + proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH) + nm = dbus.Interface(proxy, NM_SERVICE_IFACE) + nm.setWirelessEnabled(True) + elif state == 'off' or state == 0: + bus = dbus.SystemBus() + proxy = bus.get_object(NM_SERVICE_NAME, NM_SERVICE_PATH) + nm = dbus.Interface(proxy, NM_SERVICE_IFACE) + nm.setWirelessEnabled(False) + else: + raise ValueError(_("Error in specified radio argument use on/off.")) + + return 0 + +def clear_registration(): + """Clear the registration with the schoolserver + """ + pro = profile.get_profile() + pro.backup1 = None + pro.save() + return 1 + +def clear_networks(): + """Clear saved passwords and network configurations. + """ + network_manager = hardwaremanager.get_network_manager() + if not network_manager: + return + network_manager.nminfo.delete_all_networks() + return 1 diff --git a/src/jarabe/controlpanel/network/view.py b/src/jarabe/controlpanel/network/view.py new file mode 100644 index 0000000..4f4ada7 --- /dev/null +++ b/src/jarabe/controlpanel/network/view.py @@ -0,0 +1,231 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +from gettext import gettext as _ + +from sugar.graphics import style + +from jarabe.controlpanel.sectionview import SectionView +from jarabe.controlpanel.inlinealert import InlineAlert + +CLASS = 'Network' +ICON = 'module-network' +TITLE = _('Network') + +class Network(SectionView): + def __init__(self, model, alerts): + SectionView.__init__(self) + + self._model = model + self.restart_alerts = alerts + self._jabber_sid = 0 + self._jabber_valid = True + self._radio_valid = True + self._jabber_change_handler = None + self._radio_change_handler = None + self._network_configuration_reset_handler = None + + self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_spacing(style.DEFAULT_SPACING) + group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + + self._radio_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + self._jabber_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + + separator_wireless = gtk.HSeparator() + self.pack_start(separator_wireless, expand=False) + separator_wireless.show() + + label_wireless = gtk.Label(_('Wireless')) + label_wireless.set_alignment(0, 0) + self.pack_start(label_wireless, expand=False) + label_wireless.show() + box_wireless = gtk.VBox() + box_wireless.set_border_width(style.DEFAULT_SPACING * 2) + box_wireless.set_spacing(style.DEFAULT_SPACING) + + radio_info = gtk.Label(_("Turn of the wireless radio to save " + "battery life")) + radio_info.set_alignment(0, 0) + radio_info.set_line_wrap(True) + radio_info.show() + box_wireless.pack_start(radio_info, expand=False) + + box_radio = gtk.HBox(spacing=style.DEFAULT_SPACING) + self._button = gtk.CheckButton() + self._button.set_alignment(0, 0) + box_radio.pack_start(self._button, expand=False) + self._button.show() + + label_radio = gtk.Label(_('Radio')) + label_radio.set_alignment(0, 0.5) + box_radio.pack_start(label_radio, expand=False) + label_radio.show() + + box_wireless.pack_start(box_radio, expand=False) + box_radio.show() + + self._radio_alert = InlineAlert() + self._radio_alert_box.pack_start(self._radio_alert, expand=False) + box_radio.pack_end(self._radio_alert_box, expand=False) + self._radio_alert_box.show() + if 'radio' in self.restart_alerts: + self._radio_alert.props.msg = self.restart_msg + self._radio_alert.show() + + history_info = gtk.Label(_("Discard network history if you " + "have trouble connecting to the network")) + history_info.set_alignment(0, 0) + history_info.set_line_wrap(True) + history_info.show() + box_wireless.pack_start(history_info, expand=False) + + box_clear_history = gtk.HBox(spacing=style.DEFAULT_SPACING) + self._clear_history_button = gtk.Button() + self._clear_history_button.set_label(_('Discard network history')) + box_clear_history.pack_start(self._clear_history_button, expand=False) + self._clear_history_button.show() + box_wireless.pack_start(box_clear_history, expand=False) + box_clear_history.show() + + self.pack_start(box_wireless, expand=False) + box_wireless.show() + + separator_mesh = gtk.HSeparator() + self.pack_start(separator_mesh, False) + separator_mesh.show() + + label_mesh = gtk.Label(_('Mesh')) + label_mesh.set_alignment(0, 0) + self.pack_start(label_mesh, expand=False) + label_mesh.show() + box_mesh = gtk.VBox() + box_mesh.set_border_width(style.DEFAULT_SPACING * 2) + box_mesh.set_spacing(style.DEFAULT_SPACING) + + box_server = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_server = gtk.Label(_('Server:')) + label_server.set_alignment(1, 0.5) + label_server.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + box_server.pack_start(label_server, expand=False) + group.add_widget(label_server) + label_server.show() + self._entry = gtk.Entry() + self._entry.set_alignment(0) + self._entry.modify_bg(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self._entry.modify_base(gtk.STATE_INSENSITIVE, + style.COLOR_WHITE.get_gdk_color()) + self._entry.set_size_request(int(gtk.gdk.screen_width() / 3), -1) + box_server.pack_start(self._entry, expand=False) + self._entry.show() + box_mesh.pack_start(box_server, expand=False) + box_server.show() + + self._jabber_alert = InlineAlert() + label_jabber_error = gtk.Label() + group.add_widget(label_jabber_error) + self._jabber_alert_box.pack_start(label_jabber_error, expand=False) + label_jabber_error.show() + self._jabber_alert_box.pack_start(self._jabber_alert, expand=False) + box_mesh.pack_end(self._jabber_alert_box, expand=False) + self._jabber_alert_box.show() + if 'jabber' in self.restart_alerts: + self._jabber_alert.props.msg = self.restart_msg + self._jabber_alert.show() + + self.pack_start(box_mesh, expand=False) + box_mesh.show() + + self.setup() + + def setup(self): + self._entry.set_text(self._model.get_jabber()) + try: + radio_state = self._model.get_radio() + except Exception, detail: + self._radio_alert.props.msg = detail + self._radio_alert.show() + else: + self._button.set_active(radio_state) + + self._jabber_valid = True + self._radio_valid = True + self.needs_restart = False + self._radio_change_handler = self._button.connect( \ + 'toggled', self.__radio_toggled_cb) + self._jabber_change_handler = self._entry.connect( \ + 'changed', self.__jabber_changed_cb) + self._network_configuration_reset_handler = \ + self._clear_history_button.connect( \ + 'clicked', self.__network_configuration_reset_cb) + + def undo(self): + self._button.disconnect(self._radio_change_handler) + self._entry.disconnect(self._jabber_change_handler) + self._model.undo() + self._jabber_alert.hide() + self._radio_alert.hide() + + def _validate(self): + if self._jabber_valid and self._radio_valid: + self.props.is_valid = True + else: + self.props.is_valid = False + + def __radio_toggled_cb(self, widget, data=None): + radio_state = widget.get_active() + try: + self._model.set_radio(radio_state) + except Exception, detail: + self._radio_alert.props.msg = detail + self._radio_valid = False + else: + self._radio_valid = True + + self._validate() + return False + + def __jabber_changed_cb(self, widget, data=None): + if self._jabber_sid: + gobject.source_remove(self._jabber_sid) + self._jabber_sid = gobject.timeout_add(self._APPLY_TIMEOUT, + self.__jabber_timeout_cb, widget) + + def __jabber_timeout_cb(self, widget): + self._jabber_sid = 0 + if widget.get_text() == self._model.get_jabber: + return + try: + self._model.set_jabber(widget.get_text()) + except ValueError, detail: + self._jabber_alert.props.msg = detail + self._jabber_valid = False + else: + self._jabber_alert.props.msg = self.restart_msg + self._jabber_valid = True + self.needs_restart = True + self.restart_alerts.append('jabber') + + self._validate() + self._jabber_alert.show() + return False + + def __network_configuration_reset_cb(self, widget): + self._model.clear_networks() diff --git a/src/jarabe/controlpanel/power/Makefile.am b/src/jarabe/controlpanel/power/Makefile.am new file mode 100644 index 0000000..e989e97 --- /dev/null +++ b/src/jarabe/controlpanel/power/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/jarabe/controlpanel/power + +sugar_PYTHON = \ + __init__.py \ + model.py \ + view.py diff --git a/src/jarabe/controlpanel/power/__init__.py b/src/jarabe/controlpanel/power/__init__.py new file mode 100644 index 0000000..8b2e85f --- /dev/null +++ b/src/jarabe/controlpanel/power/__init__.py @@ -0,0 +1,23 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +CLASS = 'Power' +ICON = 'module-power' +TITLE = _('Power') +KEYWORDS = ['automatic', 'extreme', 'power', 'suspend', 'battery'] + diff --git a/src/jarabe/controlpanel/power/model.py b/src/jarabe/controlpanel/power/model.py new file mode 100644 index 0000000..47af483 --- /dev/null +++ b/src/jarabe/controlpanel/power/model.py @@ -0,0 +1,89 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +from gettext import gettext as _ + +from sugar import profile +import dbus + +OHM_SERVICE_NAME = 'org.freedesktop.ohm' +OHM_SERVICE_PATH = '/org/freedesktop/ohm/Keystore' +OHM_SERVICE_IFACE = 'org.freedesktop.ohm.Keystore' + +class ReadError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +def get_automatic_pm(): + pro = profile.get_profile() + ret = pro.automatic_pm + return ret + +def print_automatic_pm(): + print ('off', 'on')[get_automatic_pm()] + +def set_automatic_pm(enabled): + """Automatic suspends on/off.""" + + bus = dbus.SystemBus() + proxy = bus.get_object(OHM_SERVICE_NAME, OHM_SERVICE_PATH) + keystore = dbus.Interface(proxy, OHM_SERVICE_IFACE) + + if enabled == 'on' or enabled == 1: + keystore.SetKey("suspend.automatic_pm", 1) + enabled = True + elif enabled == 'off' or enabled == 0: + keystore.SetKey("suspend.automatic_pm", 0) + enabled = False + else: + raise ValueError(_("Error in automatic pm argument, use on/off.")) + + pro = profile.get_profile() + pro.automatic_pm = enabled + pro.save() + return 0 + +def get_extreme_pm(): + pro = profile.get_profile() + ret = pro.extreme_pm + return ret + +def print_extreme_pm(): + print ('off', 'on')[get_extreme_pm()] + +def set_extreme_pm(enabled): + """Extreme power management on/off.""" + + bus = dbus.SystemBus() + proxy = bus.get_object(OHM_SERVICE_NAME, OHM_SERVICE_PATH) + keystore = dbus.Interface(proxy, OHM_SERVICE_IFACE) + + if enabled == 'on' or enabled == 1: + keystore.SetKey("suspend.extreme_pm", 1) + enabled = True + elif enabled == 'off' or enabled == 0: + keystore.SetKey("suspend.extreme_pm", 0) + enabled = False + else: + raise ValueError(_("Error in extreme pm argument, use on/off.")) + + pro = profile.get_profile() + pro.extreme_pm = enabled + pro.save() + return 0 diff --git a/src/jarabe/controlpanel/power/view.py b/src/jarabe/controlpanel/power/view.py new file mode 100644 index 0000000..8f1ed56 --- /dev/null +++ b/src/jarabe/controlpanel/power/view.py @@ -0,0 +1,177 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +from gettext import gettext as _ + +from sugar.graphics import style + +from jarabe.controlpanel.sectionview import SectionView +from jarabe.controlpanel.inlinealert import InlineAlert + +class Power(SectionView): + def __init__(self, model, alerts): + SectionView.__init__(self) + + self._model = model + self.restart_alerts = alerts + self._automatic_pm_valid = True + self._extreme_pm_valid = True + self._extreme_pm_change_handler = None + self._automatic_pm_change_handler = None + + self.set_border_width(style.DEFAULT_SPACING * 2) + self.set_spacing(style.DEFAULT_SPACING) + group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + + self._automatic_pm_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + self._extreme_pm_alert_box = gtk.HBox(spacing=style.DEFAULT_SPACING) + + separator_pm = gtk.HSeparator() + self.pack_start(separator_pm, expand=False) + separator_pm.show() + + label_pm = gtk.Label(_('Power management')) + label_pm.set_alignment(0, 0) + self.pack_start(label_pm, expand=False) + label_pm.show() + box_pm = gtk.VBox() + box_pm.set_border_width(style.DEFAULT_SPACING * 2) + box_pm.set_spacing(style.DEFAULT_SPACING) + + box_automatic_pm = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_automatic_pm = gtk.Label( + _('Automatic power management (increases battery life)')) + label_automatic_pm.set_alignment(0, 0.5) + self._automatic_button = gtk.CheckButton() + self._automatic_button.set_alignment(0, 0) + box_automatic_pm.pack_start(self._automatic_button, expand=False) + box_automatic_pm.pack_start(label_automatic_pm, expand=False) + self._automatic_button.show() + label_automatic_pm.show() + group.add_widget(label_automatic_pm) + box_pm.pack_start(box_automatic_pm, expand=False) + box_automatic_pm.show() + + self._automatic_pm_alert = InlineAlert() + label_automatic_pm_error = gtk.Label() + group.add_widget(label_automatic_pm_error) + self._automatic_pm_alert_box.pack_start(label_automatic_pm_error, + expand=False) + label_automatic_pm_error.show() + self._automatic_pm_alert_box.pack_start(self._automatic_pm_alert, + expand=False) + box_pm.pack_end(self._automatic_pm_alert_box, expand=False) + self._automatic_pm_alert_box.show() + if 'automatic_pm' in self.restart_alerts: + self._automatic_pm_alert.props.msg = self.restart_msg + self._automatic_pm_alert.show() + + box_extreme_pm = gtk.HBox(spacing=style.DEFAULT_SPACING) + label_extreme_pm = gtk.Label( + _('Extreme power management (disables' \ + 'wireless radio, increases battery life)')) + label_extreme_pm.set_alignment(0, 0.5) + self._extreme_button = gtk.CheckButton() + self._extreme_button.set_alignment(0, 0) + box_extreme_pm.pack_start(self._extreme_button, expand=False) + self._extreme_button.show() + box_extreme_pm.pack_start(label_extreme_pm, expand=False) + group.add_widget(label_extreme_pm) + label_extreme_pm.show() + box_pm.pack_start(box_extreme_pm, expand=False) + box_extreme_pm.show() + + self._extreme_pm_alert = InlineAlert() + label_extreme_pm_error = gtk.Label() + group.add_widget(label_extreme_pm_error) + self._extreme_pm_alert_box.pack_start(label_extreme_pm_error, + expand=False) + label_extreme_pm_error.show() + self._extreme_pm_alert_box.pack_start(self._extreme_pm_alert, + expand=False) + box_pm.pack_end(self._extreme_pm_alert_box, expand=False) + self._extreme_pm_alert_box.show() + if 'extreme_pm' in self.restart_alerts: + self._extreme_pm_alert.props.msg = self.restart_msg + self._extreme_pm_alert.show() + + self.pack_start(box_pm, expand=False) + box_pm.show() + + self.setup() + + def setup(self): + try: + automatic_state = self._model.get_automatic_pm() + extreme_state = self._model.get_extreme_pm() + + except Exception, detail: + self._automatic_pm_alert.props.msg = detail + self._automatic_pm_alert.show() + + self._extreme_pm_alert.props.msg = detail + self._extreme_pm_alert.show() + else: + self._automatic_button.set_active(automatic_state) + self._extreme_button.set_active(extreme_state) + + self._extreme_pm_valid = True + self._automatic_pm_valid = True + self.needs_restart = False + self._automatic_pm_change_handler = self._automatic_button.connect( \ + 'toggled', self.__automatic_pm_toggled_cb) + self._extreme_pm_change_handler = self._extreme_button.connect( \ + 'toggled', self.__extreme_pm_toggled_cb) + + def undo(self): + self._automatic_button.disconnect(self._automatic_pm_change_handler) + self._extreme_button.disconnect(self._extreme_pm_change_handler) + self._model.undo() + self._extreme_pm_alert.hide() + self._automatic_pm_alert.hide() + + def _validate(self): + if self._extreme_pm_valid and self._automatic_pm_valid: + self.props.is_valid = True + else: + self.props.is_valid = False + + def __automatic_pm_toggled_cb(self, widget, data=None): + state = widget.get_active() + try: + self._model.set_automatic_pm(state) + except Exception, detail: + print detail + self._automatic_pm_alert.props.msg = detail + else: + self._automatic_pm_valid = True + + self._validate() + return False + + def __extreme_pm_toggled_cb(self, widget, data=None): + state = widget.get_active() + try: + self._model.set_extreme_pm(state) + except Exception, detail: + print detail + self._extreme_pm_alert.props.msg = detail + else: + self._extreme_pm_valid = True + + self._validate() + return False diff --git a/src/jarabe/controlpanel/sectionview.py b/src/jarabe/controlpanel/sectionview.py new file mode 100644 index 0000000..7b76aa1 --- /dev/null +++ b/src/jarabe/controlpanel/sectionview.py @@ -0,0 +1,55 @@ +# Copyright (C) 2008, OLPC +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk +from gettext import gettext as _ + +class SectionView(gtk.VBox): + __gtype_name__ = 'SugarSectionView' + + __gsignals__ = { + 'request-close': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + __gproperties__ = { + 'is_valid' : (bool, None, None, True, + gobject.PARAM_READWRITE) + } + + _APPLY_TIMEOUT = 1000 + + def __init__(self): + gtk.VBox.__init__(self) + self._is_valid = True + self.auto_close = False + self.needs_restart = False + self.restart_alerts = [] + self.restart_msg = _('Changes require restart') + + def do_set_property(self, pspec, value): + if pspec.name == 'is-valid': + if self._is_valid != value: + self._is_valid = value + + def do_get_property(self, pspec): + if pspec.name == 'is-valid': + return self._is_valid + + def undo(self): + '''Undo here the changes that have been made in this section.''' + pass diff --git a/src/jarabe/controlpanel/toolbar.py b/src/jarabe/controlpanel/toolbar.py new file mode 100644 index 0000000..6bb8328 --- /dev/null +++ b/src/jarabe/controlpanel/toolbar.py @@ -0,0 +1,157 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gettext +import gobject + +_ = lambda msg: gettext.dgettext('sugar', msg) + +from sugar.graphics.icon import Icon +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics import iconentry +from sugar.graphics import style + +class MainToolbar(gtk.Toolbar): + """ Main toolbar of the control panel + """ + __gtype_name__ = 'MainToolbar' + + __gsignals__ = { + 'stop-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'search-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])) + } + def __init__(self): + gtk.Toolbar.__init__(self) + + self._add_separator() + + tool_item = gtk.ToolItem() + self.insert(tool_item, -1) + tool_item.show() + self._search_entry = iconentry.IconEntry() + self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self._search_entry.add_clear_button() + self._search_entry.set_width_chars(25) + self._search_entry.connect('changed', self.__search_entry_changed_cb) + tool_item.add(self._search_entry) + self._search_entry.show() + + self._add_separator(True) + + self.stop = ToolButton(icon_name='dialog-cancel') + self.stop.set_tooltip(_('Done')) + self.stop.connect('clicked', self.__stop_clicked_cb) + self.stop.show() + self.insert(self.stop, -1) + self.stop.show() + + def get_entry(self): + return self._search_entry + + def _add_separator(self, expand=False): + separator = gtk.SeparatorToolItem() + separator.props.draw = False + if expand: + separator.set_expand(True) + else: + separator.set_size_request(style.DEFAULT_SPACING, -1) + self.insert(separator, -1) + separator.show() + + def __search_entry_changed_cb(self, search_entry): + self.emit('search-changed', search_entry.props.text) + + def __stop_clicked_cb(self, button): + self.emit('stop-clicked') + +class SectionToolbar(gtk.Toolbar): + """ Toolbar of the sections of the control panel + """ + __gtype_name__ = 'SectionToolbar' + + __gsignals__ = { + 'cancel-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'accept-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])) + } + def __init__(self): + gtk.Toolbar.__init__(self) + + self._add_separator() + + self._icon = Icon() + self._add_widget(self._icon) + + self._add_separator() + + self._title = gtk.Label() + self._add_widget(self._title) + + self._add_separator(True) + + self.cancel_button = ToolButton('dialog-cancel') + self.cancel_button.set_tooltip(_('Cancel')) + self.cancel_button.connect('clicked', self.__cancel_button_clicked_cb) + self.insert(self.cancel_button, -1) + self.cancel_button.show() + + self.accept_button = ToolButton('dialog-ok') + self.accept_button.set_tooltip(_('Ok')) + self.accept_button.connect('clicked', self.__accept_button_clicked_cb) + self.insert(self.accept_button, -1) + self.accept_button.show() + + def get_icon(self): + return self._icon + + def get_title(self): + return self._title + + def _add_separator(self, expand=False): + separator = gtk.SeparatorToolItem() + separator.props.draw = False + if expand: + separator.set_expand(True) + else: + separator.set_size_request(style.DEFAULT_SPACING, -1) + self.insert(separator, -1) + separator.show() + + 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 __cancel_button_clicked_cb(self, widget, data=None): + self.emit('cancel-clicked') + + def __accept_button_clicked_cb(self, widget, data=None): + self.emit('accept-clicked') + diff --git a/src/jarabe/emulator.py b/src/jarabe/emulator.py new file mode 100755 index 0000000..ac1f4e2 --- /dev/null +++ b/src/jarabe/emulator.py @@ -0,0 +1,134 @@ +# Copyright (C) 2006, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import subprocess +import time +from optparse import OptionParser + +log = logging.getLogger( 'sugar-emulator' ) +log.setLevel( logging.DEBUG ) + +import pygtk +pygtk.require('2.0') +import gtk +import gobject + +from sugar import env + +def _run_xephyr(display, dpi): + log.info('Starting Xephyr on display %s', display) + + cmd = [ 'Xephyr' ] + cmd.append(':%d' % display) + cmd.append('-ac') + + if gtk.gdk.screen_width() < 1200 or gtk.gdk.screen_height() < 900: + cmd.append('-fullscreen') + else: + cmd.append('-screen') + cmd.append('%dx%d' % (1200, 900)) + + if not dpi: + dpi = gtk.settings_get_default().get_property('gtk-xft-dpi') / 1024 + if dpi > 0: + cmd.append('-dpi') + cmd.append('%d' % dpi) + + log.debug('Xephyr command: %s', " ".join( cmd )) + result = gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) + pid = result[0] + + os.environ['DISPLAY'] = ":%d" % (display) + os.environ['SUGAR_EMULATOR_PID'] = str(pid) + +def _check_xephyr(display): + result = subprocess.call(['xdpyinfo', '-display', ':%d' % display], + stdout=open(os.devnull, "w"), + stderr=open(os.devnull, "w")) + return result == 0 + +def _start_xephyr(dpi=None): + for display in range(100, 110): + if not _check_xephyr(display): + _run_xephyr(display, dpi) + + tries = 10 + while tries > 0: + if _check_xephyr(display): + return + else: + tries -= 1 + time.sleep(0.1) + +def _start_matchbox(): + log.info('Starting the matchbox window manager') + cmd = ['matchbox-window-manager'] + + cmd.extend(['-use_titlebar', 'no']) + cmd.extend(['-theme', 'sugar']) + + log.debug('Matchbox command: %s', " ".join( cmd)) + gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) + +def _setup_env(): + os.environ['SUGAR_EMULATOR'] = 'yes' + os.environ['GABBLE_LOGFILE'] = os.path.join( + env.get_profile_path(), 'logs', 'telepathy-gabble.log') + os.environ['SALUT_LOGFILE'] = os.path.join( + env.get_profile_path(), 'logs', 'telepathy-salut.log') + os.environ['STREAM_ENGINE_LOGFILE'] = os.path.join( + env.get_profile_path(), 'logs', 'telepathy-stream-engine.log') + +def main(): + """Script-level operations""" + + parser = OptionParser() + parser.add_option('-x', '--xo-style', dest='xo_style', + action='store_true', help='use the XO style') + (options, args) = parser.parse_args() + + logging.basicConfig() + + _setup_env() + + if options.xo_style: + _start_xephyr(dpi=201) + else: + _start_xephyr() + + if options.xo_style: + os.environ['SUGAR_THEME'] = 'sugar-xo' + os.environ['SUGAR_XO_STYLE'] = 'yes' + else: + os.environ['SUGAR_XO_STYLE'] = 'no' + + command = ['dbus-launch', 'dbus-launch', '--exit-with-session'] + + if not args: + command.append('sugar-shell') + else: + _start_matchbox() + + if args[0].endswith('.py'): + command.append('python') + + command.append(args[0]) + + log.info("Attempting to launch sugar to replace this process: %s" + % " ".join(command)) + os.execlp( *command ) diff --git a/src/jarabe/hardware/Makefile.am b/src/jarabe/hardware/Makefile.am new file mode 100644 index 0000000..6684848 --- /dev/null +++ b/src/jarabe/hardware/Makefile.am @@ -0,0 +1,13 @@ +sugardir = $(pythondir)/jarabe/hardware +sugar_PYTHON = \ + __init__.py \ + hardwaremanager.py \ + keydialog.py \ + nmclient.py \ + nminfo.py \ + schoolserver.py + +dbusservicedir = $(sysconfdir)/dbus-1/system.d/ +dbusservice_DATA = NetworkManagerInfo.conf + +EXTRA_DIST = $(dbusservice_DATA) diff --git a/src/jarabe/hardware/NetworkManagerInfo.conf b/src/jarabe/hardware/NetworkManagerInfo.conf new file mode 100644 index 0000000..4fb8270 --- /dev/null +++ b/src/jarabe/hardware/NetworkManagerInfo.conf @@ -0,0 +1,26 @@ +<!DOCTYPE busconfig PUBLIC + "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <policy user="root"> + <allow own="org.freedesktop.NetworkManagerInfo"/> + + <allow send_destination="org.freedesktop.NetworkManagerInfo"/> + <allow send_interface="org.freedesktop.NetworkManagerInfo"/> + </policy> + <policy at_console="true"> + <allow own="org.freedesktop.NetworkManagerInfo"/> + + <allow send_destination="org.freedesktop.NetworkManagerInfo"/> + <allow send_interface="org.freedesktop.NetworkManagerInfo"/> + </policy> + <policy context="default"> + <deny own="org.freedesktop.NetworkManagerInfo"/> + + <deny send_destination="org.freedesktop.NetworkManagerInfo"/> + <deny send_interface="org.freedesktop.NetworkManagerInfo"/> + </policy> + + <limit name="max_replies_per_connection">512</limit> +</busconfig> + diff --git a/src/jarabe/hardware/__init__.py b/src/jarabe/hardware/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/hardware/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/hardware/hardwaremanager.py b/src/jarabe/hardware/hardwaremanager.py new file mode 100644 index 0000000..22cb1db --- /dev/null +++ b/src/jarabe/hardware/hardwaremanager.py @@ -0,0 +1,123 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +import gobject + +from jarabe.hardware.nmclient import NMClient +from sugar.profile import get_profile +from sugar import env +from sugar import _sugarext + +_HARDWARE_MANAGER_INTERFACE = 'org.freedesktop.ohm.Keystore' +_HARDWARE_MANAGER_SERVICE = 'org.freedesktop.ohm' +_HARDWARE_MANAGER_OBJECT_PATH = '/org/freedesktop/ohm/Keystore' + +COLOR_MODE = 0 +B_AND_W_MODE = 1 + +VOL_CHANGE_INCREMENT_RECOMMENDATION = 10 + +class HardwareManager(gobject.GObject): + __gsignals__ = { + 'muted-changed' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN])), + 'volume-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_INT, gobject.TYPE_INT])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + bus = dbus.SystemBus() + proxy = bus.get_object(_HARDWARE_MANAGER_SERVICE, + _HARDWARE_MANAGER_OBJECT_PATH, + follow_name_owner_changes=True) + self._service = dbus.Interface(proxy, _HARDWARE_MANAGER_INTERFACE) + + self._volume = _sugarext.VolumeAlsa() + + def get_muted(self): + return self._volume.get_mute() + + def get_volume(self): + return self._volume.get_volume() + + def set_volume(self, new_volume): + old_volume = self._volume.get_volume() + self._volume.set_volume(new_volume) + + self.emit('volume-changed', old_volume, new_volume) + + def set_muted(self, new_state): + old_state = self._volume.get_mute() + self._volume.set_mute(new_state) + + self.emit('muted-changed', old_state, new_state) + + def startup(self): + if env.is_emulator() is False: + profile = get_profile() + self.set_volume(profile.sound_volume) + + def shutdown(self): + if env.is_emulator() is False: + profile = get_profile() + profile.sound_volume = self.get_volume() + profile.save() + + def set_dcon_freeze(self, frozen): + try: + self._service.SetKey("display.dcon_freeze", frozen) + except dbus.DBusException: + logging.error('Cannot unfreeze the DCON') + + def set_display_mode(self, mode): + try: + self._service.SetKey("display.dcon_mode", mode) + except dbus.DBusException: + logging.error('Cannot change DCON mode') + + def set_display_brightness(self, level): + try: + self._service.SetKey("backlight.hardware_brightness", level) + except dbus.DBusException: + logging.error('Cannot set display brightness') + + def get_display_brightness(self): + try: + return self._service.GetKey("backlight.hardware_brightness") + except dbus.DBusException: + logging.error('Cannot get display brightness') + return 0 + +def get_manager(): + return _manager + +def get_network_manager(): + return _network_manager + +_manager = HardwareManager() + +try: + _network_manager = NMClient() +except dbus.DBusException, e: + _network_manager = None + logging.info('Network manager service not found.') diff --git a/src/jarabe/hardware/keydialog.py b/src/jarabe/hardware/keydialog.py new file mode 100644 index 0000000..e70e12b --- /dev/null +++ b/src/jarabe/hardware/keydialog.py @@ -0,0 +1,354 @@ +# vi: ts=4 ai noet +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import md5 +from gettext import gettext as _ + +import gtk + +IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001 +IW_AUTH_ALG_SHARED_KEY = 0x00000002 + +IW_AUTH_WPA_VERSION_DISABLED = 0x00000001 +IW_AUTH_WPA_VERSION_WPA = 0x00000002 +IW_AUTH_WPA_VERSION_WPA2 = 0x00000004 + +NM_802_11_CAP_NONE = 0x00000000 +NM_802_11_CAP_PROTO_NONE = 0x00000001 +NM_802_11_CAP_PROTO_WEP = 0x00000002 +NM_802_11_CAP_PROTO_WPA = 0x00000004 +NM_802_11_CAP_PROTO_WPA2 = 0x00000008 +NM_802_11_CAP_KEY_MGMT_PSK = 0x00000040 +NM_802_11_CAP_KEY_MGMT_802_1X = 0x00000080 +NM_802_11_CAP_CIPHER_WEP40 = 0x00001000 +NM_802_11_CAP_CIPHER_WEP104 = 0x00002000 +NM_802_11_CAP_CIPHER_TKIP = 0x00004000 +NM_802_11_CAP_CIPHER_CCMP = 0x00008000 + +NM_AUTH_TYPE_WPA_PSK_AUTO = 0x00000000 +IW_AUTH_CIPHER_NONE = 0x00000001 +IW_AUTH_CIPHER_WEP40 = 0x00000002 +IW_AUTH_CIPHER_TKIP = 0x00000004 +IW_AUTH_CIPHER_CCMP = 0x00000008 +IW_AUTH_CIPHER_WEP104 = 0x00000010 + +IW_AUTH_KEY_MGMT_802_1X = 0x1 +IW_AUTH_KEY_MGMT_PSK = 0x2 + +def string_is_hex(key): + is_hex = True + for c in key: + if not 'a' <= c.lower() <= 'f' and not '0' <= c <= '9': + is_hex = False + return is_hex + +def string_is_ascii(string): + try: + string.encode('ascii') + return True + except UnicodeEncodeError: + return False + +def string_to_hex(passphrase): + key = '' + for c in passphrase: + key += '%02x' % ord(c) + return key + +def hash_passphrase(passphrase): + # passphrase must have a length of 64 + if len(passphrase) > 64: + passphrase = passphrase[:64] + elif len(passphrase) < 64: + while len(passphrase) < 64: + passphrase += passphrase[:64 - len(passphrase)] + passphrase = md5.new(passphrase).digest() + return string_to_hex(passphrase)[:26] + +class KeyDialog(gtk.Dialog): + def __init__(self, net, async_cb, async_err_cb): + gtk.Dialog.__init__(self, flags=gtk.DIALOG_MODAL) + self.set_title("Wireless Key Required") + + self._net = net + self._async_cb = async_cb + self._async_err_cb = async_err_cb + self._entry = None + + self.set_has_separator(False) + + label = gtk.Label("A wireless encryption key is required for\n" \ + " the wireless network '%s'." % net.get_ssid()) + self.vbox.pack_start(label) + + self.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK) + self.set_default_response(gtk.RESPONSE_OK) + self.set_has_separator(True) + + def add_key_entry(self): + self._entry = gtk.Entry() + #self._entry.props.visibility = False + self._entry.connect('changed', self._update_response_sensitivity) + self._entry.connect('activate', self._entry_activate_cb) + self.vbox.pack_start(self._entry) + self.vbox.set_spacing(6) + self.vbox.show_all() + + self._update_response_sensitivity() + self._entry.grab_focus() + + def _entry_activate_cb(self, entry): + self.response(gtk.RESPONSE_OK) + + def create_security(self): + raise NotImplementedError + + def get_network(self): + return self._net + + def get_callbacks(self): + return (self._async_cb, self._async_err_cb) + +WEP_PASSPHRASE = 1 +WEP_HEX = 2 +WEP_ASCII = 3 + +class WEPKeyDialog(KeyDialog): + def __init__(self, net, async_cb, async_err_cb): + KeyDialog.__init__(self, net, async_cb, async_err_cb) + + # WEP key type + self.key_store = gtk.ListStore(str, int) + self.key_store.append(["Passphrase (128-bit)", WEP_PASSPHRASE]) + self.key_store.append(["Hex (40/128-bit)", WEP_HEX]) + self.key_store.append(["ASCII (40/128-bit)", WEP_ASCII]) + + self.key_combo = gtk.ComboBox(self.key_store) + cell = gtk.CellRendererText() + self.key_combo.pack_start(cell, True) + self.key_combo.add_attribute(cell, 'text', 0) + self.key_combo.set_active(0) + self.key_combo.connect('changed', self._key_combo_changed_cb) + + hbox = gtk.HBox() + hbox.pack_start(gtk.Label(_("Key Type:"))) + hbox.pack_start(self.key_combo) + hbox.show_all() + self.vbox.pack_start(hbox) + + # Key entry field + self.add_key_entry() + + # WEP authentication mode + self.auth_store = gtk.ListStore(str, int) + self.auth_store.append(["Open System", IW_AUTH_ALG_OPEN_SYSTEM]) + self.auth_store.append(["Shared Key", IW_AUTH_ALG_SHARED_KEY]) + + self.auth_combo = gtk.ComboBox(self.auth_store) + cell = gtk.CellRendererText() + self.auth_combo.pack_start(cell, True) + self.auth_combo.add_attribute(cell, 'text', 0) + self.auth_combo.set_active(0) + + hbox = gtk.HBox() + hbox.pack_start(gtk.Label(_("Authentication Type:"))) + hbox.pack_start(self.auth_combo) + hbox.show_all() + + self.vbox.pack_start(hbox) + + def _key_combo_changed_cb(self, widget): + self._update_response_sensitivity() + + def _get_security(self): + key = self._entry.get_text() + + it = self.key_combo.get_active_iter() + (key_type, ) = self.key_store.get(it, 1) + + if key_type == WEP_PASSPHRASE: + key = hash_passphrase(key) + elif key_type == WEP_ASCII: + key = string_to_hex(key) + + it = self.auth_combo.get_active_iter() + (auth_alg, ) = self.auth_store.get(it, 1) + + we_cipher = None + if len(key) == 26: + we_cipher = IW_AUTH_CIPHER_WEP104 + elif len(key) == 10: + we_cipher = IW_AUTH_CIPHER_WEP40 + + return (we_cipher, key, auth_alg) + + def print_security(self): + (we_cipher, key, auth_alg) = self._get_security() + print "Cipher: %d" % we_cipher + print "Key: %s" % key + print "Auth: %d" % auth_alg + + def create_security(self): + (we_cipher, key, auth_alg) = self._get_security() + from jarabe.hardware.nminfo import Security + return Security.new_from_args(we_cipher, (key, auth_alg)) + + def _update_response_sensitivity(self, ignored=None): + key = self._entry.get_text() + it = self.key_combo.get_active_iter() + (key_type, ) = self.key_store.get(it, 1) + + valid = False + if key_type == WEP_PASSPHRASE: + # As the md5 passphrase can be of any length and has no indicator, + # we cannot check for the validity of the input. + if len(key) > 0: + valid = True + elif key_type == WEP_ASCII: + if len(key) == 5 or len(key) == 13: + valid = string_is_ascii(key) + elif key_type == WEP_HEX: + if len(key) == 10 or len(key) == 26: + valid = string_is_hex(key) + + self.set_response_sensitive(gtk.RESPONSE_OK, valid) + +class WPAKeyDialog(KeyDialog): + def __init__(self, net, async_cb, async_err_cb): + KeyDialog.__init__(self, net, async_cb, async_err_cb) + self.add_key_entry() + + self.store = gtk.ListStore(str, int) + self.store.append(["Automatic", NM_AUTH_TYPE_WPA_PSK_AUTO]) + if net.get_caps() & NM_802_11_CAP_CIPHER_CCMP: + self.store.append(["AES-CCMP", IW_AUTH_CIPHER_CCMP]) + if net.get_caps() & NM_802_11_CAP_CIPHER_TKIP: + self.store.append(["TKIP", IW_AUTH_CIPHER_TKIP]) + + self.combo = gtk.ComboBox(self.store) + cell = gtk.CellRendererText() + self.combo.pack_start(cell, True) + self.combo.add_attribute(cell, 'text', 0) + self.combo.set_active(0) + + self.hbox = gtk.HBox() + self.hbox.pack_start(gtk.Label(_("Encryption Type:"))) + self.hbox.pack_start(self.combo) + self.hbox.show_all() + + self.vbox.pack_start(self.hbox) + + def _get_security(self): + ssid = self.get_network().get_ssid() + key = self._entry.get_text() + is_hex = string_is_hex(key) + + real_key = None + if len(key) == 64 and is_hex: + # Hex key + real_key = key + elif len(key) >= 8 and len(key) <= 63: + # passphrase + from subprocess import Popen, PIPE + p = Popen(['/usr/sbin/wpa_passphrase', ssid, key], stdout=PIPE) + for line in p.stdout: + if line.strip().startswith("psk="): + real_key = line.strip()[4:] + if p.wait() != 0: + raise RuntimeError("Error hashing passphrase") + if real_key and len(real_key) != 64: + real_key = None + + if not real_key: + raise RuntimeError("Invalid key") + + it = self.combo.get_active_iter() + (we_cipher, ) = self.store.get(it, 1) + + wpa_ver = IW_AUTH_WPA_VERSION_WPA + caps = self.get_network().get_caps() + if caps & NM_802_11_CAP_PROTO_WPA2: + wpa_ver = IW_AUTH_WPA_VERSION_WPA2 + + return (we_cipher, real_key, wpa_ver) + + def print_security(self): + (we_cipher, key, wpa_ver) = self._get_security() + print "Cipher: %d" % we_cipher + print "Key: %s" % key + print "WPA Ver: %d" % wpa_ver + + def create_security(self): + (we_cipher, key, wpa_ver) = self._get_security() + from jarabe.hardware.nminfo import Security + return Security.new_from_args(we_cipher, + (key, wpa_ver, IW_AUTH_KEY_MGMT_PSK)) + + def _update_response_sensitivity(self, ignored=None): + key = self._entry.get_text() + is_hex = string_is_hex(key) + + valid = False + if len(key) == 64 and is_hex: + # hex key + valid = True + elif len(key) >= 8 and len(key) <= 63: + # passphrase + valid = True + self.set_response_sensitive(gtk.RESPONSE_OK, valid) + return False + +def new_key_dialog(net, async_cb, async_err_cb): + caps = net.get_caps() + if (caps & NM_802_11_CAP_CIPHER_TKIP or caps & NM_802_11_CAP_CIPHER_CCMP) \ + and (caps & NM_802_11_CAP_PROTO_WPA or \ + caps & NM_802_11_CAP_PROTO_WPA2): + return WPAKeyDialog(net, async_cb, async_err_cb) + elif (caps & NM_802_11_CAP_CIPHER_WEP40 or \ + caps & NM_802_11_CAP_CIPHER_WEP104) and \ + (caps & NM_802_11_CAP_PROTO_WEP): + return WEPKeyDialog(net, async_cb, async_err_cb) + else: + raise RuntimeError("Unhandled network capabilities %x" % caps) + + + +class FakeNet(object): + def get_ssid(self): + return "olpcwpa" + + def get_caps(self): + return NM_802_11_CAP_CIPHER_CCMP | NM_802_11_CAP_CIPHER_TKIP | \ + NM_802_11_CAP_PROTO_WPA + +def response_cb(widget, response_id): + if response_id == gtk.RESPONSE_OK: + print dialog.print_security() + else: + print "canceled" + widget.hide() + widget.destroy() + + +if __name__ == "__main__": + fake_net = FakeNet() + dialog = new_key_dialog(fake_net, None, None) + dialog.connect("response", response_cb) + dialog.run() + diff --git a/src/jarabe/hardware/nmclient.py b/src/jarabe/hardware/nmclient.py new file mode 100644 index 0000000..b1921aa --- /dev/null +++ b/src/jarabe/hardware/nmclient.py @@ -0,0 +1,759 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +import dbus.glib +import dbus.decorators +import gobject + +from jarabe.hardware import nminfo +from sugar.graphics import xocolor + +IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001 +IW_AUTH_ALG_SHARED_KEY = 0x00000002 + +NM_DEVICE_STAGE_STRINGS = ("Unknown", + "Prepare", + "Config", + "Need Users Key", + "IP Config", + "IP Config Get", + "IP Config Commit", + "Activated", + "Failed", + "Canceled" + ) + +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_IFACE_DEVICES = 'org.freedesktop.NetworkManager.Devices' +NM_PATH = '/org/freedesktop/NetworkManager' + +DEVICE_TYPE_UNKNOWN = 0 +DEVICE_TYPE_802_3_ETHERNET = 1 +DEVICE_TYPE_802_11_WIRELESS = 2 +DEVICE_TYPE_802_11_MESH_OLPC = 3 + +NM_DEVICE_CAP_NONE = 0x00000000 +NM_DEVICE_CAP_NM_SUPPORTED = 0x00000001 +NM_DEVICE_CAP_CARRIER_DETECT = 0x00000002 +NM_DEVICE_CAP_WIRELESS_SCAN = 0x00000004 + +sys_bus = dbus.SystemBus() + +NM_802_11_CAP_NONE = 0x00000000 +NM_802_11_CAP_PROTO_NONE = 0x00000001 +NM_802_11_CAP_PROTO_WEP = 0x00000002 +NM_802_11_CAP_PROTO_WPA = 0x00000004 +NM_802_11_CAP_PROTO_WPA2 = 0x00000008 +NM_802_11_CAP_KEY_MGMT_PSK = 0x00000040 +NM_802_11_CAP_KEY_MGMT_802_1X = 0x00000080 +NM_802_11_CAP_CIPHER_WEP40 = 0x00001000 +NM_802_11_CAP_CIPHER_WEP104 = 0x00002000 +NM_802_11_CAP_CIPHER_TKIP = 0x00004000 +NM_802_11_CAP_CIPHER_CCMP = 0x00008000 + +NETWORK_STATE_CONNECTING = 0 +NETWORK_STATE_CONNECTED = 1 +NETWORK_STATE_NOTCONNECTED = 2 + +DEVICE_STATE_ACTIVATING = 0 +DEVICE_STATE_ACTIVATED = 1 +DEVICE_STATE_INACTIVE = 2 + +IW_MODE_ADHOC = 1 +IW_MODE_INFRA = 2 + +class Network(gobject.GObject): + __gsignals__ = { + 'initialized' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_BOOLEAN])), + 'strength-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'state-changed' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self, client, op): + gobject.GObject.__init__(self) + self._client = client + self._op = op + self._ssid = None + self._mode = None + self._strength = 0 + self._caps = 0 + self._valid = False + self._favorite = False + self._state = NETWORK_STATE_NOTCONNECTED + + obj = sys_bus.get_object(NM_SERVICE, self._op) + net = dbus.Interface(obj, NM_IFACE_DEVICES) + net.getProperties(reply_handler=self._update_reply_cb, + error_handler=self._update_error_cb) + + def _update_reply_cb(self, *props): + self._ssid = props[1] + self._strength = props[3] + self._mode = props[6] + self._caps = props[7] + if self._caps & NM_802_11_CAP_PROTO_WPA or \ + self._caps & NM_802_11_CAP_PROTO_WPA2: + if not (self._caps & NM_802_11_CAP_KEY_MGMT_PSK): + # 802.1x is not supported at this time + logging.debug("Net(%s): ssid '%s' dropping because 802.1x" \ + "is unsupported" % (self._op, self._ssid)) + self._valid = False + self.emit('initialized', self._valid) + return + if self._mode != IW_MODE_INFRA: + # Don't show Ad-Hoc networks; they usually don't DHCP and therefore + # won't work well here. This also works around the bug where + # we show our own mesh SSID on the Mesh view when in mesh mode + logging.debug("Net(%s): ssid '%s' is adhoc; not showing" % + (self._op, self._ssid)) + self._valid = False + self.emit('initialized', self._valid) + return + + fav_nets = [] + if self._client.nminfo: + fav_nets = self._client.nminfo.get_networks( + nminfo.NETWORK_TYPE_ALLOWED) + if self._ssid in fav_nets: + self._favorite = True + + self._valid = True + logging.debug("Net(%s): caps 0x%X" % (self._ssid, self._caps)) + self.emit('initialized', self._valid) + + def _update_error_cb(self, err): + logging.debug("Net(%s): failed to update. (%s)" % (self._op, err)) + self._valid = False + self.emit('initialized', self._valid) + + def get_colors(self): + import sha + sh = sha.new() + data = self._ssid + hex(self._caps) + hex(self._mode) + sh.update(data) + h = hash(sh.digest()) + idx = h % len(xocolor.colors) + # stroke, fill + return (xocolor.colors[idx][0], xocolor.colors[idx][1]) + + def get_ssid(self): + return self._ssid + + def get_caps(self): + return self._caps + + def get_mode(self): + return self._mode + + def get_state(self): + return self._state + + def set_state(self, state): + if state == self._state: + return + self._state = state + if self._valid: + self.emit('state-changed') + + def get_op(self): + return self._op + + def get_strength(self): + return self._strength + + def set_strength(self, strength): + if strength == self._strength: + return + self._strength = strength + if self._valid: + self.emit('strength-changed') + + def is_valid(self): + return self._valid + + def is_favorite(self): + return self._favorite + +class Device(gobject.GObject): + __gsignals__ = { + 'initialized': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'init-failed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'ssid-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'strength-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'state-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'activation-stage-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'network-appeared': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'network-disappeared': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self, client, op): + gobject.GObject.__init__(self) + self._client = client + self._op = op + self._iface = None + self._type = DEVICE_TYPE_UNKNOWN + self._udi = None + self._active = False + self._act_stage = 0 + self._strength = 0 + self._freq = 0.0 + self._link = False + self._valid = False + self._networks = {} + self._caps = 0 + self._state = DEVICE_STATE_INACTIVE + self._active_network = None + self._active_net_sigid = 0 + + obj = sys_bus.get_object(NM_SERVICE, self._op) + self.dev = dbus.Interface(obj, NM_IFACE_DEVICES) + self.dev.getProperties(reply_handler=self._update_reply_cb, + error_handler=self._update_error_cb) + + def _is_activating(self): + if self._active and self._act_stage >= 1 and self._act_stage <= 6: + return True + return False + + def _is_activated(self): + if self._active and self._act_stage == 7: + return True + return False + + def _update_reply_cb(self, *props): + self._iface = props[1] + self._type = props[2] + self._udi = props[3] + self._active = props[4] + self._act_stage = props[5] + self._link = props[15] + self._caps = props[17] + + if self._type == DEVICE_TYPE_802_11_WIRELESS: + old_strength = self._strength + self._strength = props[14] + if self._strength != old_strength: + if self._valid: + self.emit('strength-changed') + self._update_networks(props[20], props[19]) + elif self._type == DEVICE_TYPE_802_11_MESH_OLPC: + old_strength = self._strength + self._strength = props[14] + if self._strength != old_strength: + if self._valid: + self.emit('strength-changed') + + self._valid = True + + if self._is_activating(): + self.set_state(DEVICE_STATE_ACTIVATING) + elif self._is_activated(): + self.set_state(DEVICE_STATE_ACTIVATED) + else: + self.set_state(DEVICE_STATE_INACTIVE) + + self.emit('initialized') + + def _update_networks(self, net_ops, active_op): + for op in net_ops: + net = Network(self._client, op) + self._networks[op] = net + net.connect('initialized', lambda *args: + self._net_initialized_cb(active_op, *args)) + + def _update_error_cb(self, err): + logging.debug("Device(%s): failed to update. (%s)" % (self._op, err)) + self._valid = False + self.emit('init-failed') + + def _net_initialized_cb(self, active_op, net, valid): + net_op = net.get_op() + if not self._networks.has_key(net_op): + return + + if not valid: + # init failure + del self._networks[net_op] + return + + # init success + if self._valid: + self.emit('network-appeared', net) + if active_op and net_op == active_op: + self.set_active_network(net) + + def get_op(self): + return self._op + + def get_networks(self): + ret = [] + for net in self._networks.values(): + if net.is_valid(): + ret.append(net) + return ret + + def get_network(self, op): + if self._networks.has_key(op) and self._networks[op].is_valid(): + return self._networks[op] + return None + + def get_network_ops(self): + ret = [] + for net in self._networks.values(): + if net.is_valid(): + ret.append(net.get_op()) + return ret + + def get_mesh_step(self): + if self._type != DEVICE_TYPE_802_11_MESH_OLPC: + raise RuntimeError("Only valid for mesh devices") + try: + step = self.dev.getMeshStep(timeout=3) + except dbus.DBusException: + step = 0 + return step + + def get_frequency(self): + try: + freq = self.dev.getFrequency(timeout=3) + except dbus.DBusException: + freq = 0.0 + # Hz -> GHz + self._freq = freq / 1000000000.0 + return self._freq + + def get_strength(self): + return self._strength + + def set_strength(self, strength): + if strength == self._strength: + return False + + if strength >= 0 and strength <= 100: + self._strength = strength + else: + self._strength = 0 + + if self._valid: + self.emit('strength-changed') + + def network_appeared(self, network): + # NM may emit NetworkAppeared messages before the initialization-time + # getProperties call completes. This means that we are in danger of + # instantiating the "appeared" network here, and then instantiating + # the same network later on when getProperties completes + # (_update_reply_cb calls _update_networks). + # We avoid this race by confirming that getProperties has completed + # before listening to any NetworkAppeared messages. We assume that + # any networks that get reported as appeared in this race window + # will be included in the getProperties response. + if not self._valid: + return + + if self._networks.has_key(network): + return + net = Network(self._client, network) + self._networks[network] = net + net.connect('initialized', lambda *args: + self._net_initialized_cb(None, *args)) + + def network_disappeared(self, network): + if not self._networks.has_key(network): + return + + if self._valid: + self.emit('network-disappeared', self._networks[network]) + + del self._networks[network] + + def set_active_network(self, network): + if self._active_network == network: + return + + # Make sure the old one doesn't get a stuck state + if self._active_network: + self._active_network.set_state(NETWORK_STATE_NOTCONNECTED) + self._active_network.disconnect(self._active_net_sigid) + + self._active_network = network + + if self._active_network: + self._active_net_sigid = self._active_network.connect( + "initialized", self._active_net_initialized) + + # don't emit ssid-changed for networks that are not yet valid + if self._valid: + if self._active_network and self._active_network.is_valid(): + self.emit('ssid-changed') + elif not self._active_network: + self.emit('ssid-changed') + + def _active_net_initialized(self, net, user_data=None): + if self._active_network and self._active_network.is_valid(): + self.emit('ssid-changed') + + def _get_active_net_cb(self, state, net_op): + if not self._networks.has_key(net_op): + self.set_active_network(None) + return + + self.set_active_network(self._networks[net_op]) + + _device_to_network_state = { + DEVICE_STATE_ACTIVATING : NETWORK_STATE_CONNECTING, + DEVICE_STATE_ACTIVATED : NETWORK_STATE_CONNECTED, + DEVICE_STATE_INACTIVE : NETWORK_STATE_NOTCONNECTED + } + + network_state = _device_to_network_state[state] + self._active_network.set_state(network_state) + + def _get_active_net_error_cb(self, err): + logging.debug("Couldn't get active network: %s" % err) + self.set_active_network(None) + + def get_state(self): + return self._state + + def set_state(self, state): + if state == self._state: + return + + if state == DEVICE_STATE_INACTIVE: + self._act_stage = 0 + + self._state = state + if self._valid: + self.emit('state-changed') + + if self._type == DEVICE_TYPE_802_11_WIRELESS: + if state == DEVICE_STATE_INACTIVE: + self.set_active_network(None) + else: + self.dev.getActiveNetwork( + reply_handler=lambda *args: + self._get_active_net_cb(state, *args), + error_handler=self._get_active_net_error_cb) + + def set_activation_stage(self, stage): + if stage == self._act_stage: + return + self._act_stage = stage + if self._valid: + self.emit('activation-stage-changed') + + def get_activation_stage(self): + return self._act_stage + + def get_ssid(self): + if self._active_network and self._active_network.is_valid(): + return self._active_network.get_ssid() + elif not self._active_network: + return None + + def get_active_network(self): + return self._active_network + + def get_type(self): + return self._type + + def is_valid(self): + return self._valid + + def set_carrier(self, on): + self._link = on + + def get_capabilities(self): + return self._caps + +class NMClient(gobject.GObject): + __gsignals__ = { + 'device-added' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'device-activated' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'device-activating': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'device-removed' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self.nminfo = None + self._nm_present = False + self._nm_proxy = None + self._nm_obj = None + self._sig_handlers = None + self._update_timer = 0 + self._devices = {} + + self.nminfo = nminfo.NMInfo(self) + + self._setup_dbus() + if self._nm_present: + self._get_initial_devices() + + def get_devices(self): + return self._devices.values() + + def _get_initial_devices_reply_cb(self, ops): + for op in ops: + self._add_device(op) + + def _dev_initialized_cb(self, dev): + self.emit('device-added', dev) + + def _dev_init_failed_cb(self, dev): + # Device failed to initialize, likely due to dbus errors or something + op = dev.get_op() + self._remove_device(op) + + def _get_initial_devices_error_cb(self, err): + logging.debug("Error updating devices (%s)" % err) + + def _get_initial_devices(self): + self._nm_obj.getDevices( + reply_handler=self._get_initial_devices_reply_cb, + error_handler=self._get_initial_devices_error_cb) + + def _add_device(self, dev_op): + if self._devices.has_key(dev_op): + return + dev = Device(self, dev_op) + self._devices[dev_op] = dev + dev.connect('init-failed', self._dev_init_failed_cb) + dev.connect('initialized', self._dev_initialized_cb) + dev.connect('state-changed', self._dev_state_changed_cb) + + def _remove_device(self, dev_op): + if not self._devices.has_key(dev_op): + return + dev = self._devices[dev_op] + if dev.is_valid(): + self.emit('device-removed', dev) + del self._devices[dev_op] + + def _dev_state_changed_cb(self, dev): + op = dev.get_op() + if not self._devices.has_key(op) or not dev.is_valid(): + return + if dev.get_state() == DEVICE_STATE_ACTIVATING: + self.emit('device-activating', dev) + elif dev.get_state() == DEVICE_STATE_ACTIVATED: + self.emit('device-activated', dev) + + def get_device(self, dev_op): + if not self._devices.has_key(dev_op): + return None + return self._devices[dev_op] + + def _setup_dbus(self): + self._sig_handlers = { + 'StateChange': self.state_changed_sig_handler, + 'DeviceAdded': self.device_added_sig_handler, + 'DeviceRemoved': self.device_removed_sig_handler, + 'DeviceActivationStage': self.device_activation_stage_sig_handler, + 'DeviceActivating': self.device_activating_sig_handler, + 'DeviceNowActive': self.device_now_active_sig_handler, + 'DeviceNoLongerActive': self.device_no_longer_active_sig_handler, + 'DeviceActivationFailed': \ + self.device_activation_failed_sig_handler, + 'DeviceCarrierOn': self.device_carrier_on_sig_handler, + 'DeviceCarrierOff': self.device_carrier_off_sig_handler, + 'DeviceStrengthChanged': \ + self.wireless_device_strength_changed_sig_handler, + 'WirelessNetworkAppeared': \ + self.wireless_network_appeared_sig_handler, + 'WirelessNetworkDisappeared': \ + self.wireless_network_disappeared_sig_handler, + 'WirelessNetworkStrengthChanged': \ + self.wireless_network_strength_changed_sig_handler + } + + try: + self._nm_proxy = sys_bus.get_object(NM_SERVICE, NM_PATH, + follow_name_owner_changes=True) + self._nm_obj = dbus.Interface(self._nm_proxy, NM_IFACE) + except dbus.DBusException, e: + logging.debug("Could not connect to NetworkManager: %s" % e) + self._nm_present = False + return + + sys_bus.add_signal_receiver(self.name_owner_changed_sig_handler, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") + + for (signal, handler) in self._sig_handlers.items(): + sys_bus.add_signal_receiver(handler, signal_name=signal, + dbus_interface=NM_IFACE) + + # Find out whether or not NMI is running + try: + bus_object = sys_bus.get_object('org.freedesktop.DBus', + '/org/freedesktop/DBus') + name_ = bus_object.GetNameOwner( \ + "org.freedesktop.NetworkManagerInfo", + dbus_interface='org.freedesktop.DBus') + self._nm_present = True + except dbus.DBusException: + self._nm_present = False + + def set_active_device(self, device, network=None, + mesh_freq=None, mesh_start=None): + ssid = "" + if network: + ssid = network.get_ssid() + if device.get_type() == DEVICE_TYPE_802_11_MESH_OLPC: + if mesh_freq or mesh_start: + if mesh_freq and not mesh_start: + self._nm_obj.setActiveDevice(device.get_op(), + dbus.Double(mesh_freq)) + elif mesh_start and not mesh_freq: + self._nm_obj.setActiveDevice(device.get_op(), + dbus.Double(0.0), + dbus.UInt32(mesh_start)) + else: + self._nm_obj.setActiveDevice(device.get_op(), + dbus.Double(mesh_freq), + dbus.UInt32(mesh_start)) + else: + self._nm_obj.setActiveDevice(device.get_op()) + else: + self._nm_obj.setActiveDevice(device.get_op(), ssid) + + def state_changed_sig_handler(self, new_state): + logging.debug('NM State Changed to %d' % new_state) + + def device_activation_stage_sig_handler(self, device, stage): + logging.debug('Device Activation Stage "%s" for device %s' + % (NM_DEVICE_STAGE_STRINGS[stage], device)) + if not self._devices.has_key(device): + logging.debug('DeviceActivationStage, device %s does not exist' + % (device)) + return + self._devices[device].set_activation_stage(stage) + + def device_activating_sig_handler(self, device): + logging.debug('DeviceActivating for %s' % (device)) + if not self._devices.has_key(device): + logging.debug('DeviceActivating, device %s does not exist' + % (device)) + return + self._devices[device].set_state(DEVICE_STATE_ACTIVATING) + + def device_now_active_sig_handler(self, device, ssid=None): + logging.debug('DeviceNowActive for %s' % (device)) + if not self._devices.has_key(device): + logging.debug('DeviceNowActive, device %s does not exist' + % (device)) + return + self._devices[device].set_state(DEVICE_STATE_ACTIVATED) + + def device_no_longer_active_sig_handler(self, device): + logging.debug('DeviceNoLongerActive for %s' % (device)) + if not self._devices.has_key(device): + logging.debug('DeviceNoLongerActive, device %s does not exist' + % (device)) + return + self._devices[device].set_state(DEVICE_STATE_INACTIVE) + + def device_activation_failed_sig_handler(self, device, ssid=None): + logging.debug('DeviceActivationFailed for %s' % (device)) + if not self._devices.has_key(device): + logging.debug('DeviceActivationFailed, device %s does not exist' + % (device)) + return + self._devices[device].set_state(DEVICE_STATE_INACTIVE) + + def name_owner_changed_sig_handler(self, name, old, new): + if name != NM_SERVICE: + return + if (old and len(old)) and (not new and not len(new)): + # NM went away + self._nm_present = False + devs = self._devices.keys() + for op in devs: + self._remove_device(op) + self._devices = {} + elif (not old and not len(old)) and (new and len(new)): + # NM started up + self._nm_present = True + self._get_initial_devices() + + def device_added_sig_handler(self, device): + logging.debug('DeviceAdded for %s' % (device)) + self._add_device(device) + + def device_removed_sig_handler(self, device): + logging.debug('DeviceRemoved for %s' % (device)) + self._remove_device(device) + + def wireless_network_appeared_sig_handler(self, device, network): + if not self._devices.has_key(device): + return + self._devices[device].network_appeared(network) + + def wireless_network_disappeared_sig_handler(self, device, network): + if not self._devices.has_key(device): + return + self._devices[device].network_disappeared(network) + + def wireless_device_strength_changed_sig_handler(self, device, strength): + if not self._devices.has_key(device): + return + self._devices[device].set_strength(strength) + + def wireless_network_strength_changed_sig_handler(self, device, + network, strength): + if not self._devices.has_key(device): + return + net = self._devices[device].get_network(network) + if net: + net.set_strength(strength) + + def device_carrier_on_sig_handler(self, device): + if not self._devices.has_key(device): + return + self._devices[device].set_carrier(True) + + def device_carrier_off_sig_handler(self, device): + if not self._devices.has_key(device): + return + self._devices[device].set_carrier(False) diff --git a/src/jarabe/hardware/nminfo.py b/src/jarabe/hardware/nminfo.py new file mode 100644 index 0000000..64c5ab3 --- /dev/null +++ b/src/jarabe/hardware/nminfo.py @@ -0,0 +1,557 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import time +import os +import binascii +import ConfigParser +import logging + +import gtk +import dbus +import dbus.service + +from sugar import env + +from jarabe.hardware import keydialog + +IW_AUTH_KEY_MGMT_802_1X = 0x1 +IW_AUTH_KEY_MGMT_PSK = 0x2 + +IW_AUTH_WPA_VERSION_DISABLED = 0x00000001 +IW_AUTH_WPA_VERSION_WPA = 0x00000002 +IW_AUTH_WPA_VERSION_WPA2 = 0x00000004 + +NM_AUTH_TYPE_WPA_PSK_AUTO = 0x00000000 +IW_AUTH_CIPHER_NONE = 0x00000001 +IW_AUTH_CIPHER_WEP40 = 0x00000002 +IW_AUTH_CIPHER_TKIP = 0x00000004 +IW_AUTH_CIPHER_CCMP = 0x00000008 +IW_AUTH_CIPHER_WEP104 = 0x00000010 + +IW_AUTH_ALG_OPEN_SYSTEM = 0x00000001 +IW_AUTH_ALG_SHARED_KEY = 0x00000002 + +NM_INFO_IFACE = 'org.freedesktop.NetworkManagerInfo' +NM_INFO_PATH = '/org/freedesktop/NetworkManagerInfo' + + +class NoNetworks(dbus.DBusException): + def __init__(self): + dbus.DBusException.__init__(self) + self._dbus_error_name = NM_INFO_IFACE + '.NoNetworks' + +class CanceledKeyRequestError(dbus.DBusException): + def __init__(self): + dbus.DBusException.__init__(self) + self._dbus_error_name = NM_INFO_IFACE + '.CanceledError' + + +class NetworkInvalidError(Exception): + pass + + +class NMConfig(ConfigParser.ConfigParser): + def get_bool(self, section, name): + opt = self.get(section, name) + if type(opt) == str: + if opt.lower() == 'yes' or opt.lower() == 'true': + return True + elif opt.lower() == 'no' or opt.lower() == 'false': + return False + raise ValueError("Invalid format for %s/%s. Should be one of" \ + " [yes, no, true, false]." % (section, name)) + + def get_list(self, section, name): + opt = self.get(section, name) + if type(opt) != str or not len(opt): + return [] + try: + return opt.split() + except Exception: + raise ValueError("Invalid format for %s/%s. Should be a" \ + " space-separate list." % (section, name)) + + def get_int(self, section, name): + opt = self.get(section, name) + try: + return int(opt) + except ValueError: + raise ValueError("Invalid format for %s/%s. Should be a" \ + " valid integer." % (section, name)) + + def get_float(self, section, name): + opt = self.get(section, name) + try: + return float(opt) + except ValueError: + raise ValueError("Invalid format for %s/%s. Should be a" \ + " valid float." % (section, name)) + + +NETWORK_TYPE_UNKNOWN = 0 +NETWORK_TYPE_ALLOWED = 1 +NETWORK_TYPE_INVALID = 2 + + +class Security(object): + def __init__(self, we_cipher): + self._we_cipher = we_cipher + self._key = None + self._auth_alg = None + + def read_from_config(self, cfg, name): + pass + + def read_from_args(self, args): + pass + + def new_from_config(cfg, name): + security = None + we_cipher = cfg.get_int(name, "we_cipher") + if we_cipher == IW_AUTH_CIPHER_NONE: + security = Security(we_cipher) + elif we_cipher == IW_AUTH_CIPHER_WEP40 or \ + we_cipher == IW_AUTH_CIPHER_WEP104: + security = WEPSecurity(we_cipher) + elif we_cipher == NM_AUTH_TYPE_WPA_PSK_AUTO or \ + we_cipher == IW_AUTH_CIPHER_CCMP or \ + we_cipher == IW_AUTH_CIPHER_TKIP: + security = WPASecurity(we_cipher) + else: + raise ValueError("Unsupported security combo") + security.read_from_config(cfg, name) + return security + new_from_config = staticmethod(new_from_config) + + def new_from_args(we_cipher, args): + security = None + try: + if we_cipher == IW_AUTH_CIPHER_NONE: + security = Security(we_cipher) + elif we_cipher == IW_AUTH_CIPHER_WEP40 or \ + we_cipher == IW_AUTH_CIPHER_WEP104: + security = WEPSecurity(we_cipher) + elif we_cipher == NM_AUTH_TYPE_WPA_PSK_AUTO or \ + we_cipher == IW_AUTH_CIPHER_CCMP or \ + we_cipher == IW_AUTH_CIPHER_TKIP: + security = WPASecurity(we_cipher) + else: + raise ValueError("Unsupported security combo") + security.read_from_args(args) + except ValueError, e: + logging.debug("Error reading security information: %s" % e) + del security + return None + return security + new_from_args = staticmethod(new_from_args) + + def get_properties(self): + return [dbus.Int32(self._we_cipher)] + + def write_to_config(self, section, config): + config.set(section, "we_cipher", self._we_cipher) + + +class WEPSecurity(Security): + def read_from_args(self, args): + if len(args) != 2: + raise ValueError("not enough arguments") + key = args[0] + auth_alg = args[1] + if isinstance(key, unicode): + key = key.encode() + if not isinstance(key, str): + raise ValueError("wrong argument type for key") + if not isinstance(auth_alg, int): + raise ValueError("wrong argument type for auth_alg") + self._key = key + self._auth_alg = auth_alg + + def read_from_config(self, cfg, name): + # Key should be a hex encoded string + self._key = cfg.get(name, "key") + if self._we_cipher == IW_AUTH_CIPHER_WEP40 and len(self._key) != 10: + raise ValueError("Key length not right for 40-bit WEP") + if self._we_cipher == IW_AUTH_CIPHER_WEP104 and len(self._key) != 26: + raise ValueError("Key length not right for 104-bit WEP") + + try: + binascii.a2b_hex(self._key) + except TypeError: + raise ValueError("Key was not a hexadecimal string.") + + self._auth_alg = cfg.get_int(name, "auth_alg") + if self._auth_alg != IW_AUTH_ALG_OPEN_SYSTEM and \ + self._auth_alg != IW_AUTH_ALG_SHARED_KEY: + raise ValueError("Invalid authentication algorithm %d" + % self._auth_alg) + + def get_properties(self): + args = Security.get_properties(self) + args.append(dbus.String(self._key)) + args.append(dbus.Int32(self._auth_alg)) + return args + + def write_to_config(self, section, config): + Security.write_to_config(self, section, config) + config.set(section, "key", self._key) + config.set(section, "auth_alg", self._auth_alg) + +class WPASecurity(Security): + def __init__(self, we_cipher): + Security.__init__(self, we_cipher) + self._wpa_ver = None + self._key_mgmt = None + + def read_from_args(self, args): + if len(args) != 3: + raise ValueError("not enough arguments") + key = args[0] + if isinstance(key, unicode): + key = key.encode() + if not isinstance(key, str): + raise ValueError("wrong argument type for key") + + wpa_ver = args[1] + if not isinstance(wpa_ver, int): + raise ValueError("wrong argument type for WPA version") + + key_mgmt = args[2] + if not isinstance(key_mgmt, int): + raise ValueError("wrong argument type for WPA key management") + if not key_mgmt & IW_AUTH_KEY_MGMT_PSK: + raise ValueError("Key management types other than" \ + " PSK are not supported") + + self._key = key + self._wpa_ver = wpa_ver + self._key_mgmt = key_mgmt + + def read_from_config(self, cfg, name): + # Key should be a hex encoded string + self._key = cfg.get(name, "key") + if len(self._key) != 64: + raise ValueError("Key length not right for WPA-PSK") + + try: + binascii.a2b_hex(self._key) + except TypeError: + raise ValueError("Key was not a hexadecimal string.") + + self._wpa_ver = cfg.get_int(name, "wpa_ver") + if self._wpa_ver != IW_AUTH_WPA_VERSION_WPA and \ + self._wpa_ver != IW_AUTH_WPA_VERSION_WPA2: + raise ValueError("Invalid WPA version %d" % self._wpa_ver) + + self._key_mgmt = cfg.get_int(name, "key_mgmt") + if not self._key_mgmt & IW_AUTH_KEY_MGMT_PSK: + raise ValueError("Invalid WPA key management option %d" + % self._key_mgmt) + + def get_properties(self): + args = Security.get_properties(self) + args.append(dbus.String(self._key)) + args.append(dbus.Int32(self._wpa_ver)) + args.append(dbus.Int32(self._key_mgmt)) + return args + + def write_to_config(self, section, config): + Security.write_to_config(self, section, config) + config.set(section, "key", self._key) + config.set(section, "wpa_ver", self._wpa_ver) + config.set(section, "key_mgmt", self._key_mgmt) + + +class Network: + def __init__(self, ssid): + self.ssid = ssid + self.timestamp = int(time.time()) + self.bssids = [] + self.we_cipher = 0 + self._security = None + + def get_properties(self): + bssid_list = dbus.Array([], signature="s") + for item in self.bssids: + bssid_list.append(dbus.String(item)) + args = [dbus.String(self.ssid), dbus.Int32(self.timestamp), + dbus.Boolean(True), bssid_list] + args += self._security.get_properties() + return tuple(args) + + def get_security(self): + return self._security.get_properties() + + def set_security(self, security): + self._security = security + + def read_from_args(self, auto, bssid, we_cipher, args): + if auto == False: + self.timestamp = int(time.time()) + if not bssid in self.bssids: + self.bssids.append(bssid) + + self._security = Security.new_from_args(we_cipher, args) + if not self._security: + raise NetworkInvalidError("Invalid security information") + + def read_from_config(self, config): + try: + self.timestamp = config.get_int(self.ssid, "timestamp") + except (ConfigParser.NoOptionError, ValueError), e: + raise NetworkInvalidError(e) + + try: + self._security = Security.new_from_config(config, self.ssid) + except Exception, e: + raise NetworkInvalidError(e) + + # The following don't need to be present + try: + self.bssids = config.get_list(self.ssid, "bssids") + except (ConfigParser.NoOptionError, ValueError), e: + logging.debug("Error reading bssids: %s" % e) + + def write_to_config(self, config): + try: + config.add_section(self.ssid) + config.set(self.ssid, "timestamp", self.timestamp) + if len(self.bssids) > 0: + opt = " " + opt = opt.join(self.bssids) + config.set(self.ssid, "bssids", opt) + self._security.write_to_config(self.ssid, config) + except Exception, e: + logging.debug("Error writing '%s': %s" % (self.ssid, e)) + + +class NotFoundError(dbus.DBusException): + pass +class UnsupportedError(dbus.DBusException): + pass + +class NMInfoDBusServiceHelper(dbus.service.Object): + def __init__(self, parent): + self._parent = parent + bus = dbus.SystemBus() + + # If NMI is already around, don't grab the NMI service + bus_object = bus.get_object('org.freedesktop.DBus', + '/org/freedesktop/DBus') + name = None + try: + name = bus_object.GetNameOwner( \ + "org.freedesktop.NetworkManagerInfo", + dbus_interface='org.freedesktop.DBus') + except dbus.DBusException: + logging.debug("Error getting owner of NMI") + if name: + logging.info("NMI service already owned by %s, won't claim it." + % name) + + bus_name = dbus.service.BusName(NM_INFO_IFACE, bus=bus) + dbus.service.Object.__init__(self, bus_name, NM_INFO_PATH) + + @dbus.service.method(NM_INFO_IFACE, in_signature='i', out_signature='as') + def getNetworks(self, net_type): + ssids = self._parent.get_networks(net_type) + if len(ssids) > 0: + return dbus.Array(ssids) + + raise NoNetworks() + + @dbus.service.method(NM_INFO_IFACE, in_signature='si', + async_callbacks=('async_cb', 'async_err_cb')) + def getNetworkProperties(self, ssid, net_type, async_cb, async_err_cb): + self._parent.get_network_properties(ssid, net_type, + async_cb, async_err_cb) + + @dbus.service.method(NM_INFO_IFACE) + def updateNetworkInfo(self, ssid, bauto, bssid, cipher, *args): + self._parent.update_network_info(ssid, bauto, bssid, cipher, args) + + @dbus.service.method(NM_INFO_IFACE, + async_callbacks=('async_cb', 'async_err_cb')) + def getKeyForNetwork(self, dev_path, net_path, ssid, attempt, + new_key, async_cb, async_err_cb): + self._parent.get_key_for_network(dev_path, net_path, ssid, + attempt, new_key, async_cb, async_err_cb) + + @dbus.service.method(NM_INFO_IFACE) + def cancelGetKeyForNetwork(self): + self._parent.cancel_get_key_for_network() + +class NMInfo(object): + def __init__(self, client): + profile_path = env.get_profile_path() + self._cfg_file = os.path.join(profile_path, "nm", "networks.cfg") + self._nmclient = client + self._allowed_networks = self._read_config() + self._dbus_helper = NMInfoDBusServiceHelper(self) + self._key_dialog = None + + def save_config(self): + self._write_config(self._allowed_networks) + + def _read_config(self): + if not os.path.exists(os.path.dirname(self._cfg_file)): + os.makedirs(os.path.dirname(self._cfg_file), 0755) + if not os.path.exists(self._cfg_file): + self._write_config({}) + return {} + + config = NMConfig() + config.read(self._cfg_file) + networks = {} + for name in config.sections(): + try: + net = Network(name) + net.read_from_config(config) + networks[name] = net + except Exception, e: + logging.error("Error when processing config for" \ + " the network %s: %r" % (name, e)) + + del config + return networks + + def _write_config(self, networks): + fp = open(self._cfg_file, 'w') + config = NMConfig() + for net in networks.values(): + net.write_to_config(config) + config.write(fp) + fp.close() + del config + + def get_networks(self, net_type): + if net_type != NETWORK_TYPE_ALLOWED: + raise ValueError("Bad network type") + nets = [] + for net in self._allowed_networks.values(): + nets.append(net.ssid) + logging.debug("Returning networks: %s" % nets) + return nets + + def get_network_properties(self, ssid, net_type, async_cb, async_err_cb): + if not isinstance(ssid, unicode): + async_err_cb(ValueError("Invalid arguments; ssid must be unicode.")) + if net_type != NETWORK_TYPE_ALLOWED: + async_err_cb(ValueError("Bad network type")) + if not self._allowed_networks.has_key(ssid): + async_err_cb(NotFoundError("Network '%s' not found." % ssid)) + network = self._allowed_networks[ssid] + props = network.get_properties() + + # DBus workaround: the normal method return handler wraps + # the returned arguments in a tuple and then converts that to a + # struct, but NetworkManager expects a plain list of arguments. + # It turns out that the async callback method return code _doesn't_ + # wrap the returned arguments in a tuple, so as a workaround use + # the async callback stuff here even though we're not doing it + # asynchronously. + async_cb(*props) + + def update_network_info(self, ssid, auto, bssid, we_cipher, args): + if not isinstance(ssid, unicode): + raise ValueError("Invalid arguments; ssid must be unicode.") + if self._allowed_networks.has_key(ssid): + del self._allowed_networks[ssid] + net = Network(ssid) + try: + net.read_from_args(auto, bssid, we_cipher, args) + logging.debug("Updated network information for '%s'." % ssid) + self._allowed_networks[ssid] = net + self.save_config() + except NetworkInvalidError, e: + logging.debug("Error updating network information: %s" % e) + del net + + def get_key_for_network(self, dev_op, net_op, ssid, attempt, + new_key, async_cb, async_err_cb): + from jarabe.hardware import nmclient + + if not isinstance(ssid, unicode): + raise ValueError("Invalid arguments; ssid must be unicode.") + if self._allowed_networks.has_key(ssid) and not new_key: + # We've got the info already + net = self._allowed_networks[ssid] + async_cb(tuple(net.get_security())) + return + + # Otherwise, ask the user for it + net = None + dev = self._nmclient.get_device(dev_op) + if not dev: + async_err_cb(NotFoundError("Device was unknown.")) + return + + if dev.get_type() == nmclient.DEVICE_TYPE_802_3_ETHERNET: + # We don't support wired 802.1x yet... + async_err_cb(UnsupportedError("Device type is unsupported by NMI.")) + return + + net = dev.get_network(net_op) + if not net: + async_err_cb(NotFoundError("Network was unknown.")) + return + + self._key_dialog = keydialog.new_key_dialog(net, async_cb, async_err_cb) + self._key_dialog.connect("response", self._key_dialog_response_cb) + self._key_dialog.connect("destroy", self._key_dialog_destroy_cb) + self._key_dialog.show_all() + + def _key_dialog_destroy_cb(self, widget, data=None): + if widget != self._key_dialog: + return + self._key_dialog_response_cb(widget, gtk.RESPONSE_CANCEL) + + def _key_dialog_response_cb(self, widget, response_id): + if widget != self._key_dialog: + return + + (async_cb, async_err_cb) = self._key_dialog.get_callbacks() + security = None + if response_id == gtk.RESPONSE_OK: + security = self._key_dialog.create_security() + self._key_dialog = None + widget.destroy() + + if response_id in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_NONE]: + # key dialog dialog was canceled; send the error back to NM + async_err_cb(CanceledKeyRequestError()) + elif response_id == gtk.RESPONSE_OK: + if not security: + raise RuntimeError("Invalid security arguments.") + props = security.get_properties() + a = tuple(props) + async_cb(*a) + else: + raise RuntimeError("Unhandled key dialog response %d" % response_id) + + def cancel_get_key_for_network(self): + # Close the wireless key dialog and just have it return + # with the 'canceled' argument set to true + if not self._key_dialog: + return + self._key_dialog_destroy_cb(self._key_dialog) + + # this method is invoked directly in-process (not by DBus). + def delete_all_networks(self): + self._allowed_networks = {} + self.save_config() + diff --git a/src/jarabe/hardware/schoolserver.py b/src/jarabe/hardware/schoolserver.py new file mode 100644 index 0000000..a9ed60b --- /dev/null +++ b/src/jarabe/hardware/schoolserver.py @@ -0,0 +1,54 @@ +import logging +from gettext import gettext as _ +from xmlrpclib import ServerProxy, Error +import socket +import os + +from sugar.profile import get_profile + +REGISTER_URL = 'http://schoolserver:8080/' + +class RegisterError(Exception): + pass + +def register_laptop(url=REGISTER_URL): + if not have_ofw_tree(): + logging.error('Registration: Cannot obtain data needed to register.') + raise RegisterError(_('Cannot obtain data needed for registration.')) + + sn = read_ofw('mfg-data/SN') + uuid = read_ofw('mfg-data/U#') + sn = sn or 'SHF00000000' + uuid = uuid or '00000000-0000-0000-0000-000000000000' + + profile = get_profile() + + server = ServerProxy(url) + try: + data = server.register(sn, profile.nick_name, uuid, profile.pubkey) + except (Error, socket.error), e: + logging.error('Registration: cannot connect to server: %s' % e) + raise RegisterError(_('Cannot connect to the server.')) + + if data['success'] != 'OK': + logging.error('Registration: server could not complete request: %s' % + data['error']) + raise RegisterError(_('The server could not complete the request.')) + + profile.jabber_server = data['jabberserver'] + profile.backup1 = data['backupurl'] + profile.save() + + return True + +def have_ofw_tree(): + return os.path.exists('/ofw') + +def read_ofw(path): + path = os.path.join('/ofw', path) + if not os.path.exists(path): + return None + fh = open(path, 'r') + data = fh.read().rstrip('\0\n') + fh.close() + return data diff --git a/src/jarabe/intro/Makefile.am b/src/jarabe/intro/Makefile.am new file mode 100644 index 0000000..b9ddcf4 --- /dev/null +++ b/src/jarabe/intro/Makefile.am @@ -0,0 +1,9 @@ +imagedir = $(pkgdatadir)/shell/intro +image_DATA = default-picture.png + +EXTRA_DIST = $(conf_DATA) $(image_DATA) +sugardir = $(pythondir)/jarabe/intro +sugar_PYTHON = \ + __init__.py \ + colorpicker.py \ + window.py diff --git a/src/jarabe/intro/__init__.py b/src/jarabe/intro/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/jarabe/intro/__init__.py diff --git a/src/jarabe/intro/colorpicker.py b/src/jarabe/intro/colorpicker.py new file mode 100644 index 0000000..a939857 --- /dev/null +++ b/src/jarabe/intro/colorpicker.py @@ -0,0 +1,43 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + +class ColorPicker(hippo.CanvasBox, hippo.CanvasItem): + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._xo_color = None + + self._xo = CanvasIcon(size=style.XLARGE_ICON_SIZE, + icon_name='computer-xo') + self._set_random_colors() + self._xo.connect('activated', self._xo_activated_cb) + self.append(self._xo) + + def _xo_activated_cb(self, item): + self._set_random_colors() + + def get_color(self): + return self._xo_color + + def _set_random_colors(self): + self._xo_color = XoColor() + self._xo.props.xo_color = self._xo_color diff --git a/src/jarabe/intro/default-picture.png b/src/jarabe/intro/default-picture.png Binary files differnew file mode 100644 index 0000000..e26b9b0 --- /dev/null +++ b/src/jarabe/intro/default-picture.png diff --git a/src/jarabe/intro/window.py b/src/jarabe/intro/window.py new file mode 100644 index 0000000..be1a963 --- /dev/null +++ b/src/jarabe/intro/window.py @@ -0,0 +1,278 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +from gettext import gettext as _ + +import gtk +import gobject +import hippo + +from sugar import env +from sugar.graphics import style +from sugar.graphics.icon import Icon +from sugar.graphics.entry import CanvasEntry +from sugar.graphics.xocolor import XoColor +from sugar.profile import get_profile + +from jarabe.intro import colorpicker + +_BACKGROUND_COLOR = style.COLOR_WHITE + +def create_profile(name, color=None, pixbuf=None): + if not pixbuf: + path = os.path.join(os.path.dirname(__file__), 'default-picture.png') + pixbuf = gtk.gdk.pixbuf_new_from_file(path) + + if not color: + color = XoColor() + + icon_path = os.path.join(env.get_profile_path(), "buddy-icon.jpg") + pixbuf.save(icon_path, "jpeg", {"quality":"85"}) + + profile = get_profile() + profile.nick_name = name + profile.color = color + profile.save() + + # Generate keypair + import commands + keypath = os.path.join(env.get_profile_path(), "owner.key") + if not os.path.isfile(keypath): + cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % keypath + (s, o) = commands.getstatusoutput(cmd) + if s != 0: + logging.error("Could not generate key pair: %d %s" % (s, o)) + else: + logging.error("Keypair exists, skip generation.") + +class _Page(hippo.CanvasBox): + __gproperties__ = { + 'valid' : (bool, None, None, False, + gobject.PARAM_READABLE) + } + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.valid = False + + def set_valid(self, valid): + self.valid = valid + self.notify('valid') + + def do_get_property(self, pspec): + if pspec.name == 'valid': + return self.valid + + def activate(self): + pass + +class _NamePage(_Page): + def __init__(self, intro): + _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER, + background_color=_BACKGROUND_COLOR.get_int(), + spacing=style.DEFAULT_SPACING, + orientation=hippo.ORIENTATION_HORIZONTAL,) + + self._intro = intro + + label = hippo.CanvasText(text=_("Name:")) + self.append(label) + + self._entry = CanvasEntry(box_width=style.zoom(300)) + self._entry.set_background(_BACKGROUND_COLOR.get_html()) + self._entry.connect('notify::text', self._text_changed_cb) + + widget = self._entry.props.widget + widget.set_max_length(45) + + self.append(self._entry) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + self.reverse() + + def _text_changed_cb(self, entry, pspec): + valid = len(entry.props.text.strip()) > 0 + self.set_valid(valid) + + def get_name(self): + return self._entry.props.text + + def activate(self): + self._entry.props.widget.grab_focus() + +class _ColorPage(_Page): + def __init__(self, **kwargs): + _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER, + background_color=_BACKGROUND_COLOR.get_int(), + spacing=style.DEFAULT_SPACING, + yalign=hippo.ALIGNMENT_CENTER, **kwargs) + + self._label = hippo.CanvasText(text=_("Click to change color:"), + xalign=hippo.ALIGNMENT_CENTER) + self.append(self._label) + + self._cp = colorpicker.ColorPicker(xalign=hippo.ALIGNMENT_CENTER) + self.append(self._cp) + + self._color = self._cp.get_color() + self.set_valid(True) + + def get_color(self): + return self._cp.get_color() + +class _IntroBox(hippo.CanvasBox): + __gsignals__ = { + 'done': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])) + } + + PAGE_NAME = 0 + PAGE_COLOR = 1 + + PAGE_FIRST = PAGE_NAME + PAGE_LAST = PAGE_COLOR + + def __init__(self): + hippo.CanvasBox.__init__(self, padding=style.zoom(30), + background_color=_BACKGROUND_COLOR.get_int()) + + self._page = self.PAGE_NAME + self._name_page = _NamePage(self) + self._color_page = _ColorPage() + self._current_page = None + self._next_button = None + + self._setup_page() + + def _setup_page(self): + self.remove_all() + + if self._page == self.PAGE_NAME: + self._current_page = self._name_page + elif self._page == self.PAGE_COLOR: + self._current_page = self._color_page + + self.append(self._current_page, hippo.PACK_EXPAND) + + button_box = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL) + + if self._page != self.PAGE_FIRST: + back_button = hippo.CanvasButton(text=_('Back')) + image = Icon(icon_name='go-left') + back_button.props.widget.set_image(image) + back_button.connect('activated', self._back_activated_cb) + button_box.append(back_button) + + spacer = hippo.CanvasBox() + button_box.append(spacer, hippo.PACK_EXPAND) + + self._next_button = hippo.CanvasButton() + image = Icon(icon_name='go-right') + self._next_button.props.widget.set_image(image) + + if self._page == self.PAGE_LAST: + self._next_button.props.text = _('Done') + self._next_button.connect('activated', self._done_activated_cb) + else: + self._next_button.props.text = _('Next') + self._next_button.connect('activated', self._next_activated_cb) + + self._current_page.activate() + + self._update_next_button() + button_box.append(self._next_button) + + self._current_page.connect('notify::valid', + self._page_valid_changed_cb) + self.append(button_box) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + button_box.reverse() + + def _update_next_button(self): + widget = self._next_button.props.widget + widget.props.sensitive = self._current_page.props.valid + + def _page_valid_changed_cb(self, page, pspec): + self._update_next_button() + + def _back_activated_cb(self, item): + self.back() + + def back(self): + if self._page != self.PAGE_FIRST: + self._page -= 1 + self._setup_page() + + def _next_activated_cb(self, item): + self.next() + + def next(self): + if self._page == self.PAGE_LAST: + self.done() + if self._current_page.props.valid: + self._page += 1 + self._setup_page() + + def _done_activated_cb(self, item): + self.done() + + def done(self): + name = self._name_page.get_name() + color = self._color_page.get_color() + + self.emit('done', name, color) + +class IntroWindow(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + + self._canvas = hippo.Canvas() + self._intro_box = _IntroBox() + self._intro_box.connect('done', self._done_cb) + self._canvas.set_root(self._intro_box) + + self.add(self._canvas) + self._canvas.show() + self.connect('key-press-event', self.__key_press_cb) + + def _done_cb(self, box, name, color): + self.hide() + gobject.idle_add(self._create_profile_cb, name, color) + + def _create_profile_cb(self, name, color): + create_profile(name, color) + gtk.main_quit() + + return False + + def __key_press_cb(self, widget, event): + if gtk.gdk.keyval_name(event.keyval) == "Return": + self._intro_box.next() + return True + elif gtk.gdk.keyval_name(event.keyval) == "Escape": + self._intro_box.back() + return True + return False + + +if __name__ == "__main__": + w = IntroWindow() + w.show() + w.connect('destroy', gtk.main_quit) + gtk.main() diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am new file mode 100644 index 0000000..11886d6 --- /dev/null +++ b/src/jarabe/journal/Makefile.am @@ -0,0 +1,18 @@ +sugardir = $(pythondir)/jarabe/journal +sugar_PYTHON = \ + __init__.py \ + collapsedentry.py \ + detailview.py \ + expandedentry.py \ + journalactivity.py \ + journalentrybundle.py \ + journaltoolbox.py \ + keepicon.py \ + listview.py \ + misc.py \ + modalalert.py \ + objectchooser.py \ + palettes.py \ + query.py \ + volumesmanager.py \ + volumestoolbar.py diff --git a/src/jarabe/journal/__init__.py b/src/jarabe/journal/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/jarabe/journal/__init__.py diff --git a/src/jarabe/journal/collapsedentry.py b/src/jarabe/journal/collapsedentry.py new file mode 100644 index 0000000..235ac4c --- /dev/null +++ b/src/jarabe/journal/collapsedentry.py @@ -0,0 +1,385 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ + +import gobject +import gtk +import hippo +import json + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.xocolor import XoColor +from sugar.graphics import style +from sugar.datastore import datastore +from sugar.graphics.entry import CanvasEntry + +from jarabe.journal.keepicon import KeepIcon +from jarabe.journal.palettes import ObjectPalette, BuddyPalette +from jarabe.journal import misc + +class BuddyIcon(CanvasIcon): + def __init__(self, buddy, **kwargs): + CanvasIcon.__init__(self, **kwargs) + self._buddy = buddy + + def create_palette(self): + return BuddyPalette(self._buddy) + +class BuddyList(hippo.CanvasBox): + def __init__(self, model, width): + hippo.CanvasBox.__init__(self, + orientation=hippo.ORIENTATION_HORIZONTAL, + box_width=width, + xalign=hippo.ALIGNMENT_START) + self.set_model(model) + + def set_model(self, model): + for item in self.get_children(): + self.remove(item) + + for buddy in model[0:3]: + nick_, color = buddy + icon = BuddyIcon(buddy, + icon_name='computer-xo', + xo_color=XoColor(color), + cache=True) + self.append(icon) + +class EntryIcon(CanvasIcon): + def __init__(self, **kwargs): + CanvasIcon.__init__(self, **kwargs) + self._jobject = None + + def set_jobject(self, jobject): + self._jobject = jobject + self.props.file_name = misc.get_icon_name(jobject) + self.palette = None + + def create_palette(self): + if self.show_palette: + return ObjectPalette(self._jobject) + else: + return None + + show_palette = gobject.property(type=bool, default=False) + +class BaseCollapsedEntry(hippo.CanvasBox): + __gtype_name__ = 'BaseCollapsedEntry' + + _DATE_COL_WIDTH = style.GRID_CELL_SIZE * 3 + _BUDDIES_COL_WIDTH = style.GRID_CELL_SIZE * 3 + _PROGRESS_COL_WIDTH = style.GRID_CELL_SIZE * 5 + + def __init__(self): + hippo.CanvasBox.__init__(self, + spacing=style.DEFAULT_SPACING, + padding_top=style.DEFAULT_PADDING, + padding_bottom=style.DEFAULT_PADDING, + padding_left=style.DEFAULT_PADDING * 2, + padding_right=style.DEFAULT_PADDING * 2, + box_height=style.GRID_CELL_SIZE, + orientation=hippo.ORIENTATION_HORIZONTAL) + + self._jobject = None + self._is_selected = False + + self.keep_icon = self._create_keep_icon() + self.append(self.keep_icon) + + self.icon = self._create_icon() + self.append(self.icon) + + self.title = self._create_title() + self.append(self.title, hippo.PACK_EXPAND) + + self.buddies_list = self._create_buddies_list() + self.append(self.buddies_list) + + self.date = self._create_date() + self.append(self.date) + + # Progress controls + self.progress_bar = self._create_progress_bar() + self.append(self.progress_bar) + + self.cancel_button = self._create_cancel_button() + self.append(self.cancel_button) + + def _create_keep_icon(self): + keep_icon = KeepIcon(False) + keep_icon.connect('button-release-event', + self.__keep_icon_button_release_event_cb) + return keep_icon + + def _create_date(self): + date = hippo.CanvasText(text='', + xalign=hippo.ALIGNMENT_START, + font_desc=style.FONT_NORMAL.get_pango_desc(), + box_width=self._DATE_COL_WIDTH) + return date + + def _create_icon(self): + icon = EntryIcon(size=style.STANDARD_ICON_SIZE, cache=True) + return icon + + def _create_title(self): + # TODO: We'd prefer to ellipsize in the middle + title = hippo.CanvasText(text='', + xalign=hippo.ALIGNMENT_START, + font_desc=style.FONT_BOLD.get_pango_desc(), + size_mode=hippo.CANVAS_SIZE_ELLIPSIZE_END) + return title + + def _create_buddies_list(self): + return BuddyList([], self._BUDDIES_COL_WIDTH) + + def _create_progress_bar(self): + progress_bar = gtk.ProgressBar() + return hippo.CanvasWidget(widget=progress_bar, + yalign=hippo.ALIGNMENT_CENTER, + box_width=self._PROGRESS_COL_WIDTH) + + def _create_cancel_button(self): + button = CanvasIcon(icon_name='activity-stop', + size=style.SMALL_ICON_SIZE, + box_width=style.GRID_CELL_SIZE) + button.connect('button-release-event', + self._cancel_button_release_event_cb) + return button + + def _decode_buddies(self): + if self.jobject.metadata.has_key('buddies') and \ + self.jobject.metadata['buddies']: + # json cannot read unicode strings + buddies_str = self.jobject.metadata['buddies'].encode('utf8') + buddies = json.read(buddies_str).values() + else: + buddies = [] + return buddies + + def update_visibility(self): + in_process = self.is_in_progress() + + self.buddies_list.set_visible(not in_process) + self.date.set_visible(not in_process) + + self.progress_bar.set_visible(in_process) + self.cancel_button.set_visible(in_process) + + # TODO: determine the appearance of in-progress entries + def _update_color(self): + if self.is_in_progress(): + self.props.background_color = style.COLOR_WHITE.get_int() + else: + self.props.background_color = style.COLOR_WHITE.get_int() + + def is_in_progress(self): + return self._jobject.metadata.has_key('progress') and \ + int(self._jobject.metadata['progress']) < 100 + + def get_keep(self): + keep = int(self._jobject.metadata.get('keep', 0)) + return keep == 1 + + def __keep_icon_button_release_event_cb(self, button, event): + logging.debug('__keep_icon_button_release_event_cb') + jobject = datastore.get(self._jobject.object_id) + try: + if self.get_keep(): + jobject.metadata['keep'] = 0 + else: + jobject.metadata['keep'] = 1 + datastore.write(jobject, update_mtime=False) + finally: + jobject.destroy() + + self.keep_icon.props.keep = self.get_keep() + self._update_color() + + return True + + def _cancel_button_release_event_cb(self, button, event): + logging.debug('_cancel_button_release_event_cb') + datastore.delete(self._jobject.object_id) + return True + + def set_selected(self, is_selected): + self._is_selected = is_selected + self._update_color() + + def set_jobject(self, jobject): + self._jobject = jobject + self._is_selected = False + + self.keep_icon.props.keep = self.get_keep() + + self.date.props.text = misc.get_date(jobject) + + self.icon.set_jobject(jobject) + if jobject.is_activity_bundle(): + self.icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + self.icon.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + else: + if jobject.metadata.has_key('icon-color') and \ + jobject.metadata['icon-color']: + self.icon.props.xo_color = XoColor( \ + jobject.metadata['icon-color']) + else: + self.icon.props.xo_color = None + + if jobject.metadata.get('title', ''): + title_text = jobject.metadata['title'] + else: + title_text = _('Untitled') + self.title.props.text = title_text + + self.buddies_list.set_model(self._decode_buddies()) + + if jobject.metadata.has_key('progress'): + self.progress_bar.props.widget.props.fraction = \ + int(jobject.metadata['progress']) / 100.0 + + self.update_visibility() + self._update_color() + + def get_jobject(self): + return self._jobject + + jobject = property(get_jobject, set_jobject) + + def update_date(self): + self.date.props.text = misc.get_date(self._jobject) + +class CollapsedEntry(BaseCollapsedEntry): + __gtype_name__ = 'CollapsedEntry' + + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])) + } + + def __init__(self): + BaseCollapsedEntry.__init__(self) + + self.icon.props.show_palette = True + self.icon.connect('button-release-event', + self.__icon_button_release_event_cb) + + self.title.connect('button_release_event', + self.__title_button_release_event_cb) + + self._title_entry = self._create_title_entry() + self.insert_after(self._title_entry, self.title, hippo.PACK_EXPAND) + self._title_entry.set_visible(False) + + self._detail_button = self._create_detail_button() + self._detail_button.connect('motion-notify-event', + self.__detail_button_motion_notify_event_cb) + self.append(self._detail_button) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + self.reverse() + + def _create_title_entry(self): + title_entry = CanvasEntry() + title_entry.set_background(style.COLOR_WHITE.get_html()) + title_entry.props.widget.connect('focus-out-event', + self.__title_entry_focus_out_event_cb) + title_entry.props.widget.connect('activate', + self.__title_entry_activate_cb) + title_entry.connect('key-press-event', + self.__title_entry_key_press_event_cb) + return title_entry + + def _create_detail_button(self): + button = CanvasIcon(icon_name='go-right', + size=style.SMALL_ICON_SIZE, + box_width=style.GRID_CELL_SIZE * 3 / 5, + fill_color=style.COLOR_BUTTON_GREY.get_svg()) + button.connect('button-release-event', + self.__detail_button_release_event_cb) + return button + + def update_visibility(self): + BaseCollapsedEntry.update_visibility(self) + self._detail_button.set_visible(not self.is_in_progress()) + + def set_jobject(self, jobject): + BaseCollapsedEntry.set_jobject(self, jobject) + self._title_entry.props.text = self.title.props.text + + jobject = property(BaseCollapsedEntry.get_jobject, set_jobject) + + def __detail_button_release_event_cb(self, button, event): + logging.debug('_detail_button_release_event_cb') + if not self.is_in_progress(): + self.emit('detail-clicked') + return True + + def __detail_button_motion_notify_event_cb(self, button, event): + if event.detail == hippo.MOTION_DETAIL_ENTER: + button.props.fill_color = style.COLOR_TOOLBAR_GREY.get_svg() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + button.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + + def __icon_button_release_event_cb(self, button, event): + logging.debug('__icon_button_release_event_cb') + misc.resume(self.jobject) + return True + + def __title_button_release_event_cb(self, button, event): + self.title.set_visible(False) + self._title_entry.set_visible(True) + self._title_entry.props.widget.grab_focus() + + def __title_entry_focus_out_event_cb(self, entry, event): + self._apply_title_change(entry.props.text) + + def __title_entry_activate_cb(self, entry): + self._apply_title_change(entry.props.text) + + def __title_entry_key_press_event_cb(self, entry, event): + if event.key == hippo.KEY_ESCAPE: + self._cancel_title_change() + + def _apply_title_change(self, title): + self._title_entry.set_visible(False) + self.title.set_visible(True) + + if title == '': + self._cancel_title_change() + elif self.title.props.text != title: + self.title.props.text = title + self._jobject.metadata['title'] = title + self._jobject.metadata['title_set_by_user'] = '1' + datastore.write(self._jobject, update_mtime=False, + reply_handler=self._datastore_write_cb, + error_handler=self._datastore_write_error_cb) + + def _cancel_title_change(self): + self._title_entry.props.text = self.title.props.text + self._title_entry.set_visible(False) + self.title.set_visible(True) + + def _datastore_write_cb(self): + pass + + def _datastore_write_error_cb(self, error): + logging.error('CollapsedEntry._datastore_write_error_cb: %r' % error) + diff --git a/src/jarabe/journal/detailview.py b/src/jarabe/journal/detailview.py new file mode 100644 index 0000000..5748d6f --- /dev/null +++ b/src/jarabe/journal/detailview.py @@ -0,0 +1,133 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ + +import gobject +import gtk +import hippo + +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon +from sugar.datastore import datastore + +from jarabe.journal.expandedentry import ExpandedEntry + +class DetailView(gtk.VBox): + __gtype_name__ = 'DetailView' + + __gproperties__ = { + 'jobject' : (object, None, None, + gobject.PARAM_READWRITE) + } + + __gsignals__ = { + 'go-back-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + } + + def __init__(self, **kwargs): + self._jobject = None + self._expanded_entry = None + + canvas = hippo.Canvas() + + self._root = hippo.CanvasBox() + self._root.props.background_color = style.COLOR_PANEL_GREY.get_int() + canvas.set_root(self._root) + + back_bar = BackBar() + back_bar.connect('button-release-event', + self.__back_bar_release_event_cb) + self._root.append(back_bar) + + gobject.GObject.__init__(self, **kwargs) + + self.pack_start(canvas) + canvas.show() + + def _fav_icon_activated_cb(self, fav_icon): + keep = not self._expanded_entry.get_keep() + self._expanded_entry.set_keep(keep) + fav_icon.props.keep = keep + + def __back_bar_release_event_cb(self, back_bar, event): + self.emit('go-back-clicked') + return False + + def _update_view(self): + if self._expanded_entry: + self._root.remove(self._expanded_entry) + + # Work around pygobject bug #479227 + self._expanded_entry.remove_all() + import gc + gc.collect() + if self._jobject: + self._expanded_entry = ExpandedEntry(self._jobject.object_id) + self._root.append(self._expanded_entry, hippo.PACK_EXPAND) + + def refresh(self): + logging.debug('DetailView.refresh') + if self._jobject: + self._jobject = datastore.get(self._jobject.object_id) + self._update_view() + + def do_set_property(self, pspec, value): + if pspec.name == 'jobject': + self._jobject = value + self._update_view() + else: + raise AssertionError + + def do_get_property(self, pspec): + if pspec.name == 'jobject': + return self._jobject + else: + raise AssertionError + + +class BackBar(hippo.CanvasBox): + def __init__(self): + hippo.CanvasBox.__init__(self, + orientation=hippo.ORIENTATION_HORIZONTAL, + border=style.LINE_WIDTH, + background_color=style.COLOR_PANEL_GREY.get_int(), + border_color=style.COLOR_SELECTION_GREY.get_int(), + padding=style.DEFAULT_PADDING, + padding_left=style.DEFAULT_SPACING, + spacing=style.DEFAULT_SPACING) + + icon = CanvasIcon(icon_name='go-previous', + size=style.SMALL_ICON_SIZE, + fill_color=style.COLOR_TOOLBAR_GREY.get_svg()) + self.append(icon) + + label = hippo.CanvasText(text=_('Back'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + self.append(label) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + self.reverse() + + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + def __motion_notify_event_cb(self, box, event): + if event.detail == hippo.MOTION_DETAIL_ENTER: + box.props.background_color = style.COLOR_SELECTION_GREY.get_int() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + box.props.background_color = style.COLOR_PANEL_GREY.get_int() + return False diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py new file mode 100644 index 0000000..8957728 --- /dev/null +++ b/src/jarabe/journal/expandedentry.py @@ -0,0 +1,385 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import StringIO + +import hippo +import cairo +import gobject +import gtk +import json + +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.xocolor import XoColor +from sugar.graphics.entry import CanvasEntry +from sugar.datastore import datastore + +from jarabe.journal.keepicon import KeepIcon +from jarabe.journal.palettes import ObjectPalette, BuddyPalette +from jarabe.journal import misc + +class Separator(hippo.CanvasBox, hippo.CanvasItem): + def __init__(self, orientation): + hippo.CanvasBox.__init__(self, + background_color=style.COLOR_PANEL_GREY.get_int()) + + if orientation == hippo.ORIENTATION_VERTICAL: + self.props.box_width = style.LINE_WIDTH + else: + self.props.box_height = style.LINE_WIDTH + +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 + +class BuddyList(hippo.CanvasBox): + def __init__(self, model): + hippo.CanvasBox.__init__(self, xalign=hippo.ALIGNMENT_START, + orientation=hippo.ORIENTATION_HORIZONTAL) + + for buddy in model: + nick_, color = buddy + hbox = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL) + icon = CanvasIcon(icon_name='computer-xo', + xo_color=XoColor(color), + size=style.STANDARD_ICON_SIZE) + icon.set_palette(BuddyPalette(buddy)) + hbox.append(icon) + self.append(hbox) + +class ExpandedEntry(hippo.CanvasBox): + def __init__(self, object_id): + hippo.CanvasBox.__init__(self) + self.props.orientation = hippo.ORIENTATION_VERTICAL + self.props.background_color = style.COLOR_WHITE.get_int() + self.props.padding_top = style.DEFAULT_SPACING * 3 + + self._jobject = datastore.get(object_id) + self._update_title_sid = None + + # Create header + header = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + padding=style.DEFAULT_PADDING, + padding_right=style.GRID_CELL_SIZE, + spacing=style.DEFAULT_SPACING) + self.append(header) + + # Create two column body + + body = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + spacing=style.DEFAULT_SPACING * 3, + padding_left=style.GRID_CELL_SIZE, + padding_right=style.GRID_CELL_SIZE, + padding_top=style.DEFAULT_SPACING * 3) + + self.append(body, hippo.PACK_EXPAND) + + first_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + spacing=style.DEFAULT_SPACING) + body.append(first_column) + + second_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + spacing=style.DEFAULT_SPACING) + body.append(second_column, hippo.PACK_EXPAND) + + # Header + + self._keep_icon = self._create_keep_icon() + header.append(self._keep_icon) + + self._icon = self._create_icon() + header.append(self._icon) + + self._title = self._create_title() + header.append(self._title, hippo.PACK_EXPAND) + + # TODO: create a version list popup instead of a date label + self._date = self._create_date() + header.append(self._date) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + header.reverse() + + # First column + + self._preview = self._create_preview() + first_column.append(self._preview) + + # Second column + + description_box, self._description = self._create_description() + second_column.append(description_box) + + tags_box, self._tags = self._create_tags() + second_column.append(tags_box) + + self._buddy_list = self._create_buddy_list() + second_column.append(self._buddy_list) + + def _create_keep_icon(self): + keep = int(self._jobject.metadata.get('keep', 0)) == 1 + keep_icon = KeepIcon(keep) + keep_icon.connect('activated', self._keep_icon_activated_cb) + return keep_icon + + def _create_icon(self): + icon = CanvasIcon(file_name=misc.get_icon_name(self._jobject)) + icon.connect_after('button-release-event', + self._icon_button_release_event_cb) + + if self._jobject.is_activity_bundle(): + icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + icon.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + else: + if self._jobject.metadata.has_key('icon-color') and \ + self._jobject.metadata['icon-color']: + icon.props.xo_color = XoColor( \ + self._jobject.metadata['icon-color']) + + icon.set_palette(ObjectPalette(self._jobject)) + + return icon + + def _create_title(self): + title = CanvasEntry() + title.set_background(style.COLOR_WHITE.get_html()) + title.props.text = self._jobject.metadata.get('title', _('Untitled')) + title.props.widget.connect('focus-out-event', + self._title_focus_out_event_cb) + return title + + def _create_date(self): + date = hippo.CanvasText(xalign=hippo.ALIGNMENT_START, + font_desc=style.FONT_NORMAL.get_pango_desc(), + text = misc.get_date(self._jobject)) + return date + + def _create_preview(self): + width = style.zoom(320) + height = style.zoom(240) + box = hippo.CanvasBox() + + if self._jobject.metadata.has_key('preview') and \ + len(self._jobject.metadata['preview']) > 4: + + if self._jobject.metadata['preview'][1:4] == 'PNG': + preview_data = self._jobject.metadata['preview'] + else: + # TODO: We are close to be able to drop this. + import base64 + preview_data = base64.b64decode( + self._jobject.metadata['preview']) + + png_file = StringIO.StringIO(preview_data) + try: + surface = cairo.ImageSurface.create_from_png(png_file) + has_preview = True + except Exception, e: + logging.error('Error while loading the preview: %r' % e) + has_preview = False + else: + has_preview = False + + if has_preview: + preview_box = hippo.CanvasImage(image=surface, + border=style.LINE_WIDTH, + border_color=style.COLOR_BUTTON_GREY.get_int(), + xalign=hippo.ALIGNMENT_CENTER, + yalign=hippo.ALIGNMENT_CENTER, + scale_width=width, + scale_height=height) + else: + preview_box = hippo.CanvasText(text=_('No preview'), + font_desc=style.FONT_NORMAL.get_pango_desc(), + xalign=hippo.ALIGNMENT_CENTER, + yalign=hippo.ALIGNMENT_CENTER, + border=style.LINE_WIDTH, + border_color=style.COLOR_BUTTON_GREY.get_int(), + color=style.COLOR_BUTTON_GREY.get_int(), + box_width=width, + box_height=height) + preview_box.connect_after('button-release-event', + self._preview_box_button_release_event_cb) + box.append(preview_box) + return box + + def _create_buddy_list(self): + + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=_('Participants:'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + if self._jobject.metadata.has_key('buddies') and \ + self._jobject.metadata['buddies']: + # json cannot read unicode strings + buddies_str = self._jobject.metadata['buddies'].encode('utf8') + buddies = json.read(buddies_str).values() + vbox.append(BuddyList(buddies)) + return vbox + else: + return vbox + + def _create_description(self): + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=_('Description:'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + description = self._jobject.metadata.get('description', '') + text_view = CanvasTextView(description, + box_height=style.GRID_CELL_SIZE * 2) + vbox.append(text_view, hippo.PACK_EXPAND) + + text_view.text_view_widget.props.accepts_tab = False + text_view.text_view_widget.connect('focus-out-event', + self._description_focus_out_event_cb) + + return vbox, text_view + + def _create_tags(self): + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=_('Tags:'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + tags = self._jobject.metadata.get('tags', '') + text_view = CanvasTextView(tags, box_height=style.GRID_CELL_SIZE * 2) + vbox.append(text_view, hippo.PACK_EXPAND) + + text_view.text_view_widget.props.accepts_tab = False + text_view.text_view_widget.connect('focus-out-event', + self._tags_focus_out_event_cb) + + return vbox, text_view + + def _title_notify_text_cb(self, entry, pspec): + if not self._update_title_sid: + self._update_title_sid = gobject.timeout_add(1000, + self._update_title_cb) + + def _datastore_write_cb(self): + pass + + def _datastore_write_error_cb(self, error): + logging.error('ExpandedEntry._datastore_write_error_cb: %r' % error) + + def _title_focus_out_event_cb(self, entry, event): + self._update_entry() + + def _description_focus_out_event_cb(self, text_view, event): + self._update_entry() + + def _tags_focus_out_event_cb(self, text_view, event): + self._update_entry() + + def _update_entry(self): + needs_update = False + + old_title = self._jobject.metadata.get('title', None) + if old_title != self._title.props.text: + self._icon.palette.props.primary_text = self._title.props.text + self._jobject.metadata['title'] = self._title.props.text + self._jobject.metadata['title_set_by_user'] = '1' + needs_update = True + + old_tags = self._jobject.metadata.get('tags', None) + new_tags = self._tags.text_view_widget.props.buffer.props.text + if old_tags != new_tags: + self._jobject.metadata['tags'] = new_tags + needs_update = True + + old_description = self._jobject.metadata.get('description', None) + new_description = \ + self._description.text_view_widget.props.buffer.props.text + if old_description != new_description: + self._jobject.metadata['description'] = new_description + needs_update = True + + if needs_update: + datastore.write(self._jobject, update_mtime=False, + reply_handler=self._datastore_write_cb, + error_handler=self._datastore_write_error_cb) + + self._update_title_sid = None + + def get_keep(self): + return self._jobject.metadata.has_key('keep') and \ + self._jobject.metadata['keep'] == 1 + + def _keep_icon_activated_cb(self, keep_icon): + if self.get_keep(): + self._jobject.metadata['keep'] = 0 + else: + self._jobject.metadata['keep'] = 1 + datastore.write(self._jobject, update_mtime=False) + + keep_icon.props.keep = self.get_keep() + + def _icon_button_release_event_cb(self, button, event): + logging.debug('_icon_button_release_event_cb') + misc.resume(self._jobject) + return True + + def _preview_box_button_release_event_cb(self, button, event): + logging.debug('_preview_box_button_release_event_cb') + misc.resume(self._jobject) + return True + diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py new file mode 100644 index 0000000..51f0c61 --- /dev/null +++ b/src/jarabe/journal/journalactivity.py @@ -0,0 +1,338 @@ +# Copyright (C) 2006, Red Hat, Inc. +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import sys +import traceback +import uuid + +import gtk +import dbus +import statvfs +import os + +from sugar.graphics.window import Window +from sugar.bundle.bundle import ZipExtractException, RegistrationException +from sugar.datastore import datastore +from sugar import env +from sugar.activity import activityfactory +from sugar import wm + +from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox +from jarabe.journal.listview import ListView +from jarabe.journal.detailview import DetailView +from jarabe.journal.volumestoolbar import VolumesToolbar +from jarabe.journal import misc +from jarabe.journal.journalentrybundle import JournalEntryBundle +from jarabe.journal.objectchooser import ObjectChooser +from jarabe.journal.modalalert import ModalAlert + +DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' +DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' +DS_DBUS_PATH = '/org/laptop/sugar/DataStore' + +J_DBUS_SERVICE = 'org.laptop.Journal' +J_DBUS_INTERFACE = 'org.laptop.Journal' +J_DBUS_PATH = '/org/laptop/Journal' + +_SPACE_TRESHOLD = 52428800 +_BUNDLE_ID = 'org.laptop.JournalActivity' + +class JournalActivityDBusService(dbus.service.Object): + def __init__(self, parent): + self._parent = parent + session_bus = dbus.SessionBus() + bus_name = dbus.service.BusName(J_DBUS_SERVICE, + bus=session_bus, replace_existing=False, allow_replacement=False) + logging.debug('bus_name: %r', bus_name) + dbus.service.Object.__init__(self, bus_name, J_DBUS_PATH) + + @dbus.service.method(J_DBUS_INTERFACE, + in_signature='', out_signature='') + def FocusSearch(self): + """Become visible and give focus to the search entry + """ + self._parent.present() + self._parent.show_main_view() + self._parent.search_grab_focus() + + @dbus.service.method(J_DBUS_INTERFACE, + in_signature='s', out_signature='') + def ShowObject(self, object_id): + """Pop-up journal and show object with object_id""" + + logging.debug('Trying to show object %s', object_id) + + if self._parent.show_object(object_id): + self._parent.present() + + def _chooser_response_cb(self, chooser, response_id, chooser_id): + logging.debug('JournalActivityDBusService._chooser_response_cb') + if response_id == gtk.RESPONSE_ACCEPT: + object_id = chooser.get_selected_object_id() + self.ObjectChooserResponse(chooser_id, object_id) + else: + self.ObjectChooserCancelled(chooser_id) + chooser.destroy() + del chooser + + @dbus.service.method(J_DBUS_INTERFACE, in_signature='i', out_signature='s') + def ChooseObject(self, parent_xid): + chooser_id = uuid.uuid4().hex + if parent_xid > 0: + parent = gtk.gdk.window_foreign_new(parent_xid) + else: + parent = None + chooser = ObjectChooser(parent) + chooser.connect('response', self._chooser_response_cb, chooser_id) + chooser.show() + + return chooser_id + + @dbus.service.signal(J_DBUS_INTERFACE, signature="ss") + def ObjectChooserResponse(self, chooser_id, object_id): + pass + + @dbus.service.signal(J_DBUS_INTERFACE, signature="s") + def ObjectChooserCancelled(self, chooser_id): + pass + +class JournalActivity(Window): + def __init__(self): + Window.__init__(self) + + self.set_title(_('Journal')) + + self._main_view = None + self._secondary_view = None + self._list_view = None + self._detail_view = None + self._main_toolbox = None + self._detail_toolbox = None + + self._setup_main_view() + self._setup_secondary_view() + + self.add_events(gtk.gdk.ALL_EVENTS_MASK | + gtk.gdk.VISIBILITY_NOTIFY_MASK) + self._realized_sid = self.connect('realize', self.__realize_cb) + self.connect('visibility-notify-event', + self.__visibility_notify_event_cb) + self.connect('window-state-event', self.__window_state_event_cb) + self.connect('key-press-event', self._key_press_event_cb) + self.connect('focus-in-event', self._focus_in_event_cb) + + bus = dbus.SessionBus() + data_store = dbus.Interface( + bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), DS_DBUS_INTERFACE) + data_store.connect_to_signal('Created', self.__data_store_created_cb) + data_store.connect_to_signal('Updated', self.__data_store_updated_cb) + data_store.connect_to_signal('Deleted', self.__data_store_deleted_cb) + + self._dbus_service = JournalActivityDBusService(self) + + self.iconify() + + self._critical_space_alert = None + self._check_available_space() + + def __realize_cb(self, window): + wm.set_bundle_id(window.window, _BUNDLE_ID) + activity_id = activityfactory.create_activity_id() + wm.set_activity_id(window.window, str(activity_id)) + self.disconnect(self._realized_sid) + self._realized_sid = None + + def can_close(self): + return False + + def _setup_main_view(self): + self._main_toolbox = MainToolbox() + self._main_view = gtk.VBox() + + self._list_view = ListView() + self._list_view.connect('detail-clicked', self.__detail_clicked_cb) + self._main_view.pack_start(self._list_view) + self._list_view.show() + + volumes_toolbar = VolumesToolbar() + volumes_toolbar.connect('volume-changed', self._volume_changed_cb) + self._main_view.pack_start(volumes_toolbar, expand=False) + + search_toolbar = self._main_toolbox.search_toolbar + search_toolbar.connect('query-changed', self._query_changed_cb) + search_toolbar.set_volume_id(datastore.mounts()[0]['id']) + + def _setup_secondary_view(self): + self._secondary_view = gtk.VBox() + + self._detail_toolbox = DetailToolbox() + entry_toolbar = self._detail_toolbox.entry_toolbar + + self._detail_view = DetailView() + self._detail_view.connect('go-back-clicked', self.__go_back_clicked_cb) + self._secondary_view.pack_end(self._detail_view) + self._detail_view.show() + + def _key_press_event_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + logging.info(keyname) + logging.info(event.state) + if keyname == 'Escape': + self.show_main_view() + + def __detail_clicked_cb(self, list_view, entry): + self._show_secondary_view(entry.jobject) + + def __go_back_clicked_cb(self, detail_view): + self.show_main_view() + + def _query_changed_cb(self, toolbar, query): + self._list_view.update_with_query(query) + self.show_main_view() + + def show_main_view(self): + if self.toolbox != self._main_toolbox: + self.set_toolbox(self._main_toolbox) + self._main_toolbox.show() + + if self.canvas != self._main_view: + self.set_canvas(self._main_view) + self._main_view.show() + + def _show_secondary_view(self, jobject): + try: + self._detail_toolbox.entry_toolbar.set_jobject(jobject) + except Exception: + logging.error('Exception while displaying entry:\n' + \ + ''.join(traceback.format_exception(*sys.exc_info()))) + + self.set_toolbox(self._detail_toolbox) + self._detail_toolbox.show() + + try: + self._detail_view.props.jobject = jobject + except Exception: + logging.error('Exception while displaying entry:\n' + \ + ''.join(traceback.format_exception(*sys.exc_info()))) + + self.set_canvas(self._secondary_view) + self._secondary_view.show() + + def show_object(self, object_id): + jobject = datastore.get(object_id) + if jobject is None: + return False + else: + self._show_secondary_view(jobject) + return True + + def _volume_changed_cb(self, volume_toolbar, volume_id): + logging.debug('Selected volume: %r.' % volume_id) + self._main_toolbox.search_toolbar.set_volume_id(volume_id) + self._main_toolbox.set_current_toolbar(0) + + def __data_store_created_cb(self, uid): + jobject = datastore.get(uid) + if jobject is None: + return + try: + self._check_for_bundle(jobject) + finally: + jobject.destroy() + self._main_toolbox.search_toolbar.refresh_filters() + self._check_available_space() + + def __data_store_updated_cb(self, uid): + jobject = datastore.get(uid) + if jobject is None: + return + try: + self._check_for_bundle(jobject) + finally: + jobject.destroy() + self._check_available_space() + + def __data_store_deleted_cb(self, uid): + if self.canvas == self._secondary_view and \ + uid == self._detail_view.props.jobject.object_id: + self.show_main_view() + + def _focus_in_event_cb(self, window, event): + self.search_grab_focus() + self._list_view.update_dates() + + def _check_for_bundle(self, jobject): + bundle = misc.get_bundle(jobject) + if bundle is None: + return + + if bundle.is_installed(): + return + try: + bundle.install() + except (ZipExtractException, RegistrationException), e: + logging.warning('Could not install bundle %s: %r' % + (jobject.file_path, e)) + return + + if jobject.metadata['mime_type'] == JournalEntryBundle.MIME_TYPE: + datastore.delete(jobject.object_id) + + def search_grab_focus(self): + search_toolbar = self._main_toolbox.search_toolbar + search_toolbar.give_entry_focus() + + def __window_state_event_cb(self, window, event): + logging.debug('window_state_event_cb %r' % self) + if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED: + state = event.new_window_state + visible = not state & gtk.gdk.WINDOW_STATE_ICONIFIED + self._list_view.set_is_visible(visible) + + def __visibility_notify_event_cb(self, window, event): + logging.debug('visibility_notify_event_cb %r' % self) + visible = event.state != gtk.gdk.VISIBILITY_FULLY_OBSCURED + self._list_view.set_is_visible(visible) + + def _check_available_space(self): + ''' Check available space on device + + If the available space is below 50MB an alert will be + shown which encourages to delete old journal entries. + ''' + + if self._critical_space_alert: + return + stat = os.statvfs(env.get_profile_path()) + free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL] + if free_space < _SPACE_TRESHOLD: + self._critical_space_alert = ModalAlert() + self._critical_space_alert.connect('destroy', + self.__alert_closed_cb) + self._critical_space_alert.show() + + def __alert_closed_cb(self, data): + self.show_main_view() + self.present() + self._critical_space_alert = None + +def start(): + journal = JournalActivity() + journal.show() + diff --git a/src/jarabe/journal/journalentrybundle.py b/src/jarabe/journal/journalentrybundle.py new file mode 100644 index 0000000..8862ca3 --- /dev/null +++ b/src/jarabe/journal/journalentrybundle.py @@ -0,0 +1,96 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import tempfile +import shutil + +import json + +import dbus +from sugar.datastore import datastore +from sugar.bundle.bundle import Bundle, MalformedBundleException + +class JournalEntryBundle(Bundle): + """A Journal entry bundle + + See http://wiki.laptop.org/go/Journal_entry_bundles for details + """ + + MIME_TYPE = 'application/vnd.olpc-journal-entry' + + _zipped_extension = '.xoj' + _unzipped_extension = None + _infodir = None + + def __init__(self, path): + Bundle.__init__(self, path) + + def install(self): + if os.environ.has_key('SUGAR_ACTIVITY_ROOT'): + install_dir = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'], + 'data') + else: + install_dir = tempfile.gettempdir() + bundle_dir = os.path.join(install_dir, self._zip_root_dir) + uid = self._zip_root_dir + self._unzip(install_dir) + try: + metadata = self._read_metadata(bundle_dir) + jobject = datastore.create() + try: + for key, value in metadata.iteritems(): + jobject.metadata[key] = value + + preview = self._read_preview(uid, bundle_dir) + if preview is not None: + jobject.metadata['preview'] = dbus.ByteArray(preview) + + jobject.metadata['uid'] = '' + jobject.file_path = os.path.join(bundle_dir, uid) + datastore.write(jobject) + finally: + jobject.destroy() + finally: + shutil.rmtree(bundle_dir, ignore_errors=True) + + def _read_metadata(self, bundle_dir): + metadata_path = os.path.join(bundle_dir, '_metadata.json') + if not os.path.exists(metadata_path): + raise MalformedBundleException( + 'Bundle must contain the file "_metadata.json"') + f = open(metadata_path, 'r') + try: + json_data = f.read() + finally: + f.close() + return json.read(json_data) + + def _read_preview(self, uid, bundle_dir): + preview_path = os.path.join(bundle_dir, 'preview', uid) + if not os.path.exists(preview_path): + return '' + f = open(preview_path, 'r') + try: + preview_data = f.read() + finally: + f.close() + return preview_data + + def is_installed(self): + # These bundles can be reinstalled as many times as desired. + return False + diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py new file mode 100644 index 0000000..8d52a06 --- /dev/null +++ b/src/jarabe/journal/journaltoolbox.py @@ -0,0 +1,418 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging +from datetime import datetime, timedelta +import os + +import gobject +import gtk + +from sugar.graphics.toolbox import Toolbox +from sugar.graphics.toolcombobox import ToolComboBox +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.combobox import ComboBox +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.graphics import iconentry +from sugar.graphics import style +from sugar import activity +from sugar import profile +from sugar import mime +from sugar.datastore import datastore + +from jarabe.journal import volumesmanager +from jarabe.journal import misc + +_AUTOSEARCH_TIMEOUT = 1000 + +_ACTION_ANYTIME = 0 +_ACTION_TODAY = 1 +_ACTION_SINCE_YESTERDAY = 2 +_ACTION_PAST_WEEK = 3 +_ACTION_PAST_MONTH = 4 +_ACTION_PAST_YEAR = 5 + +_ACTION_ANYTHING = 0 + +_ACTION_EVERYBODY = 0 +_ACTION_MY_FRIENDS = 1 +_ACTION_MY_CLASS = 2 + +class MainToolbox(Toolbox): + def __init__(self): + Toolbox.__init__(self) + + self.search_toolbar = SearchToolbar() + self.search_toolbar.set_size_request(-1, style.GRID_CELL_SIZE) + self.add_toolbar(_('Search'), self.search_toolbar) + self.search_toolbar.show() + + #self.manage_toolbar = ManageToolbar() + #self.add_toolbar(_('Manage'), self.manage_toolbar) + #self.manage_toolbar.show() + +class SearchToolbar(gtk.Toolbar): + __gtype_name__ = 'SearchToolbar' + + __gsignals__ = { + 'query-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._volume_id = None + + self._search_entry = iconentry.IconEntry() + self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self._search_entry.connect('activate', self._search_entry_activated_cb) + self._search_entry.connect('changed', self._search_entry_changed_cb) + self._search_entry.add_clear_button() + self._autosearch_timer = None + self._add_widget(self._search_entry, expand=True) + + self._what_search_combo = ComboBox() + self._what_combo_changed_sid = self._what_search_combo.connect( + 'changed', self._combo_changed_cb) + tool_item = ToolComboBox(self._what_search_combo) + self.insert(tool_item, -1) + tool_item.show() + + self._when_search_combo = self._get_when_search_combo() + tool_item = ToolComboBox(self._when_search_combo) + self.insert(tool_item, -1) + tool_item.show() + + # TODO: enable it when the DS supports saving the buddies. + #self._with_search_combo = self._get_with_search_combo() + #tool_item = ToolComboBox(self._with_search_combo) + #self.insert(tool_item, -1) + #tool_item.show() + + self._query = self._build_query() + + self.refresh_filters() + + def give_entry_focus(self): + self._search_entry.grab_focus() + + def _get_when_search_combo(self): + when_search = ComboBox() + when_search.append_item(_ACTION_ANYTIME, _('Anytime')) + when_search.append_separator() + when_search.append_item(_ACTION_TODAY, _('Today')) + when_search.append_item(_ACTION_SINCE_YESTERDAY, + _('Since yesterday')) + # TRANS: Filter entries modified during the last 7 days. + when_search.append_item(_ACTION_PAST_WEEK, _('Past week')) + # TRANS: Filter entries modified during the last 30 days. + when_search.append_item(_ACTION_PAST_MONTH, _('Past month')) + # TRANS: Filter entries modified during the last 356 days. + when_search.append_item(_ACTION_PAST_YEAR, _('Past year')) + when_search.set_active(0) + when_search.connect('changed', self._combo_changed_cb) + return when_search + + def _get_with_search_combo(self): + with_search = ComboBox() + with_search.append_item(_ACTION_EVERYBODY, _('Anyone')) + with_search.append_separator() + with_search.append_item(_ACTION_MY_FRIENDS, _('My friends')) + with_search.append_item(_ACTION_MY_CLASS, _('My class')) + with_search.append_separator() + + # TODO: Ask the model for buddies. + with_search.append_item(3, 'Dan', 'theme:xo') + + with_search.set_active(0) + with_search.connect('changed', self._combo_changed_cb) + return with_search + + 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 _build_query(self): + query = {} + if self._volume_id: + query['mountpoints'] = [self._volume_id] + if self._what_search_combo.props.value: + value = self._what_search_combo.props.value + generic_type = mime.get_generic_type(value) + if generic_type: + mime_types = generic_type.mime_types + query['mime_type'] = mime_types + else: + query['activity'] = self._what_search_combo.props.value + if self._when_search_combo.props.value: + date_from, date_to = self._get_date_range() + query['mtime'] = {'start': date_from, 'end': date_to} + if self._search_entry.props.text: + text = self._search_entry.props.text.strip() + + if not text.startswith('"'): + query_text = '' + words = text.split(' ') + for word in words: + if word: + if query_text: + query_text += ' ' + query_text += word + '*' + else: + query_text = text + + if query_text: + query['query'] = query_text + + return query + + def _get_date_range(self): + today_start = datetime.today().replace(hour=0, minute=0, second=0) + right_now = datetime.today() + if self._when_search_combo.props.value == _ACTION_TODAY: + date_range = (today_start, right_now) + elif self._when_search_combo.props.value == _ACTION_SINCE_YESTERDAY: + date_range = (today_start - timedelta(1), right_now) + elif self._when_search_combo.props.value == _ACTION_PAST_WEEK: + date_range = (today_start - timedelta(7), right_now) + elif self._when_search_combo.props.value == _ACTION_PAST_MONTH: + date_range = (today_start - timedelta(30), right_now) + elif self._when_search_combo.props.value == _ACTION_PAST_YEAR: + date_range = (today_start - timedelta(356), right_now) + + return (date_range[0].isoformat(), + date_range[1].isoformat()) + + def _combo_changed_cb(self, combo): + new_query = self._build_query() + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def _search_entry_activated_cb(self, search_entry): + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + new_query = self._build_query() + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def _search_entry_changed_cb(self, search_entry): + if not search_entry.props.text: + search_entry.activate() + return + + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT, + self._autosearch_timer_cb) + + def _autosearch_timer_cb(self): + logging.debug('_autosearch_timer_cb') + self._autosearch_timer = None + self._search_entry.activate() + return False + + def set_volume_id(self, volume_id): + self._volume_id = volume_id + new_query = self._build_query() + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def refresh_filters(self): + current_value = self._what_search_combo.props.value + current_value_index = 0 + + self._what_search_combo.handler_block(self._what_combo_changed_sid) + try: + self._what_search_combo.remove_all() + # TRANS: Item in a combo box that filters by entry type. + self._what_search_combo.append_item(_ACTION_ANYTHING, _('Anything')) + + registry = activity.get_registry() + appended_separator = False + for service_name in datastore.get_unique_values('activity'): + activity_info = registry.get_activity(service_name) + if not activity_info is None: + if not appended_separator: + self._what_search_combo.append_separator() + appended_separator = True + + if os.path.exists(activity_info.icon): + self._what_search_combo.append_item(service_name, + activity_info.name, + file_name=activity_info.icon) + else: + self._what_search_combo.append_item(service_name, + activity_info.name, + icon_name='application-octet-stream') + + if service_name == current_value: + current_value_index = \ + len(self._what_search_combo.get_model()) - 1 + + self._what_search_combo.append_separator() + + types = mime.get_all_generic_types() + for generic_type in types : + self._what_search_combo.append_item( + generic_type.type_id, generic_type.name, generic_type.icon) + if generic_type.type_id == current_value: + current_value_index = \ + len(self._what_search_combo.get_model()) - 1 + + self._what_search_combo.set_active(current_value_index) + finally: + self._what_search_combo.handler_unblock( + self._what_combo_changed_sid) + +class ManageToolbar(gtk.Toolbar): + __gtype_name__ = 'ManageToolbar' + + def __init__(self): + gtk.Toolbar.__init__(self) + +class DetailToolbox(Toolbox): + def __init__(self): + Toolbox.__init__(self) + + self.entry_toolbar = EntryToolbar() + self.add_toolbar('', self.entry_toolbar) + self.entry_toolbar.show() + +class EntryToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + + self._jobject = None + + self._resume = ToolButton('activity-start') + self._resume.connect('clicked', self._resume_clicked_cb) + self.add(self._resume) + self._resume.show() + + self._copy = ToolButton() + + icon = Icon(icon_name='edit-copy', xo_color=profile.get_color()) + self._copy.set_icon_widget(icon) + icon.show() + + self._copy.set_tooltip(_('Copy')) + self._copy.connect('clicked', self._copy_clicked_cb) + self.add(self._copy) + self._copy.show() + + separator = gtk.SeparatorToolItem() + self.add(separator) + separator.show() + + erase_button = ToolButton('list-remove') + erase_button.set_tooltip(_('Erase')) + erase_button.connect('clicked', self._erase_button_clicked_cb) + self.add(erase_button) + erase_button.show() + + def set_jobject(self, jobject): + self._jobject = jobject + self._refresh_copy_palette() + self._refresh_resume_palette() + + def _resume_clicked_cb(self, button): + if self._jobject: + misc.resume(self._jobject) + + def _copy_clicked_cb(self, button): + clipboard = gtk.Clipboard() + clipboard.set_with_data([('text/uri-list', 0, 0)], + self._clipboard_get_func_cb, + self._clipboard_clear_func_cb) + + def _clipboard_get_func_cb(self, clipboard, selection_data, info, data): + selection_data.set_uris(['file://' + self._jobject.file_path]) + + def _clipboard_clear_func_cb(self, clipboard, data): + pass + + def _erase_button_clicked_cb(self, button): + if self._jobject: + bundle = misc.get_bundle(self._jobject) + if bundle is not None and bundle.is_installed(): + bundle.uninstall() + datastore.delete(self._jobject.object_id) + + def _resume_menu_item_activate_cb(self, menu_item, service_name): + if self._jobject: + misc.resume(self._jobject, service_name) + + def _copy_menu_item_activate_cb(self, menu_item, volume): + if self._jobject: + datastore.copy(self._jobject, volume.id) + + def _refresh_copy_palette(self): + palette = self._copy.get_palette() + + for menu_item in palette.menu.get_children(): + palette.menu.remove(menu_item) + menu_item.destroy() + + volumes_manager = volumesmanager.get_volumes_manager() + for volume in volumes_manager.get_volumes(): + if self._jobject.metadata['mountpoint'] == volume.id: + continue + menu_item = MenuItem(volume.name) + menu_item.set_image(Icon(icon_name=volume.icon_name, + icon_size=gtk.ICON_SIZE_MENU)) + menu_item.connect('activate', + self._copy_menu_item_activate_cb, + volume) + palette.menu.append(menu_item) + menu_item.show() + + def _refresh_resume_palette(self): + if self._jobject.metadata.get('activity_id', ''): + # TRANS: Action label for resuming an activity. + self._resume.set_tooltip(_('Resume')) + else: + # TRANS: Action label for starting an entry. + self._resume.set_tooltip(_('Start')) + + palette = self._resume.get_palette() + + for menu_item in palette.menu.get_children(): + palette.menu.remove(menu_item) + menu_item.destroy() + + for activity_info in misc.get_activities(self._jobject): + menu_item = MenuItem(activity_info.name) + menu_item.set_image(Icon(file=activity_info.icon, + icon_size=gtk.ICON_SIZE_MENU)) + menu_item.connect('activate', self._resume_menu_item_activate_cb, + activity_info.bundle_id) + palette.menu.append(menu_item) + menu_item.show() + diff --git a/src/jarabe/journal/keepicon.py b/src/jarabe/journal/keepicon.py new file mode 100644 index 0000000..8a86c83 --- /dev/null +++ b/src/jarabe/journal/keepicon.py @@ -0,0 +1,57 @@ +# Copyright (C) 2006, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import hippo + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar import profile + +class KeepIcon(CanvasIcon): + def __init__(self, keep): + CanvasIcon.__init__(self, icon_name='emblem-favorite', + box_width=style.GRID_CELL_SIZE * 3 / 5, + size=style.SMALL_ICON_SIZE) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + self._keep = None + self.set_keep(keep) + + def set_keep(self, keep): + if keep == self._keep: + return + + self._keep = keep + if keep: + self.props.xo_color = profile.get_color() + else: + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + + def get_keep(self): + return self._keep + + keep = gobject.property(type=int, default=0, getter=get_keep, + setter=set_keep) + + def __motion_notify_event_cb(self, icon, event): + if not self._keep: + if event.detail == hippo.MOTION_DETAIL_ENTER: + icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py new file mode 100644 index 0000000..befc7f4 --- /dev/null +++ b/src/jarabe/journal/listview.py @@ -0,0 +1,460 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import traceback +import sys +from gettext import gettext as _ + +import hippo +import gobject +import gtk +import dbus + +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon + +from jarabe.journal.collapsedentry import CollapsedEntry +from jarabe.journal import query + +DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' +DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' +DS_DBUS_PATH = '/org/laptop/sugar/DataStore' + +UPDATE_INTERVAL = 300000 + +EMPTY_JOURNAL = _("Your Journal is empty") +NO_MATCH = _("No matching entries ") + +class BaseListView(gtk.HBox): + __gtype_name__ = 'BaseListView' + + def __init__(self): + self._query = {} + self._result_set = None + self._entries = [] + self._page_size = 0 + self._last_value = -1 + self._reflow_sid = 0 + + gtk.HBox.__init__(self) + self.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS) + self.connect('key-press-event', self._key_press_event_cb) + + self._box = hippo.CanvasBox( + orientation=hippo.ORIENTATION_VERTICAL, + background_color=style.COLOR_WHITE.get_int()) + + self._canvas = hippo.Canvas() + self._canvas.set_root(self._box) + + self.pack_start(self._canvas) + self._canvas.show() + + self._vadjustment = gtk.Adjustment(value=0, lower=0, upper=0, + step_incr=1, page_incr=0, + page_size=0) + self._vadjustment.connect('value-changed', + self._vadjustment_value_changed_cb) + self._vadjustment.connect('changed', self._vadjustment_changed_cb) + + self._vscrollbar = gtk.VScrollbar(self._vadjustment) + self.pack_end(self._vscrollbar, expand=False, fill=False) + self._vscrollbar.show() + + self.connect('scroll-event', self._scroll_event_cb) + self.connect('destroy', self.__destroy_cb) + + # DND stuff + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._last_clicked_entry = None + self._canvas.drag_source_set(0, [], 0) + self._canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK | + gtk.gdk.POINTER_MOTION_HINT_MASK) + self._canvas.connect_after("motion_notify_event", + self._canvas_motion_notify_event_cb) + self._canvas.connect("button_press_event", + self._canvas_button_press_event_cb) + self._canvas.connect("drag_end", self._drag_end_cb) + self._canvas.connect("drag_data_get", self._drag_data_get_cb) + + # Auto-update stuff + self._fully_obscured = True + self._dirty = False + self._refresh_idle_handler = None + self._update_dates_timer = None + + bus = dbus.SessionBus() + datastore = dbus.Interface( + bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH), DS_DBUS_INTERFACE) + self._datastore_created_handler = \ + datastore.connect_to_signal('Created', + self.__datastore_created_cb) + self._datastore_updated_handler = \ + datastore.connect_to_signal('Updated', + self.__datastore_updated_cb) + + self._datastore_deleted_handler = \ + datastore.connect_to_signal('Deleted', + self.__datastore_deleted_cb) + + def __destroy_cb(self, widget): + self._datastore_created_handler.remove() + self._datastore_updated_handler.remove() + self._datastore_deleted_handler.remove() + + if self._result_set: + self._result_set.destroy() + + def _vadjustment_changed_cb(self, vadjustment): + if vadjustment.props.upper > self._page_size: + self._vscrollbar.show() + else: + self._vscrollbar.hide() + + def _vadjustment_value_changed_cb(self, vadjustment): + gobject.idle_add(self._do_scroll) + + def _do_scroll(self, force=False): + import time + t = time.time() + + value = int(self._vadjustment.props.value) + + if value == self._last_value and not force: + return + self._last_value = value + + self._result_set.seek(value) + jobjects = self._result_set.read(self._page_size) + + if self._result_set.length != self._vadjustment.props.upper: + self._vadjustment.props.upper = self._result_set.length + self._vadjustment.changed() + + self._refresh_view(jobjects) + self._dirty = False + + logging.debug('_do_scroll %r %r\n' % (value, (time.time() - t))) + + return False + + def _refresh_view(self, jobjects): + logging.debug('ListView %r' % self) + # Indicate when the Journal is empty + if len(jobjects) == 0: + self._show_message(EMPTY_JOURNAL) + return + + # Refresh view and create the entries if they don't exist yet. + for i in range(0, self._page_size): + try: + if i < len(jobjects): + if i >= len(self._entries): + entry = self.create_entry() + self._box.append(entry) + self._entries.append(entry) + entry.jobject = jobjects[i] + else: + entry = self._entries[i] + entry.jobject = jobjects[i] + entry.set_visible(True) + elif i < len(self._entries): + entry = self._entries[i] + entry.set_visible(False) + except Exception: + logging.error('Exception while displaying entry:\n' + \ + ''.join(traceback.format_exception(*sys.exc_info()))) + + def create_entry(self): + """ Create a descendant of BaseCollapsedEntry + """ + raise NotImplementedError + + def update_with_query(self, query_dict): + logging.debug('ListView.update_with_query') + self._query = query_dict + if self._page_size > 0: + self.refresh() + + def refresh(self): + if self._result_set: + self._result_set.destroy() + self._result_set = query.find(self._query) + self._vadjustment.props.upper = self._result_set.length + self._vadjustment.changed() + + self._vadjustment.props.value = min(self._vadjustment.props.value, + self._result_set.length - self._page_size) + if self._result_set.length == 0: + if self._query.get('query', '') or \ + self._query.get('mime_type', '') or \ + self._query.get('mtime', ''): + self._show_message(NO_MATCH) + else: + self._show_message(EMPTY_JOURNAL) + else: + self._clear_message() + self._do_scroll(force=True) + + def _scroll_event_cb(self, hbox, event): + if event.direction == gtk.gdk.SCROLL_UP: + if self._vadjustment.props.value > self._vadjustment.props.lower: + self._vadjustment.props.value -= 1 + elif event.direction == gtk.gdk.SCROLL_DOWN: + max_value = self._result_set.length - self._page_size + if self._vadjustment.props.value < max_value: + self._vadjustment.props.value += 1 + + def do_focus(self, direction): + if not self.is_focus(): + self.grab_focus() + return True + return False + + def _key_press_event_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + + if keyname == 'Up': + if self._vadjustment.props.value > self._vadjustment.props.lower: + self._vadjustment.props.value -= 1 + elif keyname == 'Down': + max_value = self._result_set.length - self._page_size + if self._vadjustment.props.value < max_value: + self._vadjustment.props.value += 1 + elif keyname == 'Page_Up' or keyname == 'KP_Page_Up': + new_position = max(0, + self._vadjustment.props.value - self._page_size) + if new_position != self._vadjustment.props.value: + self._vadjustment.props.value = new_position + elif keyname == 'Page_Down' or keyname == 'KP_Page_Down': + new_position = min(self._result_set.length - self._page_size, + self._vadjustment.props.value + self._page_size) + if new_position != self._vadjustment.props.value: + self._vadjustment.props.value = new_position + elif keyname == 'Home' or keyname == 'KP_Home': + new_position = 0 + if new_position != self._vadjustment.props.value: + self._vadjustment.props.value = new_position + elif keyname == 'End' or keyname == 'KP_End': + new_position = max(0, self._result_set.length - self._page_size) + if new_position != self._vadjustment.props.value: + self._vadjustment.props.value = new_position + else: + return False + + return True + + def do_size_allocate(self, allocation): + gtk.HBox.do_size_allocate(self, allocation) + new_page_size = int(allocation.height / style.GRID_CELL_SIZE) + + logging.debug("do_size_allocate: %r" % new_page_size) + + if new_page_size != self._page_size: + self._page_size = new_page_size + self._queue_reflow() + + def _queue_reflow(self): + if not self._reflow_sid: + self._reflow_sid = gobject.idle_add(self._reflow_idle_cb) + + def _reflow_idle_cb(self): + self._box.clear() + self._entries = [] + + self._vadjustment.props.page_size = self._page_size + self._vadjustment.props.page_increment = self._page_size + self._vadjustment.changed() + + if self._result_set is None: + self._result_set = query.find(self._query) + + max_value = max(0, self._result_set.length - self._page_size) + if self._vadjustment.props.value > max_value: + self._vadjustment.props.value = max_value + else: + self._do_scroll(force=True) + + self._reflow_sid = 0 + + def _show_message(self, message): + box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + background_color=style.COLOR_WHITE.get_int(), + yalign=hippo.ALIGNMENT_CENTER) + icon = CanvasIcon(size=style.LARGE_ICON_SIZE, + icon_name='activity-journal', + stroke_color = style.COLOR_BUTTON_GREY.get_svg(), + fill_color = style.COLOR_TRANSPARENT.get_svg()) + text = hippo.CanvasText(text=message, + xalign=hippo.ALIGNMENT_CENTER, + font_desc=style.FONT_NORMAL.get_pango_desc(), + color = style.COLOR_BUTTON_GREY.get_int()) + + box.append(icon) + box.append(text) + self._canvas.set_root(box) + + def _clear_message(self): + self._canvas.set_root(self._box) + + # TODO: Dnd methods. This should be merged somehow inside hippo-canvas. + def _canvas_motion_notify_event_cb(self, widget, event): + if not self._pressed_button: + return True + + # if the mouse button is not pressed, no drag should occurr + if not event.state & gtk.gdk.BUTTON1_MASK: + self._pressed_button = None + return True + + logging.debug("motion_notify_event_cb") + + if event.is_hint: + x, y, state_ = event.window.get_pointer() + else: + x = event.x + y = event.y + + if widget.drag_check_threshold(int(self._press_start_x), + int(self._press_start_y), + int(x), + int(y)): + context_ = widget.drag_begin([('text/uri-list', 0, 0), + ('journal-object-id', 0, 0)], + gtk.gdk.ACTION_COPY, + 1, + event) + return True + + def _drag_end_cb(self, widget, drag_context): + logging.debug("drag_end_cb") + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._last_clicked_entry = None + + def _drag_data_get_cb(self, widget, context, selection, target_type, + event_time): + logging.debug("drag_data_get_cb: requested target " + selection.target) + + jobject = self._last_clicked_entry.jobject + if selection.target == 'text/uri-list': + selection.set(selection.target, 8, jobject.file_path) + elif selection.target == 'journal-object-id': + selection.set(selection.target, 8, jobject.object_id) + + def _canvas_button_press_event_cb(self, widget, event): + logging.debug("button_press_event_cb") + + if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS: + self._last_clicked_entry = \ + self._get_entry_at_coords(event.x, event.y) + if self._last_clicked_entry: + self._pressed_button = event.button + self._press_start_x = event.x + self._press_start_y = event.y + + return False + + def _get_entry_at_coords(self, x, y): + for entry in self._box.get_children(): + entry_x, entry_y = entry.get_context().translate_to_widget(entry) + entry_width, entry_height = entry.get_allocation() + + if (x >= entry_x ) and (x <= entry_x + entry_width) and \ + (y >= entry_y ) and (y <= entry_y + entry_height): + return entry + return None + + def update_dates(self): + logging.debug('ListView.update_dates') + for entry in self._entries: + entry.update_date() + + def __datastore_created_cb(self, uid): + self._set_dirty() + + def __datastore_updated_cb(self, uid): + self._set_dirty() + + def __datastore_deleted_cb(self, uid): + self._set_dirty() + + def _set_dirty(self): + if self._fully_obscured: + self._dirty = True + else: + self._schedule_refresh() + + def _schedule_refresh(self): + if self._refresh_idle_handler is None: + logging.debug('Add refresh idle callback') + self._refresh_idle_handler = \ + gobject.idle_add(self.__refresh_idle_cb) + + def __refresh_idle_cb(self): + self.refresh() + if self._refresh_idle_handler is not None: + logging.debug('Remove refresh idle callback') + gobject.source_remove(self._refresh_idle_handler) + self._refresh_idle_handler = None + return False + + def set_is_visible(self, visible): + logging.debug('canvas_visibility_notify_event_cb %r' % visible) + if visible: + self._fully_obscured = False + if self._dirty: + self._schedule_refresh() + if self._update_dates_timer is None: + logging.debug('Adding date updating timer') + self._update_dates_timer = \ + gobject.timeout_add(UPDATE_INTERVAL, + self.__update_dates_timer_cb) + else: + self._fully_obscured = True + if self._update_dates_timer is not None: + logging.debug('Remove date updating timer') + gobject.source_remove(self._update_dates_timer) + self._update_dates_timer = None + + def __update_dates_timer_cb(self): + self.update_dates() + return True + +class ListView(BaseListView): + __gtype_name__ = 'ListView' + + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])) + } + + def __init__(self): + BaseListView.__init__(self) + + def create_entry(self): + entry = CollapsedEntry() + entry.connect('detail-clicked', self.__entry_activated_cb) + return entry + + def __entry_activated_cb(self, entry): + self.emit('detail-clicked', entry) + diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py new file mode 100644 index 0000000..42d179b --- /dev/null +++ b/src/jarabe/journal/misc.py @@ -0,0 +1,188 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import time +import traceback +import sys +import os +from gettext import gettext as _ + +import gtk + +from sugar import activity +from sugar.activity import activityfactory +from sugar.activity.activityhandle import ActivityHandle +from sugar import mime +from sugar.bundle.activitybundle import ActivityBundle +from sugar.bundle.contentbundle import ContentBundle +from sugar.bundle.bundle import MalformedBundleException +from sugar import util + +from jarabe.journal.journalentrybundle import JournalEntryBundle + +def _get_icon_file_name(icon_name): + icon_theme = gtk.icon_theme_get_default() + info = icon_theme.lookup_icon(icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR, 0) + if not info: + # display standard icon when icon for mime type is not found + info = icon_theme.lookup_icon('application-octet-stream', + gtk.ICON_SIZE_LARGE_TOOLBAR, 0) + fname = info.get_filename() + del info + return fname + +_icon_cache = util.LRU(50) + +def get_icon_name(jobject): + + cache_key = (jobject.object_id, jobject.metadata.get('timestamp', None)) + if cache_key in _icon_cache: + return _icon_cache[cache_key] + + file_name = None + + if jobject.is_activity_bundle() and jobject.file_path: + try: + bundle = ActivityBundle(jobject.file_path) + file_name = bundle.get_icon() + except Exception: + logging.warning('Could not read bundle:\n' + \ + ''.join(traceback.format_exception(*sys.exc_info()))) + file_name = _get_icon_file_name('application-octet-stream') + + if not file_name and jobject.metadata['activity']: + service_name = jobject.metadata['activity'] + activity_info = activity.get_registry().get_activity(service_name) + if activity_info: + file_name = activity_info.icon + + mime_type = jobject.metadata['mime_type'] + if not file_name and mime_type: + icon_name = mime.get_mime_icon(mime_type) + if icon_name: + file_name = _get_icon_file_name(icon_name) + + if not file_name or not os.path.exists(file_name): + file_name = _get_icon_file_name('application-octet-stream') + + _icon_cache[cache_key] = file_name + + return file_name + +def get_date(jobject): + """ Convert from a string in iso format to a more human-like format. """ + if jobject.metadata.has_key('timestamp'): + timestamp = float(jobject.metadata['timestamp']) + return util.timestamp_to_elapsed_string(timestamp) + elif jobject.metadata.has_key('mtime'): + ti = time.strptime(jobject.metadata['mtime'], "%Y-%m-%dT%H:%M:%S") + return util.timestamp_to_elapsed_string(time.mktime(ti)) + else: + return _('No date') + +def get_bundle(jobject): + try: + if jobject.is_activity_bundle() and jobject.file_path: + return ActivityBundle(jobject.file_path) + elif jobject.is_content_bundle() and jobject.file_path: + return ContentBundle(jobject.file_path) + elif jobject.metadata['mime_type'] == JournalEntryBundle.MIME_TYPE \ + and jobject.file_path: + return JournalEntryBundle(jobject.file_path) + else: + return None + except MalformedBundleException, e: + logging.warning('Incorrect bundle: %r' % e) + return None + +def _get_activities_for_mime(mime_type): + registry = activity.get_registry() + result = registry.get_activities_for_type(mime_type) + if not result: + for parent_mime in mime.get_mime_parents(mime_type): + result.extend(registry.get_activities_for_type(parent_mime)) + return result + +def get_activities(jobject): + activities = [] + + bundle_id = jobject.metadata.get('activity', '') + if bundle_id: + activity_info = activity.get_registry().get_activity(bundle_id) + if activity_info: + activities.append(activity_info) + + mime_type = jobject.metadata.get('mime_type', '') + if mime_type: + activities_info = _get_activities_for_mime(mime_type) + for activity_info in activities_info: + if activity_info.bundle_id != bundle_id: + activities.append(activity_info) + + return activities + +def resume(jobject, bundle_id=None): + if jobject.is_activity_bundle() and not bundle_id: + + logging.debug('Creating activity bundle') + bundle = ActivityBundle(jobject.file_path) + if not bundle.is_installed(): + logging.debug('Installing activity bundle') + bundle.install() + elif bundle.need_upgrade(): + logging.debug('Upgrading activity bundle') + bundle.upgrade() + + logging.debug('activityfactory.creating bundle with id %r', + bundle.get_bundle_id()) + activityfactory.create(bundle.get_bundle_id()) + + elif jobject.is_content_bundle() and not bundle_id: + + logging.debug('Creating content bundle') + bundle = ContentBundle(jobject.file_path) + if not bundle.is_installed(): + logging.debug('Installing content bundle') + bundle.install() + + activities = _get_activities_for_mime('text/html') + if len(activities) == 0: + logging.warning('No activity can open HTML content bundles') + return + + uri = bundle.get_start_uri() + logging.debug('activityfactory.creating with uri %s', uri) + activityfactory.create_with_uri(activities[0].bundle_id, + bundle.get_start_uri()) + else: + if not get_activities(jobject) and bundle_id is None: + logging.warning('No activity can open this object, %s.' % + jobject.metadata.get('mime_type', None)) + return + if bundle_id is None: + bundle_id = get_activities(jobject)[0].bundle_id + + activity_id = jobject.metadata['activity_id'] + object_id = jobject.object_id + + if activity_id: + handle = ActivityHandle(object_id=object_id, + activity_id=activity_id) + activityfactory.create(bundle_id, handle) + else: + activityfactory.create_with_object_id(bundle_id, object_id) + diff --git a/src/jarabe/journal/modalalert.py b/src/jarabe/journal/modalalert.py new file mode 100644 index 0000000..6c7bce9 --- /dev/null +++ b/src/jarabe/journal/modalalert.py @@ -0,0 +1,93 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +from gettext import gettext as _ + +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar import profile + +class ModalAlert(gtk.Window): + + __gtype_name__ = 'SugarModalAlert' + + def __init__(self): + gtk.Window.__init__(self) + + self.set_border_width(style.LINE_WIDTH) + offset = style.GRID_CELL_SIZE + width = gtk.gdk.screen_width() - offset * 2 + height = gtk.gdk.screen_height() - offset * 2 + self.set_size_request(width, height) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_decorated(False) + self.set_resizable(False) + self.set_modal(True) + + self._main_view = gtk.EventBox() + self._vbox = gtk.VBox() + self._vbox.set_spacing(style.DEFAULT_SPACING) + self._vbox.set_border_width(style.GRID_CELL_SIZE * 2) + self._main_view.modify_bg(gtk.STATE_NORMAL, + style.COLOR_BLACK.get_gdk_color()) + self._main_view.add(self._vbox) + self._vbox.show() + + icon = Icon(icon_name='activity-journal', + pixel_size=style.XLARGE_ICON_SIZE, + xo_color=profile.get_color()) + self._vbox.pack_start(icon, False) + icon.show() + + self._title = gtk.Label() + self._title.modify_fg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + self._title.set_markup('<b>%s</b>' % _('Your Journal is full')) + self._vbox.pack_start(self._title, False) + self._title.show() + + self._message = gtk.Label(_('Please delete some old Journal' + ' entries to make space for new ones.')) + self._message.modify_fg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + self._vbox.pack_start(self._message, False) + self._message.show() + + alignment = gtk.Alignment(xalign=0.5, yalign=0.5) + self._vbox.pack_start(alignment, expand=False) + alignment.show() + + self._show_journal = gtk.Button() + self._show_journal.set_label(_('Show Journal')) + alignment.add(self._show_journal) + self._show_journal.show() + self._show_journal.connect('clicked', self.__show_journal_cb) + + self.add(self._main_view) + self._main_view.show() + + self.connect("realize", self.__realize_cb) + + def __realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + + def __show_journal_cb(self, button): + '''The opener will listen on the destroy signal + ''' + self.destroy() + diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py new file mode 100644 index 0000000..947141d --- /dev/null +++ b/src/jarabe/journal/objectchooser.py @@ -0,0 +1,199 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging + +import gobject +import gtk +import hippo + +from sugar.graphics import style +from sugar.graphics.toolbutton import ToolButton +from sugar.datastore import datastore + +from jarabe.journal.listview import ListView +from jarabe.journal.collapsedentry import BaseCollapsedEntry +from jarabe.journal.journaltoolbox import SearchToolbar +from jarabe.journal.volumestoolbar import VolumesToolbar + +class ObjectChooser(gtk.Window): + + __gtype_name__ = 'ObjectChooser' + + __gsignals__ = { + 'response': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([int])) + } + + def __init__(self, parent=None): + gtk.Window.__init__(self) + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.set_decorated(False) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_border_width(style.LINE_WIDTH) + + self._selected_object_id = None + + self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) + self.connect('visibility-notify-event', + self.__visibility_notify_event_cb) + self.connect('delete-event', self.__delete_event_cb) + self.connect('key-press-event', self.__key_press_event_cb) + if parent is not None: + self.connect('realize', self.__realize_cb, parent) + + vbox = gtk.VBox() + self.add(vbox) + vbox.show() + + title_box = TitleBox() + title_box.connect('volume-changed', self.__volume_changed_cb) + title_box.close_button.connect('clicked', + self.__close_button_clicked_cb) + title_box.set_size_request(-1, style.GRID_CELL_SIZE) + vbox.pack_start(title_box, expand=False) + title_box.show() + + separator = gtk.HSeparator() + vbox.pack_start(separator, expand=False) + separator.show() + + self._toolbar = SearchToolbar() + self._toolbar.connect('query-changed', self.__query_changed_cb) + self._toolbar.set_size_request(-1, style.GRID_CELL_SIZE) + vbox.pack_start(self._toolbar, expand=False) + self._toolbar.show() + + self._list_view = ChooserListView() + self._list_view.connect('entry-activated', self.__entry_activated_cb) + vbox.pack_start(self._list_view) + self._list_view.show() + + self._toolbar.set_volume_id(datastore.mounts()[0]['id']) + + width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2 + height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2 + self.set_size_request(width, height) + + def __realize_cb(self, chooser, parent): + self.window.set_transient_for(parent) + # TODO: Should we disconnect the signal here? + + def __entry_activated_cb(self, list_view, entry): + self._selected_object_id = entry.jobject.object_id + self.emit('response', gtk.RESPONSE_ACCEPT) + + def __delete_event_cb(self, chooser, event): + self.emit('response', gtk.RESPONSE_DELETE_EVENT) + + def __key_press_event_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == 'Escape': + self.emit('response', gtk.RESPONSE_DELETE_EVENT) + + def __close_button_clicked_cb(self, button): + self.emit('response', gtk.RESPONSE_DELETE_EVENT) + + def get_selected_object_id(self): + return self._selected_object_id + + def __query_changed_cb(self, toolbar, query): + self._list_view.update_with_query(query) + + def __volume_changed_cb(self, volume_toolbar, volume_id): + logging.debug('Selected volume: %r.' % volume_id) + self._toolbar.set_volume_id(volume_id) + + def __visibility_notify_event_cb(self, window, event): + logging.debug('visibility_notify_event_cb %r' % self) + visible = event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED + self._list_view.set_is_visible(visible) + +class TitleBox(VolumesToolbar): + __gtype_name__ = 'TitleBox' + + def __init__(self): + VolumesToolbar.__init__(self) + + label = gtk.Label() + label.set_markup('<b>%s</b>' % _('Choose an object')) + label.set_alignment(0, 0.5) + self._add_widget(label, expand=True) + + self.close_button = ToolButton(icon_name='dialog-cancel') + self.close_button.set_tooltip(_('Close')) + self.insert(self.close_button, -1) + self.close_button.show() + + 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() + +class ChooserCollapsedEntry(BaseCollapsedEntry): + __gtype_name__ = 'ChooserCollapsedEntry' + + __gsignals__ = { + 'entry-activated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])) + } + + def __init__(self): + BaseCollapsedEntry.__init__(self) + + self.connect_after('button-release-event', + self.__button_release_event_cb) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + def __button_release_event_cb(self, entry, event): + self.emit('entry-activated') + return True + + def __motion_notify_event_cb(self, entry, event): + if event.detail == hippo.MOTION_DETAIL_ENTER: + self.props.background_color = style.COLOR_PANEL_GREY.get_int() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + self.props.background_color = style.COLOR_WHITE.get_int() + return False + +class ChooserListView(ListView): + __gtype_name__ = 'ChooserListView' + + __gsignals__ = { + 'entry-activated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])) + } + + def __init__(self): + ListView.__init__(self) + + def create_entry(self): + entry = ChooserCollapsedEntry() + entry.connect('entry-activated', self.__entry_activated_cb) + return entry + + def __entry_activated_cb(self, entry): + self.emit('entry-activated', entry) + diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py new file mode 100644 index 0000000..dfed3b9 --- /dev/null +++ b/src/jarabe/journal/palettes.py @@ -0,0 +1,115 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging + +import gtk + +from sugar import profile +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.datastore import datastore +from sugar.graphics.xocolor import XoColor + +from jarabe.journal import misc + +class ObjectPalette(Palette): + def __init__(self, jobject): + + self._jobject = jobject + + activity_icon = Icon(icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + activity_icon.props.file = misc.get_icon_name(jobject) + if jobject.metadata.has_key('icon-color') and \ + jobject.metadata['icon-color']: + activity_icon.props.xo_color = \ + XoColor(jobject.metadata['icon-color']) + else: + activity_icon.props.xo_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + + if jobject.metadata.has_key('title'): + title = jobject.metadata['title'] + else: + title = _('Untitled') + + Palette.__init__(self, primary_text=title, + icon=activity_icon) + + if jobject.metadata.get('activity_id', ''): + resume_label = _('Resume') + else: + resume_label = _('Start') + menu_item = MenuItem(resume_label, 'activity-start') + menu_item.connect('activate', self.__start_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + # TODO: Add "Start with" menu item + + menu_item = MenuItem(_('Copy')) + icon = Icon(icon_name='edit-copy', xo_color=profile.get_color(), + icon_size=gtk.ICON_SIZE_MENU) + menu_item.set_image(icon) + menu_item.connect('activate', self.__copy_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Erase'), 'list-remove') + menu_item.connect('activate', self.__erase_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __start_activate_cb(self, menu_item): + misc.resume(self._jobject) + + def __copy_activate_cb(self, menu_item): + clipboard = gtk.Clipboard() + clipboard.set_with_data([('text/uri-list', 0, 0)], + self.__clipboard_get_func_cb, + self.__clipboard_clear_func_cb) + + def __clipboard_get_func_cb(self, clipboard, selection_data, info, data): + logging.debug('__clipboard_get_func_cb %r' % self._jobject.file_path) + selection_data.set_uris(['file://' + self._jobject.file_path]) + + def __clipboard_clear_func_cb(self, clipboard, data): + pass + + def __erase_activate_cb(self, menu_item): + bundle = misc.get_bundle(self._jobject) + if bundle is not None and bundle.is_installed(): + bundle.uninstall() + datastore.delete(self._jobject.object_id) + + +class BuddyPalette(Palette): + def __init__(self, buddy): + self._buddy = buddy + + nick, colors = buddy + buddy_icon = Icon(icon_name='computer-xo', + icon_size=style.STANDARD_ICON_SIZE, + xo_color=XoColor(colors)) + + Palette.__init__(self, primary_text=nick, + icon=buddy_icon) + + # TODO: Support actions on buddies, like make friend, invite, etc. diff --git a/src/jarabe/journal/query.py b/src/jarabe/journal/query.py new file mode 100644 index 0000000..04d9b16 --- /dev/null +++ b/src/jarabe/journal/query.py @@ -0,0 +1,266 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +from sugar.datastore import datastore + +# Properties the journal cares about. +PROPERTIES = ['uid', 'title', 'mtime', 'timestamp', 'keep', 'buddies', + 'icon-color', 'mime_type', 'progress', 'activity', 'mountpoint', + 'activity_id'] + +class _Cache(object): + + __gtype_name__ = 'query_Cache' + + def __init__(self, jobjects=None): + self._array = [] + self._dict = {} + if jobjects is not None: + self.append_all(jobjects) + + def prepend_all(self, jobjects): + for jobject in jobjects[::-1]: + self._array.insert(0, jobject) + self._dict[jobject.object_id] = jobject + + def append_all(self, jobjects): + for jobject in jobjects: + self._array.append(jobject) + self._dict[jobject.object_id] = jobject + + def remove_all(self, jobjects): + jobjects = jobjects[:] + for jobject in jobjects: + obj = self._dict[jobject.object_id] + self._array.remove(obj) + del self._dict[obj.object_id] + obj.destroy() + + def __len__(self): + return len(self._array) + + def __getitem__(self, key): + if isinstance(key, basestring): + return self._dict[key] + else: + return self._array[key] + + def destroy(self): + self._destroy_jobjects(self._array) + self._array = [] + self._dict = {} + + def _destroy_jobjects(self, jobjects): + for jobject in jobjects: + jobject.destroy() + +class ResultSet(object): + + _CACHE_LIMIT = 80 + + def __init__(self, query, sorting): + self._total_count = -1 + self._position = -1 + self._query = query + self._sorting = sorting + + self._offset = 0 + self._cache = _Cache() + + def destroy(self): + self._cache.destroy() + + def get_length(self): + if self._total_count == -1: + jobjects, self._total_count = datastore.find(self._query, + sorting=self._sorting, + limit=ResultSet._CACHE_LIMIT, + properties=PROPERTIES) + self._cache.append_all(jobjects) + self._offset = 0 + return self._total_count + + length = property(get_length) + + def seek(self, position): + self._position = position + + def read(self, max_count): + logging.debug('ResultSet.read position: %r' % self._position) + + if max_count * 5 > ResultSet._CACHE_LIMIT: + raise RuntimeError( + 'max_count (%i) too big for ResultSet._CACHE_LIMIT' + ' (%i).' % (max_count, ResultSet._CACHE_LIMIT)) + + if self._position == -1: + self.seek(0) + + if self._position < self._offset: + remaining_forward_entries = 0 + else: + remaining_forward_entries = self._offset + len(self._cache) - \ + self._position + + if self._position > self._offset + len(self._cache): + remaining_backwards_entries = 0 + else: + remaining_backwards_entries = self._position - self._offset + + last_cached_entry = self._offset + len(self._cache) + + if (remaining_forward_entries <= 0 and + remaining_backwards_entries <= 0) or \ + max_count > ResultSet._CACHE_LIMIT: + + # Total cache miss: remake it + offset = max(0, self._position - max_count) + logging.debug('remaking cache, offset: %r limit: %r' % \ + (offset, max_count * 2)) + jobjects, self._total_count = datastore.find(self._query, + sorting=self._sorting, + offset=offset, + limit=ResultSet._CACHE_LIMIT, + properties=PROPERTIES) + + self._cache.remove_all(self._cache) + self._cache.append_all(jobjects) + self._offset = offset + + elif remaining_forward_entries < 2 * max_count and \ + last_cached_entry < self._total_count: + + # Add one page to the end of cache + logging.debug('appending one more page, offset: %r' % \ + last_cached_entry) + jobjects, self._total_count = datastore.find(self._query, + sorting=self._sorting, + offset=last_cached_entry, + limit=max_count, + properties=PROPERTIES) + # update cache + self._cache.append_all(jobjects) + + # apply the cache limit + objects_excess = len(self._cache) - ResultSet._CACHE_LIMIT + if objects_excess > 0: + self._offset += objects_excess + self._cache.remove_all(self._cache[:objects_excess]) + + elif remaining_backwards_entries < 2 * max_count and self._offset > 0: + + # Add one page to the beginning of cache + limit = min(self._offset, max_count) + self._offset = max(0, self._offset - max_count) + + logging.debug('prepending one more page, offset: %r limit: %r' % + (self._offset, limit)) + jobjects, self._total_count = datastore.find(self._query, + sorting=self._sorting, + offset=self._offset, + limit=limit, + properties=PROPERTIES) + + # update cache + self._cache.prepend_all(jobjects) + + # apply the cache limit + objects_excess = len(self._cache) - ResultSet._CACHE_LIMIT + if objects_excess > 0: + self._cache.remove_all(self._cache[-objects_excess:]) + else: + logging.debug('cache hit and no need to grow the cache') + + first_pos = self._position - self._offset + last_pos = self._position - self._offset + max_count + return self._cache[first_pos:last_pos] + +def find(query, sorting=None): + if sorting is None: + sorting = ['-mtime'] + result_set = ResultSet(query, sorting) + return result_set + +def test(): + TOTAL_ITEMS = 1000 + SCREEN_SIZE = 10 + + def mock_debug(string): + print "\tDEBUG: %s" % string + logging.debug = mock_debug + + def mock_find(query, sorting=None, limit=None, offset=None, + properties=None): + if properties is None: + properties = [] + + print "mock_find %r %r" % (offset, (offset + limit)) + + if limit is None or offset is None: + raise RuntimeError("Unimplemented test.") + + result = [] + for index in range(offset, offset + limit): + obj = datastore.DSObject(index, datastore.DSMetadata({}), '') + result.append(obj) + + return result, TOTAL_ITEMS + datastore.find = mock_find + + result_set = find({}) + + print "Get first page" + objects = result_set.read(SCREEN_SIZE) + print [obj.object_id for obj in objects] + assert range(0, SCREEN_SIZE) == [obj.object_id for obj in objects] + print "" + + print "Scroll to 5th item" + result_set.seek(5) + objects = result_set.read(SCREEN_SIZE) + print [obj.object_id for obj in objects] + assert range(5, SCREEN_SIZE + 5) == [obj.object_id for obj in objects] + print "" + + print "Scroll back to beginning" + result_set.seek(0) + objects = result_set.read(SCREEN_SIZE) + print [obj.object_id for obj in objects] + assert range(0, SCREEN_SIZE) == [obj.object_id for obj in objects] + print "" + + print "Hit PgDn five times" + for i in range(0, 5): + result_set.seek((i + 1) * SCREEN_SIZE) + objects = result_set.read(SCREEN_SIZE) + print [obj.object_id for obj in objects] + assert range((i + 1) * SCREEN_SIZE, (i + 2) * SCREEN_SIZE) == \ + [obj.object_id for obj in objects] + print "" + + print "Hit PgUp five times" + for i in range(0, 5)[::-1]: + result_set.seek(i * SCREEN_SIZE) + objects = result_set.read(SCREEN_SIZE) + print [obj.object_id for obj in objects] + assert range(i * SCREEN_SIZE, (i + 1) * SCREEN_SIZE) == \ + [obj.object_id for obj in objects] + print "" + +if __name__ == "__main__": + test() diff --git a/src/jarabe/journal/volumesmanager.py b/src/jarabe/journal/volumesmanager.py new file mode 100644 index 0000000..b2ef08d --- /dev/null +++ b/src/jarabe/journal/volumesmanager.py @@ -0,0 +1,315 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ + +import gobject +import dbus + +from sugar import profile +from sugar.datastore import datastore + +HAL_SERVICE_NAME = 'org.freedesktop.Hal' +HAL_MANAGER_PATH = '/org/freedesktop/Hal/Manager' +HAL_MANAGER_IFACE = 'org.freedesktop.Hal.Manager' +HAL_DEVICE_IFACE = 'org.freedesktop.Hal.Device' +HAL_VOLUME_IFACE = 'org.freedesktop.Hal.Device.Volume' + +MOUNT_OPTION_UID = 500 +MOUNT_OPTION_UMASK = 000 + +_volumes_manager = None + +class VolumesManager(gobject.GObject): + + __gtype_name__ = 'VolumesManager' + + __gsignals__ = { + 'volume-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])), + 'volume-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._volumes = [] + + # Internal flash is not in HAL + internal_fash_id = datastore.mounts()[0]['id'] + self._volumes.append(Volume(internal_fash_id, _('Journal'), + 'activity-journal', profile.get_color(), + None, False)) + + bus = dbus.SystemBus() + proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH) + self._hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE) + self._hal_manager.connect_to_signal('DeviceAdded', + self._hal_device_added_cb) + + for udi in self._hal_manager.FindDeviceByCapability('volume'): + if self._is_device_relevant(udi): + try: + self._add_hal_device(udi) + except Exception, e: + logging.error('Exception when mounting device %r: %r' % \ + (udi, e)) + + def get_volumes(self): + return self._volumes + + def _get_volume_by_udi(self, udi): + for volume in self._volumes: + if volume.udi == udi: + return volume + return None + + def _hal_device_added_cb(self, udi): + bus = dbus.SystemBus() + device_object = bus.get_object(HAL_SERVICE_NAME, udi) + device = dbus.Interface(device_object, HAL_DEVICE_IFACE) + if device.QueryCapability('volume'): + logging.debug('VolumesManager._hal_device_added_cb: %r', udi) + if self._is_device_relevant(udi): + self._add_hal_device(udi) + + def _is_device_relevant(self, udi): + bus = dbus.SystemBus() + device_object = bus.get_object(HAL_SERVICE_NAME, udi) + device = dbus.Interface(device_object, HAL_DEVICE_IFACE) + + # Ignore volumes without a filesystem. + if device.GetProperty('volume.fsusage') != 'filesystem': + return False + # Ignore root. + if device.GetProperty('volume.mount_point') == '/': + return False + + storage_udi = device.GetProperty('block.storage_device') + obj = bus.get_object(HAL_SERVICE_NAME, storage_udi) + storage_device = dbus.Interface(obj, HAL_DEVICE_IFACE) + + # Ignore non-removable storage. + if not storage_device.GetProperty('storage.hotpluggable'): + return False + + return True + + def _add_hal_device(self, udi): + logging.debug('VolumeToolbar._add_hal_device: %r' % udi) + + bus = dbus.SystemBus() + device_object = bus.get_object(HAL_SERVICE_NAME, udi) + device = dbus.Interface(device_object, HAL_DEVICE_IFACE) + + # listen to mount/unmount + device.connect_to_signal('PropertyModified', + lambda *args: self._hal_device_property_modified_cb(udi, *args)) + + bus.add_signal_receiver(self._hal_device_removed_cb, + 'DeviceRemoved', + HAL_MANAGER_IFACE, HAL_SERVICE_NAME, + HAL_MANAGER_PATH, arg0=udi) + + if device.GetProperty('volume.is_mounted'): + volume_id = self._mount_in_datastore(udi) + return + + label = device.GetProperty('volume.label') + fs_type = device.GetProperty('volume.fstype') + valid_options = device.GetProperty('volume.mount.valid_options') + options = [] + + if 'uid=' in valid_options: + options.append('uid=%i' % MOUNT_OPTION_UID) + + if 'umask=' in valid_options: + options.append('umask=%i' % MOUNT_OPTION_UMASK) + + if 'noatime' in valid_options: + options.append('noatime') + + if 'utf8' in valid_options: + options.append('utf8') + + if 'iocharset=' in valid_options: + options.append('iocharset=utf8') + + mount_point = label + if not mount_point: + mount_point = device.GetProperty('volume.uuid') + + volume = dbus.Interface(device_object, HAL_VOLUME_IFACE) + + # Try 100 times to get a mount point + mounted = False + i = 0 + while not mounted: + try: + if i > 0: + volume.Mount('%s_%d' % (mount_point, i), fs_type, options) + else: + volume.Mount(mount_point, fs_type, options) + mounted = True + except dbus.DBusException, e: + s = 'org.freedesktop.Hal.Device.Volume.MountPointNotAvailable' + if i < 100 and e.get_dbus_name() == s: + i += 1 + else: + raise + + def _hal_device_property_modified_cb(self, udi, count, changes): + if 'volume.is_mounted' in [change[0] for change in changes]: + logging.debug('VolumesManager._hal_device_property_modified: %r' % \ + (udi)) + bus = dbus.SystemBus() + #proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH) + #hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE) + # TODO: Why this doesn't work? + #if not hal_manager.DeviceExists(udi): + # return + + proxy = bus.get_object(HAL_SERVICE_NAME, udi) + device = dbus.Interface(proxy, HAL_DEVICE_IFACE) + try: + is_mounted = device.GetProperty('volume.is_mounted') + except dbus.DBusException, e: + logging.debug('e: %s' % e) + return + + if is_mounted: + if self._get_volume_by_udi(udi) is not None: + # device already mounted in the datastore + return + volume_id = self._mount_in_datastore(udi) + else: + self.unmount_from_datastore(udi) + return + + def _mount_in_datastore(self, udi): + logging.debug('VolumeToolbar._mount_in_datastore: %r' % udi) + + bus = dbus.SystemBus() + device_object = bus.get_object(HAL_SERVICE_NAME, udi) + device = dbus.Interface(device_object, HAL_DEVICE_IFACE) + + mount_point = device.GetProperty('volume.mount_point') + ds_mounts = datastore.mounts() + for ds_mount in ds_mounts: + if mount_point == ds_mount['uri']: + return ds_mount['id'] + + mount_id = datastore.mount('inplace:' + mount_point, + dict(title=mount_point)) + if not mount_id: + self._unmount_hal_device(udi) + raise RuntimeError('datastore.mount(%r, %r) failed.' % ( + 'inplace:' + mount_point, + dict(title=mount_point))) + + volume_name = device.GetProperty('volume.label') + if not volume_name: + volume_name = device.GetProperty('volume.uuid') + volume = Volume(mount_id, + volume_name, + self._get_icon_for_volume(udi), + profile.get_color(), + udi, + True) + self._volumes.append(volume) + self.emit('volume-added', volume) + + logging.debug('mounted volume %s' % mount_point) + + def _hal_device_removed_cb(self, udi): + logging.debug('VolumesManager._hal_device_removed_cb: %r', udi) + bus = dbus.SystemBus() + #proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH) + #hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE) + # TODO: Why this doesn't work? + #if not hal_manager.DeviceExists(udi): + # self._unmount_from_datastore(udi) + # self._remove_button(udi) + # return + + proxy = bus.get_object(HAL_SERVICE_NAME, udi) + device = dbus.Interface(proxy, HAL_DEVICE_IFACE) + try: + is_mounted = device.GetProperty('volume.is_mounted') + except dbus.DBusException, e: + logging.debug('e: %s' % e) + self.unmount_from_datastore(udi) + return + + if is_mounted: + self._unmount_from_datastore(udi) + self._unmount_hal_device(udi) + + def unmount_from_datastore(self, udi): + logging.debug('VolumesManager._unmount_from_datastore: %r', udi) + volume = self._get_volume_by_udi(udi) + if volume is not None: + datastore.unmount(volume.id) + + self._volumes.remove(volume) + self.emit('volume-removed', volume) + + def unmount_hal_device(self, udi): + logging.debug('VolumesManager._unmount_hal_device: %r', udi) + bus = dbus.SystemBus() + device_object = bus.get_object(HAL_SERVICE_NAME, udi) + volume = dbus.Interface(device_object, HAL_VOLUME_IFACE) + volume.Unmount([]) + + def _get_icon_for_volume(self, udi): + bus = dbus.SystemBus() + device_object = bus.get_object(HAL_SERVICE_NAME, udi) + device = dbus.Interface(device_object, HAL_DEVICE_IFACE) + + storage_udi = device.GetProperty('block.storage_device') + obj = bus.get_object(HAL_SERVICE_NAME, storage_udi) + storage_device = dbus.Interface(obj, HAL_DEVICE_IFACE) + + storage_drive_type = storage_device.GetProperty('storage.drive_type') + if storage_drive_type == 'sd_mmc': + return 'media-flash-sd-mmc' + else: + return 'media-flash-usb' + +class Volume(object): + def __init__(self, volume_id, name, icon_name, icon_color, udi, + can_unmount): + self.id = volume_id + self.name = name + self.icon_name = icon_name + self.icon_color = icon_color + self.udi = udi + self.can_unmount = can_unmount + + def unmount(self): + get_volumes_manager().unmount_from_datastore(self.udi) + get_volumes_manager().unmount_hal_device(self.udi) + +def get_volumes_manager(): + global _volumes_manager + if _volumes_manager is None: + _volumes_manager = VolumesManager() + return _volumes_manager + diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py new file mode 100644 index 0000000..f4f21db --- /dev/null +++ b/src/jarabe/journal/volumestoolbar.py @@ -0,0 +1,137 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ + +import gobject +import gtk + +from sugar.datastore import datastore +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.palette import Palette + +from jarabe.journal import volumesmanager + +class VolumesToolbar(gtk.Toolbar): + __gtype_name__ = 'VolumesToolbar' + + __gsignals__ = { + 'volume-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + self._volume_buttons = [] + self._volume_added_hid = None + self._volume_removed_hid = None + + self.connect('destroy', self.__destroy_cb) + + gobject.idle_add(self._set_up_volumes) + + def __destroy_cb(self, widget): + volumes_manager = volumesmanager.get_volumes_manager() + volumes_manager.disconnect(self._volume_added_hid) + volumes_manager.disconnect(self._volume_removed_hid) + + def _set_up_volumes(self): + volumes_manager = volumesmanager.get_volumes_manager() + self._volume_added_hid = \ + volumes_manager.connect('volume-added', self._volume_added_cb) + self._volume_removed_hid = \ + volumes_manager.connect('volume-removed', + self._volume_removed_cb) + + for volume in volumes_manager.get_volumes(): + self._add_button(volume) + + def _volume_added_cb(self, volumes_manager, volume): + self._add_button(volume) + + def _volume_removed_cb(self, volumes_manager, volume): + self._remove_button(volume) + + def _add_button(self, volume): + logging.debug('VolumeToolbar._add_button: %r' % volume.name) + + if self._volume_buttons: + group = self._volume_buttons[0] + else: + group = None + + palette = Palette(volume.name) + + button = VolumeButton(volume, group) + button.set_palette(palette) + button.connect('toggled', self._button_toggled_cb, volume) + if self._volume_buttons: + position = self.get_item_index(self._volume_buttons[-1]) + 1 + else: + position = 0 + self.insert(button, position) + button.show() + + self._volume_buttons.append(button) + + if volume.can_unmount: + menu_item = gtk.MenuItem(_('Unmount')) + menu_item.connect('activate', self._unmount_activated_cb, volume) + palette.menu.append(menu_item) + menu_item.show() + + if len(self.get_children()) > 1: + self.show() + + def _button_toggled_cb(self, button, volume): + if button.props.active: + self.emit('volume-changed', volume.id) + + def _unmount_activated_cb(self, menu_item, volume): + logging.debug('VolumesToolbar._unmount_activated_cb: %r', volume.udi) + volume.unmount() + + def _remove_button(self, volume): + for button in self.get_children(): + if button.volume.id == volume.id: + self._volume_buttons.remove(button) + self.remove(button) + self.get_children()[0].props.active = True + + if len(self.get_children()) < 2: + self.hide() + return + +class VolumeButton(RadioToolButton): + def __init__(self, volume, group): + RadioToolButton.__init__(self) + self.props.named_icon = volume.icon_name + self.props.xo_color = volume.icon_color + self.props.group = group + + self.volume = volume + self.drag_dest_set(gtk.DEST_DEFAULT_ALL, + [('journal-object-id', 0, 0)], + gtk.gdk.ACTION_COPY) + self.connect('drag-data-received', self._drag_data_received_cb) + + def _drag_data_received_cb(self, widget, drag_context, x, y, selection_data, + info, timestamp): + jobject = datastore.get(selection_data.data) + datastore.copy(jobject, self.volume.id) + diff --git a/src/jarabe/logsmanager.py b/src/jarabe/logsmanager.py new file mode 100644 index 0000000..9360211 --- /dev/null +++ b/src/jarabe/logsmanager.py @@ -0,0 +1,55 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import time + +from sugar import env + +_MAX_BACKUP_DIRS = 3 + +def setup(): + """Clean up the log directory, moving old logs into a numbered backup + directory. We only keep `_MAX_BACKUP_DIRS` of these backup directories + around; the rest are removed.""" + logs_dir = env.get_logs_path() + if not os.path.isdir(logs_dir): + os.makedirs(logs_dir) + + backup_logs = [] + backup_dirs = [] + for f in os.listdir(logs_dir): + path = os.path.join(logs_dir, f) + if os.path.isfile(path): + backup_logs.append(f) + elif os.path.isdir(path): + backup_dirs.append(path) + + if len(backup_dirs) > _MAX_BACKUP_DIRS: + backup_dirs.sort() + root = backup_dirs[0] + for f in os.listdir(root): + os.remove(os.path.join(root, f)) + os.rmdir(root) + + if len(backup_logs) > 0: + name = str(int(time.time())) + backup_dir = os.path.join(logs_dir, name) + os.mkdir(backup_dir) + for log in backup_logs: + source_path = os.path.join(logs_dir, log) + dest_path = os.path.join(backup_dir, log) + os.rename(source_path, dest_path) diff --git a/src/jarabe/main.py b/src/jarabe/main.py new file mode 100644 index 0000000..b8fab9b --- /dev/null +++ b/src/jarabe/main.py @@ -0,0 +1,158 @@ +# Copyright (C) 2006, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import gettext + +import pygtk +pygtk.require('2.0') +import gtk +import gobject +import dbus.glib + +gtk.gdk.threads_init() +dbus.glib.threads_init() + +from sugar import logger +from sugar.profile import get_profile + +import view.Shell +from shellservice import ShellService +from jarabe.hardware import hardwaremanager +from jarabe.intro.window import IntroWindow +from jarabe.intro.window import create_profile +from session import get_session_manager +import logsmanager +import config + +def _start_matchbox(): + cmd = ['matchbox-window-manager'] + + cmd.extend(['-use_titlebar', 'no']) + cmd.extend(['-theme', 'sugar']) + cmd.extend(['-kbdconfig', os.path.join(config.data_path, 'kbdconfig')]) + + gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) + +def _setup_translations(): + locale_path = os.path.join(config.prefix, 'share', 'locale') + domain = 'sugar' + + gettext.bindtextdomain(domain, locale_path) + gettext.textdomain(domain) + +def check_cm(bus_name): + try: + bus = dbus.SessionBus() + bus_object = bus.get_object('org.freedesktop.DBus', + '/org/freedesktop/DBus') + name_ = bus_object.GetNameOwner(bus_name, + dbus_interface='org.freedesktop.DBus') + except dbus.DBusException: + return False + return True + +def _shell_started_cb(): + # Unfreeze the display + hw_manager = hardwaremanager.get_manager() + hw_manager.set_dcon_freeze(0) + +def _software_update_cb(): + '''Ask the homeview to display an alert about available software updates + ''' + shell = view.Shell.get_instance() + home_box = shell.home_window.get_home_box() + home_box.show_software_updates_alert() + +def main(): + gobject.idle_add(_shell_started_cb) + + try: + logsmanager.setup() + except Exception, e: + # logs setup is not critical; it should not prevent sugar from + # starting if (for example) the disk is full or read-only. + print 'Log setup failed: %s' % e + + logger.start('shell') + + _start_matchbox() + _setup_translations() + + hw_manager = hardwaremanager.get_manager() + hw_manager.startup() + + icons_path = os.path.join(config.data_path, 'icons') + gtk.icon_theme_get_default().append_search_path(icons_path) + + # Do initial setup if needed + if not get_profile().is_valid(): + if 'SUGAR_PROFILE_NAME' in os.environ: + create_profile(os.environ['SUGAR_PROFILE_NAME']) + else: + win = IntroWindow() + win.show_all() + gtk.main() + + # set timezone + if get_profile().timezone is not None: + os.environ['TZ'] = get_profile().timezone + + if os.environ.has_key("SUGAR_TP_DEBUG"): + # Allow the user time to start up telepathy connection managers + # using the Sugar DBus bus address + import time + from telepathy.client import ManagerRegistry + + registry = ManagerRegistry() + registry.LoadManagers() + + debug_flags = os.environ["SUGAR_TP_DEBUG"].split(',') + for cm_name in debug_flags: + if cm_name not in ["gabble", "salut"]: + continue + + try: + cm = registry.services[cm_name] + except KeyError: + print RuntimeError("%s connection manager not found!" % cm_name) + + while not check_cm(cm['busname']): + print "Waiting for %s on: DBUS_SESSION_BUS_ADDRESS=%s" % \ + (cm_name, os.environ["DBUS_SESSION_BUS_ADDRESS"]) + try: + time.sleep(5) + except KeyboardInterrupt: + print "Got Ctrl+C, continuing..." + break + + # TODO: move initializations from the Shell constructor to a start() method + view.Shell.get_instance() + ShellService() + + session_manager = get_session_manager() + session_manager.start() + + # dlo trac #7495: open 'software update' control panel after an upgrade + # to update activities. + update_trigger_file = os.path.expanduser('~/.sugar-update') + if os.path.isfile(update_trigger_file): + gobject.idle_add(_software_update_cb) + + try: + gtk.main() + except KeyboardInterrupt: + print 'Ctrl+C pressed, exiting...' diff --git a/src/jarabe/model/BuddyModel.py b/src/jarabe/model/BuddyModel.py new file mode 100644 index 0000000..b51b808 --- /dev/null +++ b/src/jarabe/model/BuddyModel.py @@ -0,0 +1,171 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.presence import presenceservice +from sugar.graphics.xocolor import XoColor +import gobject + +_NOT_PRESENT_COLOR = "#d5d5d5,#FFFFFF" + +class BuddyModel(gobject.GObject): + __gsignals__ = { + 'appeared': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'disappeared': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'nick-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'color-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'icon-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'current-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self, key=None, buddy=None, nick=None): + if (key and buddy) or (not key and not buddy): + raise RuntimeError("Must specify only _one_ of key or buddy.") + + gobject.GObject.__init__(self) + + self._color = None + self._ba_handler = None + self._pc_handler = None + self._dis_handler = None + self._bic_handler = None + self._cac_handler = None + + self._pservice = presenceservice.get_instance() + + self._buddy = None + + if not buddy: + self._key = key + # connect to the PS's buddy-appeared signal and + # wait for the buddy to appear + self._ba_handler = self._pservice.connect('buddy-appeared', + self._buddy_appeared_cb) + # Set color to 'inactive'/'disconnected' + self._set_color_from_string(_NOT_PRESENT_COLOR) + self._nick = nick + + self._pservice.get_buddies_async(reply_handler=self._get_buddies_cb) + else: + self._update_buddy(buddy) + + def _get_buddies_cb(self, buddy_list): + buddy = None + for iter_buddy in buddy_list: + if iter_buddy.props.key == self._key: + buddy = iter_buddy + break + + if buddy: + if self._ba_handler: + # Once we have the buddy, we no longer need to + # monitor buddy-appeared events + self._pservice.disconnect(self._ba_handler) + self._ba_handler = None + + self._update_buddy(buddy) + + def _set_color_from_string(self, color_string): + self._color = XoColor(color_string) + + def get_key(self): + return self._key + + def get_nick(self): + return self._nick + + def get_color(self): + return self._color + + def get_buddy(self): + return self._buddy + + def is_owner(self): + if not self._buddy: + return False + return self._buddy.props.owner + + def is_present(self): + if self._buddy: + return True + return False + + def get_current_activity(self): + if self._buddy: + return self._buddy.props.current_activity + return None + + def _update_buddy(self, buddy): + if not buddy: + raise ValueError("Buddy cannot be None.") + + self._buddy = buddy + self._key = self._buddy.props.key + self._nick = self._buddy.props.nick + self._set_color_from_string(self._buddy.props.color) + + self._pc_handler = self._buddy.connect('property-changed', + self._buddy_property_changed_cb) + self._bic_handler = self._buddy.connect('icon-changed', + self._buddy_icon_changed_cb) + + def _buddy_appeared_cb(self, pservice, buddy): + if self._buddy or buddy.props.key != self._key: + return + + if self._ba_handler: + # Once we have the buddy, we no longer need to + # monitor buddy-appeared events + self._pservice.disconnect(self._ba_handler) + self._ba_handler = None + + self._update_buddy(buddy) + self.emit('appeared') + + def _buddy_property_changed_cb(self, buddy, keys): + if not self._buddy: + return + if 'color' in keys: + self._set_color_from_string(self._buddy.props.color) + self.emit('color-changed', self.get_color()) + if 'current-activity' in keys: + self.emit('current-activity-changed', buddy.props.current_activity) + if 'nick' in keys: + self._nick = self._buddy.props.nick + self.emit('nick-changed', self.get_nick()) + + def _buddy_disappeared_cb(self, buddy): + if buddy != self._buddy: + return + self._buddy.disconnect(self._pc_handler) + self._buddy.disconnect(self._dis_handler) + self._buddy.disconnect(self._bic_handler) + self._buddy.disconnect(self._cac_handler) + self._set_color_from_string(_NOT_PRESENT_COLOR) + self.emit('disappeared') + self._buddy = None + + def _buddy_icon_changed_cb(self, buddy): + self.emit('icon-changed') diff --git a/src/jarabe/model/Friends.py b/src/jarabe/model/Friends.py new file mode 100644 index 0000000..fc6ff65 --- /dev/null +++ b/src/jarabe/model/Friends.py @@ -0,0 +1,114 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import dbus +import os +from ConfigParser import ConfigParser + +import gobject + +from jarabe.model.BuddyModel import BuddyModel +from sugar import env +import logging + +class Friends(gobject.GObject): + __gsignals__ = { + 'friend-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object])), + 'friend-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([str])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._friends = {} + self._path = os.path.join(env.get_profile_path(), 'friends') + + self.load() + + def has_buddy(self, buddy): + return self._friends.has_key(buddy.get_key()) + + def add_friend(self, buddy_info): + self._friends[buddy_info.get_key()] = buddy_info + self.emit('friend-added', buddy_info) + + def make_friend(self, buddy): + if not self.has_buddy(buddy): + self.add_friend(buddy) + self.save() + + def remove(self, buddy_info): + del self._friends[buddy_info.get_key()] + self.save() + self.emit('friend-removed', buddy_info.get_key()) + + def __iter__(self): + return self._friends.values().__iter__() + + def load(self): + cp = ConfigParser() + + try: + success = cp.read([self._path]) + if success: + for key in cp.sections(): + # HACK: don't screw up on old friends files + if len(key) < 20: + continue + buddy = BuddyModel(key=key, nick=cp.get(key, 'nick')) + self.add_friend(buddy) + except Exception, exc: + logging.error("Error parsing friends file: %s" % exc) + + def save(self): + cp = ConfigParser() + + for friend in self: + section = friend.get_key() + cp.add_section(section) + cp.set(section, 'nick', friend.get_nick()) + cp.set(section, 'color', friend.get_color().to_string()) + + fileobject = open(self._path, 'w') + cp.write(fileobject) + fileobject.close() + + self._sync_friends() + + def _sync_friends(self): + # XXX: temporary hack + # remove this when the shell service has a D-Bus API for buddies + + def friends_synced(): + pass + + def friends_synced_error(e): + logging.error("Error asking presence service to sync friends: %s" + % e) + + keys = [] + for friend in self: + keys.append(friend.get_key()) + + bus = dbus.SessionBus() + ps = bus.get_object('org.laptop.Sugar.Presence', + '/org/laptop/Sugar/Presence') + psi = dbus.Interface(ps, 'org.laptop.Sugar.Presence') + psi.SyncFriends(keys, + reply_handler=friends_synced, + error_handler=friends_synced_error) diff --git a/src/jarabe/model/Invites.py b/src/jarabe/model/Invites.py new file mode 100644 index 0000000..8bcffd2 --- /dev/null +++ b/src/jarabe/model/Invites.py @@ -0,0 +1,123 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +from sugar.presence import presenceservice + + +class BaseInvite: + """Invitation to shared activity or private 1-1 Telepathy channel""" + def __init__(self, bundle_id): + """init for BaseInvite. + + bundle_id: string, e.g. 'org.laptop.Chat' + """ + self._bundle_id = bundle_id + + def get_bundle_id(self): + return self._bundle_id + + +class ActivityInvite(BaseInvite): + """Invitation to a shared activity.""" + def __init__(self, bundle_id, activity_id): + BaseInvite.__init__(self, bundle_id) + self._activity_id = activity_id + + def get_activity_id(self): + return self._activity_id + + +class PrivateInvite(BaseInvite): + """Invitation to a private 1-1 Telepathy channel. + + This includes text chat or streaming media. + """ + def __init__(self, bundle_id, private_channel): + """init for PrivateInvite. + + bundle_id: string, e.g. 'org.laptop.Chat' + private_channel: string containing simplejson dump of Telepathy + bus, connection and channel + """ + BaseInvite.__init__(self, bundle_id) + self._private_channel = private_channel + + def get_private_channel(self): + """Telepathy channel info from private invitation""" + return self._private_channel + + +class Invites(gobject.GObject): + __gsignals__ = { + 'invite-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object])), + 'invite-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._dict = {} + + ps = presenceservice.get_instance() + owner = ps.get_owner() + owner.connect('joined-activity', self._owner_joined_cb) + + def add_invite(self, bundle_id, activity_id): + if activity_id in self._dict: + # there is no point to add more than one time + # an invite for the same activity + return + + invite = ActivityInvite(bundle_id, activity_id) + self._dict[activity_id] = invite + self.emit('invite-added', invite) + + def add_private_invite(self, private_channel, bundle_id): + if private_channel in self._dict: + # there is no point to add more than one invite for the + # same incoming connection + return + + invite = PrivateInvite(bundle_id, private_channel) + self._dict[private_channel] = invite + self.emit('invite-added', invite) + + def remove_invite(self, invite): + del self._dict[invite.get_activity_id()] + self.emit('invite-removed', invite) + + def remove_private_invite(self, invite): + del self._dict[invite.get_private_channel()] + self.emit('invite-removed', invite) + + def remove_activity(self, activity_id): + invite = self._dict.get(activity_id) + if invite is not None: + self.remove_invite(invite) + + def remove_private_channel(self, private_channel): + invite = self._dict.get(private_channel) + if invite is not None: + self.remove_private_invite(invite) + + def _owner_joined_cb(self, owner, activity): + self.remove_activity(activity.props.id) + + def __iter__(self): + return self._dict.values().__iter__() diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am new file mode 100644 index 0000000..2c4150b --- /dev/null +++ b/src/jarabe/model/Makefile.am @@ -0,0 +1,16 @@ +SUBDIRS = devices + +sugardir = $(pythondir)/jarabe/model +sugar_PYTHON = \ + __init__.py \ + accesspointmodel.py \ + BuddyModel.py \ + clipboard.py \ + clipboardobject.py \ + Friends.py \ + Invites.py \ + Owner.py \ + MeshModel.py \ + shellmodel.py \ + homeactivity.py \ + homemodel.py diff --git a/src/jarabe/model/MeshModel.py b/src/jarabe/model/MeshModel.py new file mode 100644 index 0000000..8e16c21 --- /dev/null +++ b/src/jarabe/model/MeshModel.py @@ -0,0 +1,237 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject + +from sugar.graphics.xocolor import XoColor +from sugar.presence import presenceservice +from sugar import activity + +from jarabe.model.BuddyModel import BuddyModel +from jarabe.model.accesspointmodel import AccessPointModel +from jarabe.hardware import hardwaremanager +from jarabe.hardware import nmclient + +class ActivityModel: + def __init__(self, act, bundle): + self.activity = act + self.bundle = bundle + + def get_id(self): + return self.activity.props.id + + def get_icon_name(self): + return self.bundle.icon + + def get_color(self): + return XoColor(self.activity.props.color) + + def get_bundle_id(self): + return self.bundle.bundle_id + +class MeshModel(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'buddy-moved': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'access-point-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'access-point-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'mesh-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'mesh-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._activities = {} + self._buddies = {} + self._access_points = {} + self._mesh = None + + self._pservice = presenceservice.get_instance() + self._pservice.connect("activity-appeared", + self._activity_appeared_cb) + self._pservice.connect('activity-disappeared', + self._activity_disappeared_cb) + self._pservice.connect("buddy-appeared", + self._buddy_appeared_cb) + self._pservice.connect("buddy-disappeared", + self._buddy_disappeared_cb) + + # Add any buddies the PS knows about already + self._pservice.get_buddies_async(reply_handler=self._get_buddies_cb) + + self._pservice.get_activities_async( + reply_handler=self._get_activities_cb) + + network_manager = hardwaremanager.get_network_manager() + if network_manager: + for nm_device in network_manager.get_devices(): + self._add_network_device(nm_device) + network_manager.connect('device-added', + self._nm_device_added_cb) + network_manager.connect('device-removed', + self._nm_device_removed_cb) + + def _get_buddies_cb(self, buddy_list): + for buddy in buddy_list: + self._buddy_appeared_cb(self._pservice, buddy) + + def _get_activities_cb(self, activity_list): + for act in activity_list: + self._check_activity(act) + + def _nm_device_added_cb(self, manager, nm_device): + self._add_network_device(nm_device) + + def _nm_device_removed_cb(self, manager, nm_device): + self._remove_network_device(nm_device) + + def _nm_network_appeared_cb(self, nm_device, nm_network): + self._add_access_point(nm_device, nm_network) + + def _nm_network_disappeared_cb(self, nm_device, nm_network): + if self._access_points.has_key(nm_network.get_op()): + ap = self._access_points[nm_network.get_op()] + self._remove_access_point(ap) + + def _add_network_device(self, nm_device): + dtype = nm_device.get_type() + if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS: + for nm_network in nm_device.get_networks(): + self._add_access_point(nm_device, nm_network) + + nm_device.connect('network-appeared', + self._nm_network_appeared_cb) + nm_device.connect('network-disappeared', + self._nm_network_disappeared_cb) + elif dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC: + self._mesh = nm_device + self.emit('mesh-added', self._mesh) + + def _remove_network_device(self, nm_device): + if nm_device == self._mesh: + self._mesh = None + self.emit('mesh-removed') + elif nm_device.get_type() == nmclient.DEVICE_TYPE_802_11_WIRELESS: + aplist = self._access_points.values() + for ap in aplist: + if ap.get_nm_device() == nm_device: + self._remove_access_point(ap) + + def _add_access_point(self, nm_device, nm_network): + model = AccessPointModel(nm_device, nm_network) + self._access_points[model.get_id()] = model + self.emit('access-point-added', model) + + def _remove_access_point(self, ap): + if not self._access_points.has_key(ap.get_id()): + return + self.emit('access-point-removed', ap) + del self._access_points[ap.get_id()] + + def get_mesh(self): + return self._mesh + + def get_access_points(self): + return self._access_points.values() + + def get_activities(self): + return self._activities.values() + + def get_buddies(self): + return self._buddies.values() + + def _buddy_activity_changed_cb(self, model, cur_activity): + if not self._buddies.has_key(model.get_buddy().object_path()): + return + if cur_activity and self._activities.has_key(cur_activity.props.id): + activity_model = self._activities[cur_activity.props.id] + self.emit('buddy-moved', model, activity_model) + else: + self.emit('buddy-moved', model, None) + + def _buddy_appeared_cb(self, pservice, buddy): + if self._buddies.has_key(buddy.object_path()): + return + + model = BuddyModel(buddy=buddy) + model.connect('current-activity-changed', + self._buddy_activity_changed_cb) + self._buddies[buddy.object_path()] = model + self.emit('buddy-added', model) + + cur_activity = buddy.props.current_activity + if cur_activity: + self._buddy_activity_changed_cb(model, cur_activity) + + def _buddy_disappeared_cb(self, pservice, buddy): + if not self._buddies.has_key(buddy.object_path()): + return + self.emit('buddy-removed', self._buddies[buddy.object_path()]) + del self._buddies[buddy.object_path()] + + def _activity_appeared_cb(self, pservice, act): + self._check_activity(act) + + def _check_activity(self, presence_activity): + registry = activity.get_registry() + bundle = registry.get_activity(presence_activity.props.type) + if not bundle: + return + if self.has_activity(presence_activity.props.id): + return + self.add_activity(bundle, presence_activity) + + def has_activity(self, activity_id): + return self._activities.has_key(activity_id) + + def get_activity(self, activity_id): + if self.has_activity(activity_id): + return self._activities[activity_id] + else: + return None + + def add_activity(self, bundle, act): + model = ActivityModel(act, bundle) + self._activities[model.get_id()] = model + self.emit('activity-added', model) + + for buddy in self._pservice.get_buddies(): + cur_activity = buddy.props.current_activity + object_path = buddy.object_path() + if cur_activity == activity and object_path in self._buddies: + buddy_model = self._buddies[object_path] + self.emit('buddy-moved', buddy_model, model) + + def _activity_disappeared_cb(self, pservice, act): + if self._activities.has_key(act.props.id): + activity_model = self._activities[act.props.id] + self.emit('activity-removed', activity_model) + del self._activities[act.props.id] diff --git a/src/jarabe/model/Owner.py b/src/jarabe/model/Owner.py new file mode 100644 index 0000000..91be127 --- /dev/null +++ b/src/jarabe/model/Owner.py @@ -0,0 +1,104 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import os +import simplejson + +from telepathy.interfaces import CHANNEL_TYPE_TEXT + +from sugar import env +from sugar import profile +from sugar.presence import presenceservice +from sugar import util +from jarabe.model.Invites import Invites + +class ShellOwner(gobject.GObject): + """Class representing the owner of this machine/instance. This class + runs in the shell and serves up the buddy icon and other stuff. It's the + server portion of the Owner, paired with the client portion in Buddy.py. + """ + __gtype_name__ = "ShellOwner" + + __gsignals__ = { + 'nick-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_STRING])), + 'color-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'icon-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._nick = profile.get_nick_name() + + self._icon = None + self._icon_hash = "" + icon = os.path.join(env.get_profile_path(), "buddy-icon.jpg") + if not os.path.exists(icon): + raise RuntimeError("missing buddy icon") + + fd = open(icon, "r") + self._icon = fd.read() + fd.close() + if not self._icon: + raise RuntimeError("invalid buddy icon") + + # Get the icon's hash + import md5 + digest = md5.new(self._icon).digest() + self._icon_hash = util.printable_hash(digest) + + self._pservice = presenceservice.get_instance() + self._pservice.connect('activity-invitation', + self._activity_invitation_cb) + self._pservice.connect('private-invitation', + self._private_invitation_cb) + self._pservice.connect('activity-disappeared', + self._activity_disappeared_cb) + + self._invites = Invites() + + def get_invites(self): + return self._invites + + def get_nick(self): + return self._nick + + def _activity_invitation_cb(self, pservice, activity, buddy, message): + self._invites.add_invite(activity.props.type, + activity.props.id) + + def _private_invitation_cb(self, pservice, bus_name, connection, + channel, channel_type): + """Handle a private-invitation from Presence Service. + + This is a connection by a non-Sugar XMPP client, so + launch Chat or VideoChat with the Telepathy connection and + channel. + """ + if channel_type == CHANNEL_TYPE_TEXT: + bundle_id = 'org.laptop.Chat' + else: + bundle_id = 'org.laptop.VideoChat' + tp_channel = simplejson.dumps([bus_name, connection, channel]) + self._invites.add_private_invite(tp_channel, bundle_id) + + def _activity_disappeared_cb(self, pservice, activity): + self._invites.remove_activity(activity.props.id) diff --git a/src/jarabe/model/__init__.py b/src/jarabe/model/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/model/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/model/accesspointmodel.py b/src/jarabe/model/accesspointmodel.py new file mode 100644 index 0000000..a4760bc --- /dev/null +++ b/src/jarabe/model/accesspointmodel.py @@ -0,0 +1,80 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject + +from jarabe.hardware import nmclient + +STATE_CONNECTING = 0 +STATE_CONNECTED = 1 +STATE_NOTCONNECTED = 2 + +_nm_state_to_state = { + nmclient.NETWORK_STATE_CONNECTED : STATE_CONNECTED, + nmclient.NETWORK_STATE_CONNECTING : STATE_CONNECTING, + nmclient.NETWORK_STATE_NOTCONNECTED : STATE_NOTCONNECTED +} + +class AccessPointModel(gobject.GObject): + __gproperties__ = { + 'name' : (str, None, None, None, + gobject.PARAM_READABLE), + 'strength' : (int, None, None, 0, 100, 0, + gobject.PARAM_READABLE), + 'state' : (int, None, None, STATE_CONNECTING, + STATE_NOTCONNECTED, 0, gobject.PARAM_READABLE), + 'capabilities' : (int, None, None, 0, 0x7FFFFFFF, 0, + gobject.PARAM_READABLE), + 'mode' : (int, None, None, 0, 6, 0, gobject.PARAM_READABLE) + } + + def __init__(self, nm_device, nm_network): + gobject.GObject.__init__(self) + self._nm_network = nm_network + self._nm_device = nm_device + + self._nm_network.connect('strength-changed', + self._strength_changed_cb) + self._nm_network.connect('state-changed', + self._state_changed_cb) + + def _strength_changed_cb(self, nm_network): + self.notify('strength') + + def _state_changed_cb(self, nm_network): + self.notify('state') + + def get_id(self): + return self._nm_network.get_op() + + def get_nm_device(self): + return self._nm_device + + def get_nm_network(self): + return self._nm_network + + def do_get_property(self, pspec): + if pspec.name == 'strength': + return self._nm_network.get_strength() + elif pspec.name == 'name': + return self._nm_network.get_ssid() + elif pspec.name == 'state': + nm_state = self._nm_network.get_state() + return _nm_state_to_state[nm_state] + elif pspec.name == 'capabilities': + return self._nm_network.get_caps() + elif pspec.name == 'mode': + return self._nm_network.get_mode() diff --git a/src/jarabe/model/clipboard.py b/src/jarabe/model/clipboard.py new file mode 100644 index 0000000..b79867e --- /dev/null +++ b/src/jarabe/model/clipboard.py @@ -0,0 +1,149 @@ +# Copyright (C) 2006, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import os +import shutil +import urlparse +import tempfile + +import gobject + +from sugar import mime + +from jarabe.model.clipboardobject import ClipboardObject, Format + +class Clipboard(gobject.GObject): + + __gsignals__ = { + 'object-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'object-deleted': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([int])), + 'object-state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._objects = {} + self._next_id = 0 + + def _get_next_object_id(self): + self._next_id += 1 + return self._next_id + + def add_object(self, name): + logging.debug('Clipboard.add_object') + object_id = self._get_next_object_id() + self._objects[object_id] = ClipboardObject(object_id, name) + self.emit('object-added', self._objects[object_id]) + return object_id + + def add_object_format(self, object_id, format_type, data, on_disk): + logging.debug('Clipboard.add_object_format') + cb_object = self._objects[object_id] + + if format_type == 'XdndDirectSave0': + format = Format('text/uri-list', data + '\r\n', on_disk) + format.owns_disk_data = True + cb_object.add_format(format) + elif on_disk and cb_object.get_percent() == 100: + new_uri = self._copy_file(data) + cb_object.add_format(Format(format_type, new_uri, on_disk)) + logging.debug('Added format of type ' + format_type + + ' with path at ' + new_uri) + else: + cb_object.add_format(Format(format_type, data, on_disk)) + logging.debug('Added in-memory format of type ' + format_type + '.') + + self.emit('object-state-changed', cb_object) + + def delete_object(self, object_id): + cb_object = self._objects.pop(object_id) + cb_object.destroy() + self.emit('object-deleted', object_id) + logging.debug('Deleted object with object_id %r' % object_id) + + def set_object_percent(self, object_id, percent): + cb_object = self._objects[object_id] + if percent < 0 or percent > 100: + raise ValueError("invalid percentage") + if cb_object.get_percent() > percent: + raise ValueError("invalid percentage; less than current percent") + if cb_object.get_percent() == percent: + # ignore setting same percentage + return + + cb_object.set_percent(percent) + + if percent == 100: + self._process_object(cb_object) + + self.emit('object-state-changed', cb_object) + + def _process_object(self, cb_object): + formats = cb_object.get_formats() + for format_name, format in formats.iteritems(): + if format.is_on_disk() and not format.owns_disk_data: + new_uri = self._copy_file(format.get_data()) + format.set_data(new_uri) + + # Add a text/plain format to objects that are text but lack it + if 'text/plain' not in formats.keys(): + if 'UTF8_STRING' in formats.keys(): + self.add_object_format( + cb_object.get_id(), 'text/plain', + data=formats['UTF8_STRING'].get_data(), on_disk=False) + elif 'text/unicode' in formats.keys(): + self.add_object_format( + cb_object.get_id(), 'text/plain', + data=formats['UTF8_STRING'].get_data(), on_disk=False) + + def get_object(self, object_id): + logging.debug('Clipboard.get_object') + return self._objects[object_id] + + def get_object_data(self, object_id, format_type): + logging.debug('Clipboard.get_object_data') + cb_object = self._objects[object_id] + format = cb_object.get_formats()[format_type] + return format + + def _copy_file(self, original_uri): + uri = urlparse.urlparse(original_uri) + path_, file_name = os.path.split(uri.path) + + root, ext = os.path.splitext(file_name) + if not ext or ext == '.': + mime_type = mime.get_for_file(uri.path) + ext = '.' + mime.get_primary_extension(mime_type) + + f_, new_file_path = tempfile.mkstemp(ext, root) + del f_ + shutil.copyfile(uri.path, new_file_path) + os.chmod(new_file_path, 0644) + + return 'file://' + new_file_path + +_instance = None + +def get_instance(): + global _instance + if not _instance: + _instance = Clipboard() + return _instance diff --git a/src/jarabe/model/clipboardobject.py b/src/jarabe/model/clipboardobject.py new file mode 100644 index 0000000..a4cd388 --- /dev/null +++ b/src/jarabe/model/clipboardobject.py @@ -0,0 +1,117 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import urlparse + +from sugar import mime +from sugar.bundle.activitybundle import ActivityBundle + +class ClipboardObject(object): + + def __init__(self, object_path, name): + self._id = object_path + self._name = name + self._percent = 0 + self._formats = {} + + def destroy(self): + for format in self._formats.itervalues(): + format.destroy() + + def get_id(self): + return self._id + + def get_name(self): + name = self._name + if not name: + name = mime.get_mime_description(self.get_mime_type()) + if not name: + name = '' + return name + + def get_icon(self): + return mime.get_mime_icon(self.get_mime_type()) + + def get_preview(self): + # TODO: should previews really be here? + #return self._get_type_info().get_preview() + return '' + + def is_bundle(self): + # A bundle will have only one format. + if not self._formats: + return False + else: + return self._formats.keys()[0] in [ActivityBundle.MIME_TYPE, + ActivityBundle.DEPRECATED_MIME_TYPE] + + def get_percent(self): + return self._percent + + def set_percent(self, percent): + self._percent = percent + + def add_format(self, format): + self._formats[format.get_type()] = format + + def get_formats(self): + return self._formats + + def get_mime_type(self): + if not self._formats: + return '' + + format = mime.choose_most_significant(self._formats.keys()) + if format == 'text/uri-list': + data = self._formats['text/uri-list'].get_data() + uri = urlparse.urlparse(mime.split_uri_list(data)[0], 'file') + if uri.scheme == 'file': + if os.path.exists(uri.path): + format = mime.get_for_file(uri.path) + else: + format = mime.get_from_file_name(uri.path) + logging.debug('Choosed %r!' % format) + + return format + +class Format(object): + + def __init__(self, mime_type, data, on_disk): + self.owns_disk_data = False + + self._type = mime_type + self._data = data + self._on_disk = on_disk + + def destroy(self): + if self._on_disk: + uri = urlparse.urlparse(self._data) + if os.path.isfile(uri.path): + os.remove(uri.path) + + def get_type(self): + return self._type + + def get_data(self): + return self._data + + def set_data(self, data): + self._data = data + + def is_on_disk(self): + return self._on_disk diff --git a/src/jarabe/model/devices/Makefile.am b/src/jarabe/model/devices/Makefile.am new file mode 100644 index 0000000..564d609 --- /dev/null +++ b/src/jarabe/model/devices/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = network + +sugardir = $(pythondir)/jarabe/model/devices +sugar_PYTHON = \ + __init__.py \ + battery.py \ + device.py \ + devicesmodel.py \ + speaker.py + diff --git a/src/jarabe/model/devices/__init__.py b/src/jarabe/model/devices/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/model/devices/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/model/devices/battery.py b/src/jarabe/model/devices/battery.py new file mode 100644 index 0000000..d041907 --- /dev/null +++ b/src/jarabe/model/devices/battery.py @@ -0,0 +1,97 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gobject +import dbus + +from jarabe.model.devices import device + +_LEVEL_PROP = 'battery.charge_level.percentage' +_CHARGING_PROP = 'battery.rechargeable.is_charging' +_DISCHARGING_PROP = 'battery.rechargeable.is_discharging' + +class Device(device.Device): + __gproperties__ = { + 'level' : (int, None, None, 0, 100, 0, + gobject.PARAM_READABLE), + 'charging' : (bool, None, None, False, + gobject.PARAM_READABLE), + 'discharging' : (bool, None, None, False, + gobject.PARAM_READABLE) + } + + def __init__(self, udi): + device.Device.__init__(self, udi) + + bus = dbus.Bus(dbus.Bus.TYPE_SYSTEM) + proxy = bus.get_object('org.freedesktop.Hal', udi, + follow_name_owner_changes=True) + self._battery = dbus.Interface(proxy, 'org.freedesktop.Hal.Device') + bus.add_signal_receiver(self._battery_changed, + 'PropertyModified', + 'org.freedesktop.Hal.Device', + 'org.freedesktop.Hal', + udi) + + self._level = self._get_level() + self._charging = self._get_charging() + self._discharging = self._get_discharging() + + def _get_level(self): + try: + return self._battery.GetProperty(_LEVEL_PROP) + except dbus.DBusException: + logging.error('Cannot access %s' % _LEVEL_PROP) + return 0 + + def _get_charging(self): + try: + return self._battery.GetProperty(_CHARGING_PROP) + except dbus.DBusException: + logging.error('Cannot access %s' % _CHARGING_PROP) + return False + + def _get_discharging(self): + try: + return self._battery.GetProperty(_DISCHARGING_PROP) + except dbus.DBusException: + logging.error('Cannot access %s' % _DISCHARGING_PROP) + return False + + def do_get_property(self, pspec): + if pspec.name == 'level': + return self._level + if pspec.name == 'charging': + return self._charging + if pspec.name == 'discharging': + return self._discharging + + def get_type(self): + return 'battery' + + def _battery_changed(self, num_changes, changes_list): + for change in changes_list: + if change[0] == _LEVEL_PROP: + self._level = self._get_level() + self.notify('level') + elif change[0] == _CHARGING_PROP: + self._charging = self._get_charging() + self.notify('charging') + elif change[0] == _DISCHARGING_PROP: + self._discharging = self._get_discharging() + self.notify('discharging') diff --git a/src/jarabe/model/devices/device.py b/src/jarabe/model/devices/device.py new file mode 100644 index 0000000..8d62415 --- /dev/null +++ b/src/jarabe/model/devices/device.py @@ -0,0 +1,45 @@ +# +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +from jarabe.hardware import nmclient + +from sugar import util + +STATE_ACTIVATING = 0 +STATE_ACTIVATED = 1 +STATE_INACTIVE = 2 + +nm_state_to_state = { + nmclient.DEVICE_STATE_ACTIVATING : STATE_ACTIVATING, + nmclient.DEVICE_STATE_ACTIVATED : STATE_ACTIVATED, + nmclient.DEVICE_STATE_INACTIVE : STATE_INACTIVE +} + +class Device(gobject.GObject): + def __init__(self, device_id=None): + gobject.GObject.__init__(self) + if device_id: + self._id = device_id + else: + self._id = util.unique_id() + + def get_type(self): + return 'unknown' + + def get_id(self): + return self._id diff --git a/src/jarabe/model/devices/devicesmodel.py b/src/jarabe/model/devices/devicesmodel.py new file mode 100644 index 0000000..5d6353d --- /dev/null +++ b/src/jarabe/model/devices/devicesmodel.py @@ -0,0 +1,144 @@ +# +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import gobject +import dbus + +from jarabe.model.devices import device +from jarabe.model.devices.network import wireless +from jarabe.model.devices.network import mesh +from jarabe.model.devices import battery +from jarabe.model.devices import speaker +from jarabe.hardware import hardwaremanager +from jarabe.hardware import nmclient + +class DevicesModel(gobject.GObject): + __gsignals__ = { + 'device-appeared' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'device-disappeared': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._devices = {} + self._sigids = {} + + self._observe_hal_manager() + self._observe_network_manager() + + try: + self.add_device(speaker.Device()) + except Exception, speaker_fail_msg: + logging.error("could not initialize speaker device: %s" % + speaker_fail_msg) + + def _observe_hal_manager(self): + bus = dbus.Bus(dbus.Bus.TYPE_SYSTEM) + proxy = bus.get_object('org.freedesktop.Hal', + '/org/freedesktop/Hal/Manager') + hal_manager = dbus.Interface(proxy, 'org.freedesktop.Hal.Manager') + + for udi in hal_manager.FindDeviceByCapability('battery'): + self.add_device(battery.Device(udi)) + + def _observe_network_manager(self): + network_manager = hardwaremanager.get_network_manager() + if not network_manager: + return + + for dev in network_manager.get_devices(): + self._check_network_device(dev) + + network_manager.connect('device-added', + self._network_device_added_cb) + network_manager.connect('device-activating', + self._network_device_activating_cb) + network_manager.connect('device-activated', + self._network_device_activated_cb) + network_manager.connect('device-removed', + self._network_device_removed_cb) + + def _network_device_added_cb(self, network_manager, nm_device): + state = nm_device.get_state() + if state == nmclient.DEVICE_STATE_ACTIVATING \ + or state == nmclient.DEVICE_STATE_ACTIVATED: + self._check_network_device(nm_device) + + def _network_device_activating_cb(self, network_manager, nm_device): + self._check_network_device(nm_device) + + def _network_device_activated_cb(self, network_manager, nm_device): + pass + + def _network_device_removed_cb(self, network_manager, nm_device): + if self._devices.has_key(str(nm_device.get_op())): + self.remove_device(self._get_network_device(nm_device)) + + def _check_network_device(self, nm_device): + if not nm_device.is_valid(): + logging.debug("Device %s not valid" % nm_device.get_op()) + return + + dtype = nm_device.get_type() + if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS \ + or dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC: + self._add_network_device(nm_device) + + def _get_network_device(self, nm_device): + return self._devices[str(nm_device.get_op())] + + def _network_device_state_changed_cb(self, dev, param): + if dev.props.state == device.STATE_INACTIVE: + self.remove_device(dev) + + def _add_network_device(self, nm_device): + if self._devices.has_key(str(nm_device.get_op())): + logging.debug("Tried to add device %s twice" % nm_device.get_op()) + return + + dtype = nm_device.get_type() + if dtype == nmclient.DEVICE_TYPE_802_11_WIRELESS: + dev = wireless.Device(nm_device) + self.add_device(dev) + sigid = dev.connect('notify::state', + self._network_device_state_changed_cb) + self._sigids[dev] = sigid + if dtype == nmclient.DEVICE_TYPE_802_11_MESH_OLPC: + dev = mesh.Device(nm_device) + self.add_device(dev) + sigid = dev.connect('notify::state', + self._network_device_state_changed_cb) + self._sigids[dev] = sigid + + def __iter__(self): + return iter(self._devices.values()) + + def add_device(self, dev): + self._devices[dev.get_id()] = dev + self.emit('device-appeared', dev) + + def remove_device(self, dev): + self.emit('device-disappeared', self._devices[dev.get_id()]) + dev.disconnect(self._sigids[dev]) + del self._sigids[dev] + del self._devices[dev.get_id()] diff --git a/src/jarabe/model/devices/network/Makefile.am b/src/jarabe/model/devices/network/Makefile.am new file mode 100644 index 0000000..736d96c --- /dev/null +++ b/src/jarabe/model/devices/network/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pythondir)/jarabe/model/devices/network +sugar_PYTHON = \ + __init__.py \ + mesh.py \ + wired.py \ + wireless.py diff --git a/src/jarabe/model/devices/network/__init__.py b/src/jarabe/model/devices/network/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/model/devices/network/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/model/devices/network/mesh.py b/src/jarabe/model/devices/network/mesh.py new file mode 100644 index 0000000..57f375f --- /dev/null +++ b/src/jarabe/model/devices/network/mesh.py @@ -0,0 +1,74 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject + +from jarabe.model.devices import device + +class Device(device.Device): + __gproperties__ = { + 'strength' : (int, None, None, 0, 100, 0, + gobject.PARAM_READABLE), + 'state' : (int, None, None, device.STATE_ACTIVATING, + device.STATE_INACTIVE, 0, gobject.PARAM_READABLE), + 'activation-stage': (int, None, None, 0, 7, 0, gobject.PARAM_READABLE), + 'frequency': (float, None, None, 0, 2.72, 0, gobject.PARAM_READABLE), + 'mesh-step': (int, None, None, 0, 4, 0, gobject.PARAM_READABLE), + } + + def __init__(self, nm_device): + device.Device.__init__(self) + self._nm_device = nm_device + + self._nm_device.connect('strength-changed', + self._strength_changed_cb) + self._nm_device.connect('state-changed', + self._state_changed_cb) + self._nm_device.connect('activation-stage-changed', + self._activation_stage_changed_cb) + + def _strength_changed_cb(self, nm_device): + self.notify('strength') + + def _state_changed_cb(self, nm_device): + self.notify('state') + + def _activation_stage_changed_cb(self, nm_device): + self.notify('activation-stage') + + def do_get_property(self, pspec): + if pspec.name == 'strength': + return self._nm_device.get_strength() + elif pspec.name == 'state': + nm_state = self._nm_device.get_state() + return device.nm_state_to_state[nm_state] + elif pspec.name == 'activation-stage': + return self._nm_device.get_activation_stage() + elif pspec.name == 'frequency': + return self._nm_device.get_frequency() + elif pspec.name == 'mesh-step': + return self._nm_device.get_mesh_step() + + def get_type(self): + return 'network.mesh' + + def get_id(self): + return str(self._nm_device.get_op()) + + def get_nm_device(self): + return self._nm_device + diff --git a/src/jarabe/model/devices/network/wired.py b/src/jarabe/model/devices/network/wired.py new file mode 100644 index 0000000..4178010 --- /dev/null +++ b/src/jarabe/model/devices/network/wired.py @@ -0,0 +1,28 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from jarabe.model.devices import device + +class Device(device.Device): + def __init__(self, nm_device): + device.Device.__init__(self) + self._nm_device = nm_device + + def get_id(self): + return str(self._nm_device.get_op()) + + def get_type(self): + return 'network.wired' diff --git a/src/jarabe/model/devices/network/wireless.py b/src/jarabe/model/devices/network/wireless.py new file mode 100644 index 0000000..c5b2a5c --- /dev/null +++ b/src/jarabe/model/devices/network/wireless.py @@ -0,0 +1,95 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject + +from jarabe.model.devices import device + +def freq_to_channel(freq): + ftoc = { 2.412: 1, 2.417: 2, 2.422: 3, 2.427: 4, + 2.432: 5, 2.437: 6, 2.442: 7, 2.447: 8, + 2.452: 9, 2.457: 10, 2.462: 11, 2.467: 12, + 2.472: 13 + } + return ftoc[freq] + +def channel_to_freq(channel): + ctof = { 1: 2.412, 2: 2.417, 3: 2.422, 4: 2.427, + 5: 2.432, 6: 2.437, 7: 2.442, 8: 2.447, + 9: 2.452, 10: 2.457, 11: 2.462, 12: 2.467, + 13: 2.472 + } + return ctof[channel] + +class Device(device.Device): + __gproperties__ = { + 'name' : (str, None, None, None, + gobject.PARAM_READABLE), + 'strength' : (int, None, None, 0, 100, 0, + gobject.PARAM_READABLE), + 'state' : (int, None, None, device.STATE_ACTIVATING, + device.STATE_INACTIVE, 0, gobject.PARAM_READABLE), + 'frequency': (float, None, None, 0.0, 9999.99, 0.0, + gobject.PARAM_READABLE) + } + + def __init__(self, nm_device): + device.Device.__init__(self) + self._nm_device = nm_device + + self._nm_device.connect('strength-changed', + self._strength_changed_cb) + self._nm_device.connect('ssid-changed', + self._ssid_changed_cb) + self._nm_device.connect('state-changed', + self._state_changed_cb) + + def _strength_changed_cb(self, nm_device): + self.notify('strength') + + def _ssid_changed_cb(self, nm_device): + self.notify('name') + + def _state_changed_cb(self, nm_device): + self.notify('state') + + def do_get_property(self, pspec): + if pspec.name == 'strength': + return self._nm_device.get_strength() + elif pspec.name == 'name': + import logging + logging.debug('wireless.Device.props.name: %s' % + self._nm_device.get_ssid()) + return self._nm_device.get_ssid() + elif pspec.name == 'state': + nm_state = self._nm_device.get_state() + return device.nm_state_to_state[nm_state] + elif pspec.name == 'frequency': + return self._nm_device.get_frequency() + + def get_type(self): + return 'network.wireless' + + def get_id(self): + return str(self._nm_device.get_op()) + + def get_active_network_colors(self): + net = self._nm_device.get_active_network() + if not net: + return (None, None) + return net.get_colors() + diff --git a/src/jarabe/model/devices/speaker.py b/src/jarabe/model/devices/speaker.py new file mode 100644 index 0000000..73e4f6e --- /dev/null +++ b/src/jarabe/model/devices/speaker.py @@ -0,0 +1,65 @@ +# Copyright (C) 2008 Martin Dengler +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject + +from jarabe.hardware import hardwaremanager +from jarabe.model.devices import device + +class Device(device.Device): + __gproperties__ = { + 'level' : (int, None, None, 0, 100, 0, gobject.PARAM_READWRITE), + 'muted' : (bool, None, None, False, gobject.PARAM_READWRITE), + } + + def __init__(self): + device.Device.__init__(self) + self._manager = hardwaremanager.get_manager() + self._manager.connect('muted-changed', self.__muted_changed_cb) + self._manager.connect('volume-changed', self.__volume_changed_cb) + + def __muted_changed_cb(self, sender_, old_state_, new_state_): + self.notify('muted') + + def __volume_changed_cb(self, sender_, old_volume_, new_volume_): + self.notify('level') + + def _get_level(self): + return self._manager.get_volume() + + def _set_level(self, new_volume): + self._manager.set_volume(new_volume) + + def _get_muted(self): + return self._manager.get_muted() + + def _set_muted(self, mute): + self._manager.set_muted(mute) + + def get_type(self): + return 'speaker' + + def do_get_property(self, pspec): + if pspec.name == "level": + return self._get_level() + elif pspec.name == "muted": + return self._get_muted() + + def do_set_property(self, pspec, value): + if pspec.name == "level": + self._set_level(value) + elif pspec.name == "muted": + self._set_muted(value) diff --git a/src/jarabe/model/homeactivity.py b/src/jarabe/model/homeactivity.py new file mode 100644 index 0000000..ba0979e --- /dev/null +++ b/src/jarabe/model/homeactivity.py @@ -0,0 +1,245 @@ +# Copyright (C) 2006-2007 Owen Williams. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import time +import logging +import os + +import gobject +import dbus + +from sugar.graphics.xocolor import XoColor +from sugar.presence import presenceservice +from sugar import profile +from sugar import wm + +import config + +_SERVICE_NAME = "org.laptop.Activity" +_SERVICE_PATH = "/org/laptop/Activity" +_SERVICE_INTERFACE = "org.laptop.Activity" + +class HomeActivity(gobject.GObject): + """Activity which appears in the "Home View" of the Sugar shell + + This class stores the Sugar Shell's metadata regarding a + given activity/application in the system. It interacts with + the sugar.activity.* modules extensively in order to + accomplish its tasks. + """ + + __gtype_name__ = 'SugarHomeActivity' + + __gproperties__ = { + 'launching' : (bool, None, None, False, + gobject.PARAM_READWRITE), + } + + def __init__(self, activity_info, activity_id, window=None): + """Initialise the HomeActivity + + activity_info -- sugar.activity.registry.ActivityInfo instance, + provides the information required to actually + create the new instance. This is, in effect, + the "type" of activity being created. + activity_id -- unique identifier for this instance + of the activity type + window -- Main WnckWindow of the activity + """ + gobject.GObject.__init__(self) + + self._window = None + self._service = None + self._activity_id = activity_id + self._activity_info = activity_info + self._launch_time = time.time() + self._launching = False + + if window is not None: + self.set_window(window) + + self._retrieve_service() + + self._name_owner_changed_handler = None + if not self._service: + bus = dbus.SessionBus() + self._name_owner_changed_handler = bus.add_signal_receiver( + self._name_owner_changed_cb, + signal_name="NameOwnerChanged", + dbus_interface="org.freedesktop.DBus") + + def set_window(self, window): + """Set the window for the activity + + We allow resetting the window for an activity so that we + can replace the launcher once we get its real window. + """ + if not window: + raise ValueError("window must be valid") + self._window = window + + def get_service(self): + """Get the activity service + + Note that non-native Sugar applications will not have + such a service, so the return value will be None in + those cases. + """ + + return self._service + + def get_title(self): + """Retrieve the application's root window's suggested title""" + if self._window: + return self._window.get_name() + else: + return '' + + def get_icon_path(self): + """Retrieve the activity's icon (file) name""" + if self.is_journal(): + return os.path.join(config.data_path, 'icons/activity-journal.svg') + elif self._activity_info: + return self._activity_info.icon + else: + return None + + def get_icon_color(self): + """Retrieve the appropriate icon colour for this activity + + Uses activity_id to index into the PresenceService's + set of activity colours, if the PresenceService does not + have an entry (implying that this is not a Sugar-shared application) + uses the local user's profile.get_color() to determine the + colour for the icon. + """ + pservice = presenceservice.get_instance() + + # HACK to suppress warning in logs when activity isn't found + # (if it's locally launched and not shared yet) + activity = None + for act in pservice.get_activities(): + if self._activity_id == act.props.id: + activity = act + break + + if activity != None: + return XoColor(activity.props.color) + else: + return profile.get_color() + + def get_activity_id(self): + """Retrieve the "activity_id" passed in to our constructor + + This is a "globally likely unique" identifier generated by + sugar.util.unique_id + """ + return self._activity_id + + def get_xid(self): + """Retrieve the X-windows ID of our root window""" + return self._window.get_xid() + + def get_window(self): + """Retrieve the X-windows root window of this application + + This was stored by the set_window method, which was + called by HomeModel._add_activity, which was called + via a callback that looks for all 'window-opened' + events. + + HomeModel currently uses a dbus service query on the + activity to determine to which HomeActivity the newly + launched window belongs. + """ + return self._window + + def get_type(self): + """Retrieve the activity bundle id for future reference""" + if self._window is None: + return None + else: + return wm.get_bundle_id(self._window) + + def is_journal(self): + """Returns boolean if the activity is of type JournalActivity""" + return self.get_type() == 'org.laptop.JournalActivity' + + def get_launch_time(self): + """Return the time at which the activity was first launched + + Format is floating-point time.time() value + (seconds since the epoch) + """ + return self._launch_time + + def get_pid(self): + """Returns the activity's PID""" + return self._window.get_pid() + + def equals(self, activity): + if self._activity_id and activity.get_activity_id(): + return self._activity_id == activity.get_activity_id() + if self._window.get_xid() and activity.get_xid(): + return self._window.get_xid() == activity.get_xid() + return False + + def do_set_property(self, pspec, value): + if pspec.name == 'launching': + self._launching = value + + def do_get_property(self, pspec): + if pspec.name == 'launching': + return self._launching + + def _get_service_name(self): + if self._activity_id: + return _SERVICE_NAME + self._activity_id + else: + return None + + def _retrieve_service(self): + if not self._activity_id: + return + + try: + bus = dbus.SessionBus() + proxy = bus.get_object(self._get_service_name(), + _SERVICE_PATH + "/" + self._activity_id) + self._service = dbus.Interface(proxy, _SERVICE_INTERFACE) + except dbus.DBusException: + self._service = None + + def _name_owner_changed_cb(self, name, old, new): + if name == self._get_service_name(): + self._retrieve_service() + self.set_active(True) + self._name_owner_changed_handler.remove() + self._name_owner_changed_handler = None + + def set_active(self, state): + """Propagate the current state to the activity object""" + if self._service is not None: + self._service.SetActive(state, + reply_handler=self._set_active_success, + error_handler=self._set_active_error) + + def _set_active_success(self): + pass + + def _set_active_error(self, err): + logging.error("set_active() failed: %s" % err) + diff --git a/src/jarabe/model/homemodel.py b/src/jarabe/model/homemodel.py new file mode 100644 index 0000000..3f116bf --- /dev/null +++ b/src/jarabe/model/homemodel.py @@ -0,0 +1,286 @@ +# Copyright (C) 2006-2007 Owen Williams. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gobject +import wnck +import gtk + +from sugar import wm +from sugar.activity import get_registry + +from jarabe.model.homeactivity import HomeActivity + +def get_sugar_window_type(wnck_window): + window = gtk.gdk.window_foreign_new(wnck_window.get_xid()) + prop_info = window.property_get('_SUGAR_WINDOW_TYPE', 'STRING') + if prop_info is None: + return None + else: + return prop_info[2] + +class HomeModel(gobject.GObject): + """Model of the "Home" view (activity management) + + The HomeModel is basically the point of registration + for all running activities within Sugar. It traps + events that tell the system there is a new activity + being created (generated by the activity factories), + or removed, as well as those which tell us that the + currently focussed activity has changed. + + The HomeModel tracks a set of HomeActivity instances, + which are tracking the window to activity mappings + the activity factories have set up. + """ + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'active-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'tabbing-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-started': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-completed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-failed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._activities = [] + self._active_activity = None + self._tabbing_activity = None + + screen = wnck.screen_get_default() + screen.connect('window-opened', self._window_opened_cb) + screen.connect('window-closed', self._window_closed_cb) + screen.connect('active-window-changed', + self._active_window_changed_cb) + + def _get_activities_with_window(self): + ret = [] + for i in self._activities: + if i.get_window() is not None: + ret.append(i) + return ret + + def get_previous_activity(self, current=None): + if not current: + current = self._active_activity + + activities = self._get_activities_with_window() + i = activities.index(current) + if len(activities) == 0: + return None + elif i - 1 >= 0: + return activities[i - 1] + else: + return activities[len(activities) - 1] + + def get_next_activity(self, current=None): + if not current: + current = self._active_activity + + activities = self._get_activities_with_window() + i = activities.index(current) + if len(activities) == 0: + return None + elif i + 1 < len(activities): + return activities[i + 1] + else: + return activities[0] + + def get_active_activity(self): + """Returns the activity that the user is currently working in""" + return self._active_activity + + def get_tabbing_activity(self): + """Returns the activity that is currently highlighted during tabbing""" + return self._tabbing_activity + + def set_tabbing_activity(self, activity): + """Sets the activity that is currently highlighted during tabbing""" + self._tabbing_activity = activity + self.emit("tabbing-activity-changed", self._tabbing_activity) + + def _set_active_activity(self, home_activity): + if self._active_activity == home_activity: + return + + if home_activity: + home_activity.set_active(True) + + if self._active_activity: + self._active_activity.set_active(False) + + self._active_activity = home_activity + self.emit('active-activity-changed', self._active_activity) + + def __iter__(self): + return iter(self._activities) + + def __len__(self): + return len(self._activities) + + def __getitem__(self, i): + return self._activities[i] + + def index(self, obj): + return self._activities.index(obj) + + def _window_opened_cb(self, screen, window): + if window.get_window_type() == wnck.WINDOW_NORMAL: + home_activity = None + + activity_id = wm.get_activity_id(window) + + service_name = wm.get_bundle_id(window) + if service_name: + registry = get_registry() + activity_info = registry.get_activity(service_name) + else: + activity_info = None + + if activity_id: + home_activity = self._get_activity_by_id(activity_id) + + if not home_activity: + home_activity = HomeActivity(activity_info, activity_id, window) + self._add_activity(home_activity) + else: + home_activity.set_window(window) + + if get_sugar_window_type(window) != 'launcher': + home_activity.props.launching = False + self.emit('launch-completed', home_activity) + + if self._active_activity is None: + self._set_active_activity(home_activity) + + def _window_closed_cb(self, screen, window): + if window.get_window_type() == wnck.WINDOW_NORMAL: + self._remove_activity_by_xid(window.get_xid()) + + def _get_activity_by_xid(self, xid): + for home_activity in self._activities: + if home_activity.get_xid() == xid: + return home_activity + return None + + def _get_activity_by_id(self, activity_id): + for home_activity in self._activities: + if home_activity.get_activity_id() == activity_id: + return home_activity + return None + + def _active_window_changed_cb(self, screen, previous_window=None): + window = screen.get_active_window() + if window is None: + return + + if window.get_window_type() != wnck.WINDOW_DIALOG: + while window.get_transient() is not None: + window = window.get_transient() + + act = self._get_activity_by_xid(window.get_xid()) + if act is not None: + self._set_active_activity(act) + + def _add_activity(self, home_activity): + self._activities.append(home_activity) + self.emit('activity-added', home_activity) + + def _remove_activity(self, home_activity): + if home_activity == self._active_activity: + windows = wnck.screen_get_default().get_windows_stacked() + windows.reverse() + for window in windows: + new_activity = self._get_activity_by_xid(window.get_xid()) + if new_activity is not None: + self._set_active_activity(new_activity) + break + else: + logging.error('No activities are running') + self._set_active_activity(None) + + self.emit('activity-removed', home_activity) + self._activities.remove(home_activity) + + def _remove_activity_by_xid(self, xid): + home_activity = self._get_activity_by_xid(xid) + if home_activity: + self._remove_activity(home_activity) + else: + logging.error('Model for window %d does not exist.' % xid) + + def notify_launch(self, activity_id, service_name): + registry = get_registry() + activity_info = registry.get_activity(service_name) + if not activity_info: + raise ValueError("Activity service name '%s'" \ + " was not found in the bundle registry." + % service_name) + home_activity = HomeActivity(activity_info, activity_id) + home_activity.props.launching = True + self._add_activity(home_activity) + + self._set_active_activity(home_activity) + + self.emit('launch-started', home_activity) + + # FIXME: better learn about finishing processes by receiving a signal. + # Now just check whether an activity has a window after ~90sec + gobject.timeout_add(90000, self._check_activity_launched, activity_id) + + def notify_launch_failed(self, activity_id): + home_activity = self._get_activity_by_id(activity_id) + if home_activity: + logging.debug("Activity %s (%s) launch failed" % \ + (activity_id, home_activity.get_type())) + home_activity.props.launching = False + self._remove_activity(home_activity) + else: + logging.error('Model for activity id %s does not exist.' + % activity_id) + + self.emit('launch-failed', home_activity) + + def _check_activity_launched(self, activity_id): + home_activity = self._get_activity_by_id(activity_id) + + if not home_activity: + logging.debug('Activity %s has been closed already.' % activity_id) + return False + + if home_activity.props.launching: + logging.debug('Activity %s still launching, assuming it failed...' + % activity_id) + self.notify_launch_failed(activity_id) + return False diff --git a/src/jarabe/model/shellmodel.py b/src/jarabe/model/shellmodel.py new file mode 100644 index 0000000..0c37f35 --- /dev/null +++ b/src/jarabe/model/shellmodel.py @@ -0,0 +1,104 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import wnck +import gobject + +from sugar.presence import presenceservice +from jarabe.model.Friends import Friends +from jarabe.model.MeshModel import MeshModel +from jarabe.model.homemodel import HomeModel +from jarabe.model.Owner import ShellOwner +from jarabe.model.devices.devicesmodel import DevicesModel + +class ShellModel(gobject.GObject): + ZOOM_MESH = 0 + ZOOM_FRIENDS = 1 + ZOOM_HOME = 2 + ZOOM_ACTIVITY = 3 + + __gproperties__ = { + 'zoom-level' : (int, None, None, + 0, 3, ZOOM_HOME, + gobject.PARAM_READABLE) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._current_activity = None + self._zoom_level = self.ZOOM_HOME + self._showing_desktop = True + + self._pservice = presenceservice.get_instance() + + self._owner = ShellOwner() + + self._friends = Friends() + self._mesh = MeshModel() + self._home = HomeModel() + self._devices = DevicesModel() + + self._screen = wnck.screen_get_default() + self._screen.connect('showing-desktop-changed', + self._showing_desktop_changed_cb) + + def set_zoom_level(self, level): + self._zoom_level = level + self.notify('zoom-level') + + def get_zoom_level(self): + if self._screen.get_showing_desktop(): + return self._zoom_level + else: + return self.ZOOM_ACTIVITY + + def do_get_property(self, pspec): + if pspec.name == 'zoom-level': + return self.get_zoom_level() + + def get_mesh(self): + return self._mesh + + def get_friends(self): + return self._friends + + def get_invites(self): + return self._owner.get_invites() + + def get_home(self): + return self._home + + def get_owner(self): + return self._owner + + def get_devices(self): + return self._devices + + def _showing_desktop_changed_cb(self, screen): + showing_desktop = self._screen.get_showing_desktop() + if self._showing_desktop != showing_desktop: + self._showing_desktop = showing_desktop + self.notify('zoom-level') + +_instance = None + +def get_instance(): + global _instance + if not _instance: + _instance = ShellModel() + return _instance + diff --git a/src/jarabe/session.py b/src/jarabe/session.py new file mode 100644 index 0000000..c1f8d0f --- /dev/null +++ b/src/jarabe/session.py @@ -0,0 +1,82 @@ +# Copyright (C) 2008, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import dbus +import os +import signal + +from sugar import session +from sugar import env + +from jarabe.hardware import hardwaremanager + +_session_manager = None + +class SessionManager(session.SessionManager): + MODE_LOGOUT = 0 + MODE_SHUTDOWN = 1 + MODE_REBOOT = 2 + + def __init__(self): + session.SessionManager.__init__(self) + self._logout_mode = None + + def logout(self): + self._logout_mode = self.MODE_LOGOUT + self.initiate_shutdown() + + def shutdown(self): + self._logout_mode = self.MODE_SHUTDOWN + self.initiate_shutdown() + + def reboot(self): + self._logout_mode = self.MODE_REBOOT + self.initiate_shutdown() + + def shutdown_completed(self): + session.SessionManager.shutdown_completed(self) + + hw_manager = hardwaremanager.get_manager() + hw_manager.shutdown() + + bus = dbus.SystemBus() + proxy = bus.get_object('org.freedesktop.Hal', + '/org/freedesktop/Hal/devices/computer') + pm = dbus.Interface(proxy, \ + 'org.freedesktop.Hal.Device.SystemPowerManagement') + + if env.is_emulator(): + self._close_emulator() + else: + if self._logout_mode == self.MODE_SHUTDOWN: + pm.Shutdown() + elif self._logout_mode == self.MODE_REBOOT: + pm.Reboot() + + gtk.main_quit() + + def _close_emulator(self): + if os.environ.has_key('SUGAR_EMULATOR_PID'): + pid = int(os.environ['SUGAR_EMULATOR_PID']) + os.kill(pid, signal.SIGTERM) + +def get_session_manager(): + global _session_manager + + if _session_manager == None: + _session_manager = SessionManager() + return _session_manager diff --git a/src/jarabe/shellservice.py b/src/jarabe/shellservice.py new file mode 100644 index 0000000..fd8868e --- /dev/null +++ b/src/jarabe/shellservice.py @@ -0,0 +1,135 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""D-bus service providing access to the shell's functionality""" +import dbus +import os + +from jarabe.view import Shell +from jarabe.model import shellmodel + +_DBUS_SERVICE = "org.laptop.Shell" +_DBUS_SHELL_IFACE = "org.laptop.Shell" +_DBUS_OWNER_IFACE = "org.laptop.Shell.Owner" +_DBUS_PATH = "/org/laptop/Shell" + +_DBUS_RAINBOW_IFACE = "org.laptop.security.Rainbow" + +class ShellService(dbus.service.Object): + """Provides d-bus service to script the shell's operations + + Uses a shell_model object to observe events such as changes to: + + * nickname + * colour + * icon + * currently active activity + + and pass the event off to the methods in the dbus signature. + + Key method here at the moment is add_bundle, which is used to + do a run-time registration of a bundle using it's application path. + + XXX At the moment the d-bus service methods do not appear to do + anything other than add_bundle + """ + + _rainbow = None + + def __init__(self): + self._shell = Shell.get_instance() + self._shell_model = shellmodel.get_instance() + + self._owner = self._shell_model.get_owner() + self._owner.connect('nick-changed', self._owner_nick_changed_cb) + self._owner.connect('icon-changed', self._owner_icon_changed_cb) + self._owner.connect('color-changed', self._owner_color_changed_cb) + + self._home_model = self._shell_model.get_home() + self._home_model.connect('active-activity-changed', + self._cur_activity_changed_cb) + + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus) + dbus.service.Object.__init__(self, bus_name, _DBUS_PATH) + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature="s", out_signature="b") + def ActivateActivity(self, activity_id): + host = self._shell.get_activity(activity_id) + if host: + host.present() + return True + + return False + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature="ss", out_signature="") + def NotifyLaunch(self, bundle_id, activity_id): + home = self._shell.get_model().get_home() + home.notify_launch(activity_id, bundle_id) + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature="s", out_signature="") + def NotifyLaunchFailure(self, activity_id): + home = self._shell.get_model().get_home() + home.notify_launch_failed(activity_id) + + @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s") + def ColorChanged(self, color): + pass + + def _owner_color_changed_cb(self, new_color): + self.ColorChanged(new_color.to_string()) + + @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s") + def NickChanged(self, nick): + pass + + def _owner_nick_changed_cb(self, new_nick): + self.NickChanged(new_nick) + + @dbus.service.signal(_DBUS_OWNER_IFACE, signature="ay") + def IconChanged(self, icon_data): + pass + + def _owner_icon_changed_cb(self, new_icon): + self.IconChanged(dbus.ByteArray(new_icon)) + + def _get_rainbow_service(self): + """Lazily initializes an interface to the Rainbow security daemon.""" + if self._rainbow is None: + system_bus = dbus.SystemBus() + obj = system_bus.get_object(_DBUS_RAINBOW_IFACE, '/', + follow_name_owner_changes=True) + self._rainbow = dbus.Interface(obj, + dbus_interface=_DBUS_RAINBOW_IFACE) + return self._rainbow + + @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s") + def CurrentActivityChanged(self, activity_id): + if os.path.exists('/etc/olpc-security'): + self._get_rainbow_service().ChangeActivity( + activity_id, + dbus_interface=_DBUS_RAINBOW_IFACE) + + def _cur_activity_changed_cb(self, owner, new_activity): + new_id = "" + if new_activity: + new_id = new_activity.get_activity_id() + if new_id: + self.CurrentActivityChanged(new_id) + diff --git a/src/jarabe/uicheck.py b/src/jarabe/uicheck.py new file mode 100644 index 0000000..aebe990 --- /dev/null +++ b/src/jarabe/uicheck.py @@ -0,0 +1,148 @@ +# Copyright (C) 2008, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import os +import sys +import subprocess +import time + +import gobject +import gtk +import wnck + +from sugar import wm + +from jarabe.model.homemodel import get_sugar_window_type +import config + +checks_queue = [] +checks_failed = [] +checks_succeeded = [] + +def get_dbus_version(): + p = subprocess.Popen(['dbus-daemon', '--version'], stdout=subprocess.PIPE) + + output = p.communicate()[0] + first_line = output.split('\n')[0] + + return first_line.split(' ')[-1] + +class Check(object): + def __init__(self): + self.name = None + self.succeeded = False + self.start_time = None + self.max_time = None + self.timeout = None + + def start(self): + logging.info('Start %s check.' % self.name) + + self.start_time = time.time() + + def get_failed(self): + if self.max_time and self.start_time: + if time.time() - self.start_time > self.max_time: + return True + return False + + failed = property(get_failed) + +class ShellCheck(Check): + def __init__(self): + Check.__init__(self) + + self.name = 'Shell' + self.max_time = 30 + + def start(self): + Check.start(self) + + screen = wnck.screen_get_default() + screen.connect('window-opened', self._window_opened_cb) + + def _window_opened_cb(self, screen, window): + if window.get_window_type() == wnck.WINDOW_DESKTOP: + self.succeeded = True + +class ActivityCheck(Check): + def __init__(self, bundle_id): + Check.__init__(self) + + self.name = bundle_id + self.max_time = 30 + + def start(self): + Check.start(self) + + self.launch_activity() + + screen = wnck.screen_get_default() + screen.connect('window-opened', self._window_opened_cb) + + def launch_activity(self): + from sugar.activity import activityfactory + + activityfactory.create(self.name) + + def _window_opened_cb(self, screen, window): + if wm.get_bundle_id(window) == self.name and \ + get_sugar_window_type(window) != 'launcher': + self.succeeded = True + +def _timeout_cb(): + check = checks_queue[0] + if check.failed: + logging.info('%s check failed.' % (check.name)) + checks_failed.append(checks_queue.pop(0)) + elif check.succeeded: + logging.info('%s check succeeded.' % (check.name)) + checks_succeeded.append(checks_queue.pop(0)) + else: + return True + + if len(checks_queue) > 0: + checks_queue[0].start() + else: + gtk.main_quit() + + return True + +def main(): + os.environ['GTK2_RC_FILES'] = os.path.join(config.data_path, 'sugar.gtkrc') + + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(levelname)s %(message)s') + + checks_queue.append(ShellCheck()) + + if get_dbus_version() >= '1.2.1': + # FIXME needs to get a list of the installed activities + checks_queue.append(ActivityCheck('org.laptop.Log')) + checks_queue.append(ActivityCheck('org.laptop.Chat')) + checks_queue.append(ActivityCheck('org.laptop.WebActivity')) + checks_queue.append(ActivityCheck('org.laptop.Pippy')) + checks_queue.append(ActivityCheck('org.laptop.Terminal')) + checks_queue.append(ActivityCheck('org.laptop.AbiWordActivity')) + + checks_queue[0].start() + gobject.timeout_add(500, _timeout_cb) + + gtk.main() + + if len(checks_failed) > 0: + sys.exit(1) diff --git a/src/jarabe/view/ActivityHost.py b/src/jarabe/view/ActivityHost.py new file mode 100644 index 0000000..e3be3db --- /dev/null +++ b/src/jarabe/view/ActivityHost.py @@ -0,0 +1,63 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import logging + +from jarabe.view import OverlayWindow + +class ActivityHost: + def __init__(self, model): + self._model = model + self._window = model.get_window() + self._gdk_window = gtk.gdk.window_foreign_new(self.get_xid()) + + try: + self._overlay_window = OverlayWindow.OverlayWindow(self._gdk_window) + except RuntimeError: + self._overlay_window = None + + def get_id(self): + return self._model.get_activity_id() + + def get_xid(self): + return self._window.get_xid() + + def get_model(self): + return self._model + + def invite(self, buddy_model): + service = self._model.get_service() + if service: + buddy = buddy_model.get_buddy() + service.Invite(buddy.props.key) + else: + logging.error('Invite failed, activity service not ') + + def toggle_fullscreen(self): + fullscreen = not self._window.is_fullscreen() + self._window.set_fullscreen(fullscreen) + + def present(self): + self._window.activate(gtk.get_current_event_time()) + + def close(self): + # The "1" is a fake timestamp as with present() + self._window.close(1) + + def show_dialog(self, dialog): + dialog.show() + dialog.window.set_transient_for(self._gdk_window) diff --git a/src/jarabe/view/BuddyIcon.py b/src/jarabe/view/BuddyIcon.py new file mode 100644 index 0000000..3b1db4c --- /dev/null +++ b/src/jarabe/view/BuddyIcon.py @@ -0,0 +1,60 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style + +from jarabe.view.BuddyMenu import BuddyMenu + +class BuddyIcon(CanvasIcon): + def __init__(self, buddy, size=style.STANDARD_ICON_SIZE): + CanvasIcon.__init__(self, icon_name='computer-xo', size=size) + + self._greyed_out = False + self._buddy = buddy + self._buddy.connect('appeared', self._buddy_presence_change_cb) + self._buddy.connect('disappeared', self._buddy_presence_change_cb) + self._buddy.connect('color-changed', self._buddy_presence_change_cb) + + palette = BuddyMenu(buddy) + self.set_palette(palette) + + self._update_color() + + def _buddy_presence_change_cb(self, buddy, color=None): + # Update the icon's color when the buddy comes and goes + self._update_color() + + def _update_color(self): + + # keep the icon in the palette in sync with the view + palette = self.get_palette() + palette_icon = palette.props.icon + + if self._greyed_out: + self.props.stroke_color = '#D5D5D5' + self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + palette_icon.props.stroke_color = '#D5D5D5' + palette_icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + else: + self.props.xo_color = self._buddy.get_color() + palette_icon.props.xo_color = self._buddy.get_color() + + def set_filter(self, query): + self._greyed_out = (self._buddy.get_nick().lower().find(query) == -1) \ + and not self._buddy.is_owner() + self._update_color() + diff --git a/src/jarabe/view/BuddyMenu.py b/src/jarabe/view/BuddyMenu.py new file mode 100644 index 0000000..2f7a048 --- /dev/null +++ b/src/jarabe/view/BuddyMenu.py @@ -0,0 +1,121 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +import gtk + +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon + +from jarabe.model import shellmodel +import view.Shell + +class BuddyMenu(Palette): + def __init__(self, buddy): + self._buddy = buddy + + buddy_icon = Icon(icon_name='computer-xo', + xo_color=buddy.get_color(), + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + Palette.__init__(self, None, primary_text=buddy.get_nick(), + icon=buddy_icon) + self._invite_menu = None + self._active_activity_changed_hid = None + self.connect('destroy', self.__destroy_cb) + + self._buddy.connect('icon-changed', self._buddy_icon_changed_cb) + self._buddy.connect('nick-changed', self._buddy_nick_changed_cb) + + if not buddy.is_owner(): + self._add_items() + + def _get_home_model(self): + return shellmodel.get_instance().get_home() + + def __destroy_cb(self, menu): + if self._active_activity_changed_hid is not None: + home_model = self._get_home_model() + home_model.disconnect(self._active_activity_changed_hid) + self._buddy.disconnect_by_func(self._buddy_icon_changed_cb) + self._buddy.disconnect_by_func(self._buddy_nick_changed_cb) + + def _add_items(self): + friends = shellmodel.get_instance().get_friends() + if friends.has_buddy(self._buddy): + menu_item = MenuItem(_('Remove friend'), 'list-remove') + menu_item.connect('activate', self._remove_friend_cb) + else: + menu_item = MenuItem(_('Make friend'), 'list-add') + menu_item.connect('activate', self._make_friend_cb) + + self.menu.append(menu_item) + menu_item.show() + + self._invite_menu = MenuItem('') + self._invite_menu.connect('activate', self._invite_friend_cb) + self.menu.append(self._invite_menu) + + home_model = self._get_home_model() + self._active_activity_changed_hid = home_model.connect( + 'active-activity-changed', self._cur_activity_changed_cb) + activity = home_model.get_active_activity() + self._update_invite_menu(activity) + + def _update_invite_menu(self, activity): + buddy_activity = self._buddy.get_current_activity() + if buddy_activity is not None: + buddy_activity_id = buddy_activity.props.id + else: + buddy_activity_id = None + + if activity is None or activity.is_journal() or \ + activity.get_activity_id() == buddy_activity_id: + self._invite_menu.hide() + else: + title = activity.get_title() + label = self._invite_menu.get_children()[0] + label.set_text(_('Invite to %s') % title) + + icon = Icon(file=activity.get_icon_path()) + icon.props.xo_color = activity.get_icon_color() + self._invite_menu.set_image(icon) + icon.show() + + self._invite_menu.show() + + def _cur_activity_changed_cb(self, home_model, activity_model): + self._update_invite_menu(activity_model) + + def _buddy_icon_changed_cb(self, buddy): + pass + + def _buddy_nick_changed_cb(self, buddy, nick): + self.set_primary_text(nick) + + def _make_friend_cb(self, menuitem): + friends = shellmodel.get_instance().get_friends() + friends.make_friend(self._buddy) + + def _remove_friend_cb(self, menuitem): + friends = shellmodel.get_instance().get_friends() + friends.remove(self._buddy) + + def _invite_friend_cb(self, menuitem): + activity = view.Shell.get_instance().get_current_activity() + activity.invite(self._buddy) + diff --git a/src/jarabe/view/Makefile.am b/src/jarabe/view/Makefile.am new file mode 100644 index 0000000..040e6e7 --- /dev/null +++ b/src/jarabe/view/Makefile.am @@ -0,0 +1,17 @@ +SUBDIRS = devices frame home + +sugardir = $(pythondir)/jarabe/view +sugar_PYTHON = \ + __init__.py \ + ActivityHost.py \ + BuddyIcon.py \ + BuddyMenu.py \ + clipboardicon.py \ + clipboardmenu.py \ + keyhandler.py \ + launchwindow.py \ + pulsingicon.py \ + tabbinghandler.py \ + OverlayWindow.py \ + palettes.py \ + Shell.py diff --git a/src/jarabe/view/OverlayWindow.py b/src/jarabe/view/OverlayWindow.py new file mode 100644 index 0000000..15bcb49 --- /dev/null +++ b/src/jarabe/view/OverlayWindow.py @@ -0,0 +1,70 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import cairo + +def _grab_pixbuf(window=None): + if not window: + screen = gtk.gdk.screen_get_default() + window = screen.get_root_window() + color_map = gtk.gdk.colormap_get_system() + (x, y, w, h, bpp_) = window.get_geometry() + return gtk.gdk.pixbuf_get_from_drawable(None, window, color_map, + x, y, 0, 0, w, h) + +class OverlayWindow(gtk.Window): + def __init__(self, lower_window): + gtk.Window.__init__(self) + self._lower_window = lower_window + + self._img = gtk.Image() + self.add(self._img) + + self.realize() + + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + self.window.set_transient_for(lower_window) + + self.set_decorated(False) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_default_size(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + self.set_app_paintable(True) + + # self.connect('expose-event', self._expose_cb) + + def appear(self): + pbuf = _grab_pixbuf(self._lower_window) + #pbuf.saturate_and_pixelate(pbuf, 0.5, False) + w = pbuf.get_width() + h = pbuf.get_height() + pbuf2 = pbuf.composite_color_simple(w, h, gtk.gdk.INTERP_NEAREST, + 100, 1024, 0, 0) + self._img.set_from_pixbuf(pbuf2) + self.show_all() + + def disappear(self): + self._img.set_from_pixbuf(None) + self.hide() + + def _expose_cb(self, widget, event): + cr = widget.window.cairo_create() + cr.set_source_rgba(0.0, 0.0, 0.0, 0.4) # Transparent + cr.set_operator(cairo.OPERATOR_SOURCE) + cr.paint() + return False + diff --git a/src/jarabe/view/Shell.py b/src/jarabe/view/Shell.py new file mode 100644 index 0000000..089bfb5 --- /dev/null +++ b/src/jarabe/view/Shell.py @@ -0,0 +1,270 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging +import tempfile +import os +import time +import shutil + +import gobject +import gtk +import wnck +import dbus + +from sugar.activity.activityhandle import ActivityHandle +from sugar import activity +from sugar.activity import activityfactory +from sugar.datastore import datastore +from sugar import profile +from sugar import env + +from jarabe.view.ActivityHost import ActivityHost +from jarabe.view.frame import frame +from jarabe.view.keyhandler import KeyHandler +from jarabe.view.home.HomeWindow import HomeWindow +from jarabe.view.launchwindow import LaunchWindow +from jarabe.model import shellmodel +from jarabe.journal import journalactivity + +# #3903 - this constant can be removed and assumed to be 1 when dbus-python +# 0.82.3 is the only version used +if dbus.version >= (0, 82, 3): + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1 +else: + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1000 + +class Shell(gobject.GObject): + def __init__(self): + gobject.GObject.__init__(self) + + self._model = shellmodel.get_instance() + self._hosts = {} + self._launchers = {} + self._screen = wnck.screen_get_default() + self._screen_rotation = 0 + + self._key_handler = KeyHandler() + + self._frame = frame.get_instance() + + self.home_window = HomeWindow() + self.home_window.show() + + home_model = self._model.get_home() + home_model.connect('launch-started', self.__launch_started_cb) + home_model.connect('launch-failed', self.__launch_failed_cb) + home_model.connect('launch-completed', self.__launch_completed_cb) + home_model.connect('activity-removed', self._activity_removed_cb) + + gobject.idle_add(self._start_journal_idle) + + def _start_journal_idle(self): + # Mount the datastore in internal flash + ds_path = env.get_profile_path('datastore') + try: + datastore.mount(ds_path, [], timeout=120 * \ + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND) + except Exception, e: + # Don't explode if there's corruption; move the data out of the way + # and attempt to create a store from scratch. + logging.error(e) + shutil.move(ds_path, os.path.abspath(ds_path) + str(time.time())) + datastore.mount(ds_path, [], timeout=120 * \ + DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND) + + journalactivity.start() + + def __launch_started_cb(self, home_model, home_activity): + if home_activity.is_journal(): + return + + launch_window = LaunchWindow(home_activity) + launch_window.show() + + self._launchers[home_activity.get_activity_id()] = launch_window + self._model.set_zoom_level(shellmodel.ShellModel.ZOOM_ACTIVITY) + + def __launch_failed_cb(self, home_model, home_activity): + if not home_activity.is_journal(): + self._destroy_launcher(home_activity) + + def __launch_completed_cb(self, home_model, home_activity): + activity_host = ActivityHost(home_activity) + self._hosts[activity_host.get_xid()] = activity_host + + if not home_activity.is_journal(): + self._destroy_launcher(home_activity) + + def _destroy_launcher(self, home_activity): + activity_id = home_activity.get_activity_id() + + if activity_id in self._launchers: + self._launchers[activity_id].destroy() + del self._launchers[activity_id] + else: + logging.error('Launcher for %s is missing' % activity_id) + + def _activity_removed_cb(self, home_model, home_activity): + xid = home_activity.get_xid() + if self._hosts.has_key(xid): + del self._hosts[xid] + + def _get_host_from_activity_model(self, activity_model): + if activity_model is None: + raise ValueError('activity_model cannot be None') + xid = activity_model.get_xid() + if xid: + return self._hosts[activity_model.get_xid()] + else: + logging.debug('Activity %r dont have a window yet' % activity_model) + return None + + def get_model(self): + return self._model + + def get_frame(self): + return self._frame + + def join_activity(self, bundle_id, activity_id): + activity_host = self.get_activity(activity_id) + if activity_host: + activity_host.present() + return + + # Get the service name for this activity, if + # we have a bundle on the system capable of handling + # this activity type + registry = activity.get_registry() + bundle = registry.get_activity(bundle_id) + if not bundle: + logging.error("Couldn't find activity for type %s" % bundle_id) + return + + handle = ActivityHandle(activity_id) + activityfactory.create(bundle_id, handle) + + def start_activity(self, activity_type): + activityfactory.create(activity_type) + + def start_activity_with_uri(self, activity_type, uri): + activityfactory.create_with_uri(activity_type, uri) + + def take_activity_screenshot(self): + if self._model.get_zoom_level() != shellmodel.ShellModel.ZOOM_ACTIVITY: + return + if self.get_frame().visible: + return + + home_model = self._model.get_home() + active_activity = home_model.get_active_activity() + if active_activity is not None: + service = active_activity.get_service() + if service is not None: + try: + service.TakeScreenshot(timeout=2.0) + except dbus.DBusException, e: + logging.debug('Error raised by TakeScreenshot(): %s', e) + + def set_zoom_level(self, level): + if level == self._model.get_zoom_level(): + logging.debug('Already in the level %r' % level) + return + + if level == shellmodel.ShellModel.ZOOM_ACTIVITY: + host = self.get_current_activity() + if host is None: + raise ValueError('No current activity') + host.present() + else: + self._model.set_zoom_level(level) + self._screen.toggle_showing_desktop(True) + + def toggle_activity_fullscreen(self): + if self._model.get_zoom_level() == shellmodel.ShellModel.ZOOM_ACTIVITY: + self.get_current_activity().toggle_fullscreen() + + def activate_previous_activity(self): + home_model = self._model.get_home() + previous_activity = home_model.get_previous_activity() + if previous_activity: + previous_activity.get_window().activate( + gtk.get_current_event_time()) + + def activate_next_activity(self): + home_model = self._model.get_home() + next_activity = home_model.get_next_activity() + if next_activity: + next_activity.get_window().activate(gtk.get_current_event_time()) + + def close_current_activity(self): + if self._model.get_zoom_level() != shellmodel.ShellModel.ZOOM_ACTIVITY: + return + + home_model = self._model.get_home() + active_activity = home_model.get_active_activity() + if active_activity.is_journal(): + return + + self.get_current_activity().close() + + def get_current_activity(self): + home_model = self._model.get_home() + active_activity = home_model.get_active_activity() + return self._get_host_from_activity_model(active_activity) + + def get_activity(self, activity_id): + for host in self._hosts.values(): + if host.get_id() == activity_id: + return host + return None + + def take_screenshot(self): + file_path = os.path.join(tempfile.gettempdir(), '%i' % time.time()) + + window = gtk.gdk.get_default_root_window() + width, height = window.get_size() + x_orig, y_orig = window.get_origin() + + screenshot = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=False, + bits_per_sample=8, width=width, + height=height) + screenshot.get_from_drawable(window, window.get_colormap(), x_orig, + y_orig, 0, 0, width, height) + screenshot.save(file_path, "png") + jobject = datastore.create() + try: + jobject.metadata['title'] = _('Screenshot') + jobject.metadata['keep'] = '0' + jobject.metadata['buddies'] = '' + jobject.metadata['preview'] = '' + jobject.metadata['icon-color'] = profile.get_color().to_string() + jobject.metadata['mime_type'] = 'image/png' + jobject.file_path = file_path + datastore.write(jobject, transfer_ownership=True) + finally: + jobject.destroy() + del jobject + +_instance = None + +def get_instance(): + global _instance + if not _instance: + _instance = Shell() + return _instance + diff --git a/src/jarabe/view/__init__.py b/src/jarabe/view/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/view/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/view/clipboardicon.py b/src/jarabe/view/clipboardicon.py new file mode 100644 index 0000000..bd5d00e --- /dev/null +++ b/src/jarabe/view/clipboardicon.py @@ -0,0 +1,142 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import gtk + +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar import profile + +from jarabe.model import clipboard +from jarabe.view.clipboardmenu import ClipboardMenu +from jarabe.view.frame.frameinvoker import FrameWidgetInvoker +from jarabe.view.frame.notification import NotificationIcon +import view.frame.frame + +class ClipboardIcon(RadioToolButton): + __gtype_name__ = 'SugarClipboardIcon' + + def __init__(self, cb_object, group): + RadioToolButton.__init__(self, group=group) + self._cb_object = cb_object + self.owns_clipboard = False + self.props.sensitive = False + self.props.active = False + self._notif_icon = None + self._current_percent = None + + self._icon = Icon() + self._icon.props.xo_color = profile.get_color() + self.set_icon_widget(self._icon) + self._icon.show() + + cb_service = clipboard.get_instance() + cb_service.connect('object-state-changed', + self._object_state_changed_cb) + + self.palette = ClipboardMenu(cb_object) + self.palette.props.invoker = FrameWidgetInvoker(self) + + child = self.get_child() + child.connect('drag_data_get', self._drag_data_get_cb) + self.connect('notify::active', self._notify_active_cb) + + def get_object_id(self): + return self._cb_object.get_id() + + def _drag_data_get_cb(self, widget, context, selection, target_type, + event_time): + logging.debug('_drag_data_get_cb: requested target ' + selection.target) + data = self._cb_object.get_formats()[selection.target].get_data() + selection.set(selection.target, 8, data) + + def _put_in_clipboard(self): + logging.debug('ClipboardIcon._put_in_clipboard') + + if self._cb_object.get_percent() < 100: + raise ValueError('Object is not complete,' \ + ' cannot be put into the clipboard.') + + targets = self._get_targets() + if targets: + x_clipboard = gtk.Clipboard() + if not x_clipboard.set_with_data(targets, + self._clipboard_data_get_cb, + self._clipboard_clear_cb, + targets): + logging.error('GtkClipboard.set_with_data failed!') + else: + self.owns_clipboard = True + + def _clipboard_data_get_cb(self, x_clipboard, selection, info, targets): + if not selection.target in [target[0] for target in targets]: + logging.warning('ClipboardIcon._clipboard_data_get_cb: asked %s' \ + ' but only have %r.' % (selection.target, targets)) + return + data = self._cb_object.get_formats()[selection.target].get_data() + selection.set(selection.target, 8, data) + + def _clipboard_clear_cb(self, x_clipboard, targets): + logging.debug('ClipboardIcon._clipboard_clear_cb') + self.owns_clipboard = False + + def _object_state_changed_cb(self, cb_service, cb_object): + if cb_object != self._cb_object: + return + + if cb_object.get_icon(): + self._icon.props.icon_name = cb_object.get_icon() + else: + self._icon.props.icon_name = 'application-octet-stream' + + child = self.get_child() + child.drag_source_set(gtk.gdk.BUTTON1_MASK, + self._get_targets(), + gtk.gdk.ACTION_COPY) + child.drag_source_set_icon_name(self._icon.props.icon_name) + + if cb_object.get_percent() == 100: + self.props.sensitive = True + + # Clipboard object became complete. Make it the active one. + if self._current_percent < 100 and cb_object.get_percent() == 100: + self.props.active = True + + self._notif_icon = NotificationIcon() + self._notif_icon.props.icon_name = self._icon.props.icon_name + self._notif_icon.props.xo_color = \ + XoColor('%s,%s' % (self._icon.props.stroke_color, + self._icon.props.fill_color)) + frame = view.frame.frame.get_instance() + frame.add_notification(self._notif_icon, + view.frame.frame.BOTTOM_LEFT) + self._current_percent = cb_object.get_percent() + + def _notify_active_cb(self, widget, pspec): + if self.props.active: + self._put_in_clipboard() + else: + self.owns_clipboard = False + + def _get_targets(self): + targets = [] + for format_type in self._cb_object.get_formats().keys(): + targets.append((format_type, 0, 0)) + return targets + diff --git a/src/jarabe/view/clipboardmenu.py b/src/jarabe/view/clipboardmenu.py new file mode 100644 index 0000000..cacdef0 --- /dev/null +++ b/src/jarabe/view/clipboardmenu.py @@ -0,0 +1,238 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import tempfile +import urlparse +import os +import logging + +import gtk + +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.datastore import datastore +from sugar import mime +from sugar import profile +from sugar import activity + +from jarabe.model import clipboard +import journal.misc + +class ClipboardMenu(Palette): + + def __init__(self, cb_object): + Palette.__init__(self, cb_object.get_name()) + + self._cb_object = cb_object + + self.set_group_id('frame') + + cb_service = clipboard.get_instance() + cb_service.connect('object-state-changed', + self._object_state_changed_cb) + + self._progress_bar = None + + self._remove_item = MenuItem(_('Remove'), 'list-remove') + self._remove_item.connect('activate', self._remove_item_activate_cb) + self.menu.append(self._remove_item) + self._remove_item.show() + + self._open_item = MenuItem(_('Open'), 'zoom-activity') + self._open_item.connect('activate', self._open_item_activate_cb) + self.menu.append(self._open_item) + self._open_item.show() + + self._journal_item = MenuItem(_('Keep')) + icon = Icon(icon_name='document-save', icon_size=gtk.ICON_SIZE_MENU, + xo_color=profile.get_color()) + self._journal_item.set_image(icon) + + self._journal_item.connect('activate', self._journal_item_activate_cb) + self.menu.append(self._journal_item) + self._journal_item.show() + + self._update_items_visibility() + self._update_open_submenu() + + def _update_open_submenu(self): + activities = self._get_activities() + logging.debug('_update_open_submenu: %r' % activities) + child = self._open_item.get_child() + if activities is None or len(activities) <= 1: + child.set_text(_('Open')) + if self._open_item.get_submenu() is not None: + self._open_item.remove_submenu() + return + + child.set_text(_('Open with')) + submenu = self._open_item.get_submenu() + if submenu is None: + submenu = gtk.Menu() + self._open_item.set_submenu(submenu) + submenu.show() + else: + for item in submenu.get_children(): + submenu.remove(item) + + for service_name in activities: + registry = activity.get_registry() + activity_info = registry.get_activity(service_name) + + if not activity_info: + logging.warning('Activity %s is unknown.' % service_name) + + item = gtk.MenuItem(activity_info.name) + item.connect('activate', self._open_submenu_item_activate_cb, + service_name) + submenu.append(item) + item.show() + + def _update_items_visibility(self): + activities = self._get_activities() + installable = self._cb_object.is_bundle() + percent = self._cb_object.get_percent() + + if percent == 100 and (activities or installable): + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = True + self._journal_item.props.sensitive = True + elif percent == 100 and (not activities and not installable): + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = False + self._journal_item.props.sensitive = True + else: + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = False + self._journal_item.props.sensitive = False + + self._update_progress_bar() + + def _get_activities(self): + mime_type = self._cb_object.get_mime_type() + if not mime_type: + return '' + + registry = activity.get_registry() + activities = registry.get_activities_for_type(mime_type) + if activities: + return [activity_info.bundle_id for activity_info in activities] + else: + return '' + + def _update_progress_bar(self): + percent = self._cb_object.get_percent() + if percent == 100.0: + if self._progress_bar: + self._progress_bar = None + self.set_content(None) + else: + if self._progress_bar is None: + self._progress_bar = gtk.ProgressBar() + self._progress_bar.show() + self.set_content(self._progress_bar) + + self._progress_bar.props.fraction = percent / 100.0 + self._progress_bar.props.text = '%.2f %%' % percent + + def _object_state_changed_cb(self, cb_service, cb_object): + if cb_object != self._cb_object: + return + self.set_primary_text(cb_object.get_name()) + self._update_progress_bar() + self._update_items_visibility() + self._update_open_submenu() + + def _open_item_activate_cb(self, menu_item): + logging.debug('_open_item_activate_cb') + percent = self._cb_object.get_percent() + if percent < 100 or menu_item.get_submenu() is not None: + return + jobject = self._copy_to_journal() + journal.misc.resume(jobject, self._activities[0]) + jobject.destroy() + + def _open_submenu_item_activate_cb(self, menu_item, service_name): + logging.debug('_open_submenu_item_activate_cb') + percent = self._cb_object.get_percent() + if percent < 100: + return + jobject = self._copy_to_journal() + journal.misc.resume(jobject, service_name) + jobject.destroy() + + def _remove_item_activate_cb(self, menu_item): + cb_service = clipboard.get_instance() + cb_service.delete_object(self._cb_object.get_id()) + + def _journal_item_activate_cb(self, menu_item): + logging.debug('_journal_item_activate_cb') + jobject = self._copy_to_journal() + jobject.destroy() + + def _write_to_temp_file(self, data): + f, file_path = tempfile.mkstemp() + try: + os.write(f, data) + finally: + os.close(f) + return file_path + + def _copy_to_journal(self): + formats = self._cb_object.get_formats().keys() + most_significant_mime_type = mime.choose_most_significant(formats) + format = self._cb_object.get_formats()[most_significant_mime_type] + + transfer_ownership = False + if most_significant_mime_type == 'text/uri-list': + uris = mime.split_uri_list(format.get_data()) + if len(uris) == 1 and uris[0].startswith('file://'): + file_path = urlparse.urlparse(uris[0]).path + transfer_ownership = False + mime_type = mime.get_for_file(file_path) + else: + file_path = self._write_to_temp_file(format.get_data()) + transfer_ownership = True + mime_type = 'text/uri-list' + else: + if format.is_on_disk(): + file_path = urlparse.urlparse(format.get_data()).path + transfer_ownership = False + mime_type = mime.get_for_file(file_path) + else: + file_path = self._write_to_temp_file(format.get_data()) + transfer_ownership = True + sniffed_mime_type = mime.get_for_file(file_path) + if sniffed_mime_type == 'application/octet-stream': + mime_type = most_significant_mime_type + else: + mime_type = sniffed_mime_type + + name = self._cb_object.get_name() + jobject = datastore.create() + jobject.metadata['title'] = _('%s clipping') % name + jobject.metadata['keep'] = '0' + jobject.metadata['buddies'] = '' + jobject.metadata['preview'] = '' + jobject.metadata['icon-color'] = profile.get_color().to_string() + jobject.metadata['mime_type'] = mime_type + jobject.file_path = file_path + datastore.write(jobject, transfer_ownership=transfer_ownership) + + return jobject + diff --git a/src/jarabe/view/devices/Makefile.am b/src/jarabe/view/devices/Makefile.am new file mode 100644 index 0000000..dd71ce6 --- /dev/null +++ b/src/jarabe/view/devices/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS = network + +sugardir = $(pythondir)/jarabe/view/devices +sugar_PYTHON = \ + __init__.py \ + battery.py \ + deviceview.py \ + speaker.py + diff --git a/src/jarabe/view/devices/__init__.py b/src/jarabe/view/devices/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/view/devices/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/view/devices/battery.py b/src/jarabe/view/devices/battery.py new file mode 100644 index 0000000..90137ea --- /dev/null +++ b/src/jarabe/view/devices/battery.py @@ -0,0 +1,130 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +import gtk + +from sugar import profile +from sugar.graphics import style +from sugar.graphics.icon import get_icon_state +from sugar.graphics.tray import TrayIcon +from sugar.graphics.palette import Palette +from sugar.graphics.xocolor import XoColor + +from jarabe.view.frame.frameinvoker import FrameWidgetInvoker + +_ICON_NAME = 'battery' + +_STATUS_CHARGING = 0 +_STATUS_DISCHARGING = 1 +_STATUS_FULLY_CHARGED = 2 + +class DeviceView(TrayIcon): + + FRAME_POSITION_RELATIVE = 1000 + + def __init__(self, model): + TrayIcon.__init__(self, icon_name=_ICON_NAME, + xo_color=profile.get_color()) + + self._model = model + self.palette = BatteryPalette(_('My Battery')) + self.set_palette(self.palette) + self.palette.props.invoker = FrameWidgetInvoker(self) + self.palette.set_group_id('frame') + + model.connect('notify::level', self._battery_status_changed_cb) + model.connect('notify::charging', self._battery_status_changed_cb) + model.connect('notify::discharging', self._battery_status_changed_cb) + self._update_info() + + def _update_info(self): + name = _ICON_NAME + current_level = self._model.props.level + xo_color = profile.get_color() + badge_name = None + + if self._model.props.charging: + status = _STATUS_CHARGING + name += '-charging' + xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), + style.COLOR_WHITE.get_svg())) + elif self._model.props.discharging: + status = _STATUS_DISCHARGING + if current_level <= 15: + badge_name = 'emblem-warning' + else: + status = _STATUS_FULLY_CHARGED + + self.icon.props.icon_name = get_icon_state(name, current_level, step=-5) + self.icon.props.xo_color = xo_color + self.icon.props.badge_name = badge_name + + self.palette.set_level(current_level) + self.palette.set_status(status) + + def _battery_status_changed_cb(self, pspec, param): + self._update_info() + +class BatteryPalette(Palette): + + def __init__(self, primary_text): + Palette.__init__(self, primary_text) + + self._level = 0 + self._progress_bar = gtk.ProgressBar() + self._progress_bar.set_size_request( + style.zoom(style.GRID_CELL_SIZE * 4), -1) + self._progress_bar.show() + self._status_label = gtk.Label() + self._status_label.show() + + vbox = gtk.VBox() + vbox.pack_start(self._progress_bar) + vbox.pack_start(self._status_label) + vbox.show() + + self.set_content(vbox) + + def set_level(self, percent): + self._level = percent + fraction = percent / 100.0 + self._progress_bar.set_fraction(fraction) + + def set_status(self, status): + current_level = self._level + secondary_text = '' + status_text = '%s%%' % current_level + + if status == _STATUS_CHARGING: + secondary_text = _('Charging') + elif status == _STATUS_DISCHARGING: + if current_level <= 15: + secondary_text = _('Very little power remaining') + else: + #TODO: make this less of an wild/educated guess + minutes_remaining = int(current_level / 0.59) + remaining_hourpart = minutes_remaining / 60 + remaining_minpart = minutes_remaining % 60 + secondary_text = _('%(hour)d:%(min).2d remaining' + % { 'hour': remaining_hourpart, + 'min': remaining_minpart}) + else: + secondary_text = _('Charged') + + self.props.secondary_text = secondary_text + self._status_label.set_text(status_text) diff --git a/src/jarabe/view/devices/deviceview.py b/src/jarabe/view/devices/deviceview.py new file mode 100644 index 0000000..90ebbf5 --- /dev/null +++ b/src/jarabe/view/devices/deviceview.py @@ -0,0 +1,26 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +def create(model): + name = 'view.devices.' + model.get_type() + + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + + return mod.DeviceView(model) + diff --git a/src/jarabe/view/devices/network/Makefile.am b/src/jarabe/view/devices/network/Makefile.am new file mode 100644 index 0000000..433bbfd --- /dev/null +++ b/src/jarabe/view/devices/network/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pythondir)/jarabe/view/devices/network +sugar_PYTHON = \ + __init__.py \ + mesh.py \ + wireless.py diff --git a/src/jarabe/view/devices/network/__init__.py b/src/jarabe/view/devices/network/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/view/devices/network/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/view/devices/network/mesh.py b/src/jarabe/view/devices/network/mesh.py new file mode 100644 index 0000000..0a945ea --- /dev/null +++ b/src/jarabe/view/devices/network/mesh.py @@ -0,0 +1,131 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +import gtk + +from sugar import profile +from sugar.graphics.tray import TrayIcon +from sugar.graphics import style +from sugar.graphics.palette import Palette + +from jarabe.model.devices import device +from jarabe.model.devices.network import wireless +from jarabe.hardware import hardwaremanager +from jarabe.view.frame.frameinvoker import FrameWidgetInvoker + +class DeviceView(TrayIcon): + + FRAME_POSITION_RELATIVE = 400 + + def __init__(self, model): + TrayIcon.__init__(self, icon_name='network-mesh') + + self._model = model + + self.palette = MeshPalette(_("Mesh Network"), model) + self.set_palette(self.palette) + self.palette.props.invoker = FrameWidgetInvoker(self) + self.palette.set_group_id('frame') + + model.connect('notify::state', self._state_changed_cb) + model.connect('notify::activation-stage', self._state_changed_cb) + self._update_state() + + def _state_changed_cb(self, model, pspec): + self._update_state() + + def _update_state(self): + # FIXME Change icon colors once we have real icons + state = self._model.props.state + self.palette.update_state(state) + + if state == device.STATE_ACTIVATING: + self.icon.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg() + self.icon.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg() + elif state == device.STATE_ACTIVATED: + self.icon.props.xo_color = profile.get_color() + elif state == device.STATE_INACTIVE: + self.icon.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg() + self.icon.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg() + + if state == device.STATE_INACTIVE: + self.palette.set_primary_text(_("Mesh Network")) + else: + chan = wireless.freq_to_channel(self._model.props.frequency) + if chan > 0: + self.palette.set_primary_text(_("Mesh Network") + " %d" % chan) + self.palette.set_mesh_step(self._model.props.mesh_step, state) + +class MeshPalette(Palette): + def __init__(self, primary_text, model): + Palette.__init__(self, primary_text, menu_after_content=True) + self._model = model + + self._step_label = gtk.Label() + self._step_label.show() + + vbox = gtk.VBox() + vbox.pack_start(self._step_label) + vbox.show() + + self.set_content(vbox) + + self._disconnect_item = gtk.MenuItem(_('Disconnect...')) + self._disconnect_item.connect('activate', self._disconnect_activate_cb) + self.menu.append(self._disconnect_item) + + def update_state(self, state): + if state == device.STATE_ACTIVATED: + self._disconnect_item.show() + else: + self._disconnect_item.hide() + + def _disconnect_activate_cb(self, menuitem): + # Disconnection for an mesh means activating the default mesh device + # again without a channel + network_manager = hardwaremanager.get_network_manager() + nm_device = self._model.get_nm_device() + if network_manager and nm_device: + network_manager.set_active_device(nm_device) + + def set_mesh_step(self, step, state): + label = "" + if step == 1: + if state == device.STATE_ACTIVATED: + label = _("Connected to a School Mesh Portal") + elif state == device.STATE_ACTIVATING: + label = _("Looking for a School Mesh Portal...") + elif step == 3: + if state == device.STATE_ACTIVATED: + label = _("Connected to an XO Mesh Portal") + elif state == device.STATE_ACTIVATING: + label = _("Looking for an XO Mesh Portal...") + elif step == 4: + if state == device.STATE_ACTIVATED: + label = _("Connected to a Simple Mesh") + elif state == device.STATE_ACTIVATING: + label = _("Starting a Simple Mesh") + + if len(label): + self._step_label.set_text(label) + else: + import logging + logging.debug("Unhandled mesh step %d" % step) + self._step_label.set_text(_("Unknown Mesh")) + diff --git a/src/jarabe/view/devices/network/wireless.py b/src/jarabe/view/devices/network/wireless.py new file mode 100644 index 0000000..5db3ae2 --- /dev/null +++ b/src/jarabe/view/devices/network/wireless.py @@ -0,0 +1,144 @@ +# +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +import gtk + +from sugar.graphics.icon import get_icon_state +from sugar.graphics.tray import TrayIcon +from sugar.graphics import style +from sugar.graphics.palette import Palette + +from jarabe.model.devices.network import wireless +from jarabe.model.devices import device +from jarabe.hardware import hardwaremanager +from jarabe.hardware import nmclient +from jarabe.view.frame.frameinvoker import FrameWidgetInvoker + +_ICON_NAME = 'network-wireless' + +class DeviceView(TrayIcon): + + FRAME_POSITION_RELATIVE = 300 + + def __init__(self, model): + TrayIcon.__init__(self, icon_name=_ICON_NAME) + self._model = model + + meshdev = None + network_manager = hardwaremanager.get_network_manager() + for dev in network_manager.get_devices(): + if dev.get_type() == nmclient.DEVICE_TYPE_802_11_MESH_OLPC: + meshdev = dev + break + + self._counter = 0 + self.palette = WirelessPalette(self._get_palette_primary_text(), + meshdev) + self.set_palette(self.palette) + self.palette.props.invoker = FrameWidgetInvoker(self) + self.palette.set_group_id('frame') + self.palette.set_frequency(self._model.props.frequency) + + model.connect('notify::name', self._name_changed_cb) + model.connect('notify::strength', self._strength_changed_cb) + model.connect('notify::state', self._state_changed_cb) + + self._update_icon() + self._update_state() + + def _get_palette_primary_text(self): + if self._model.props.state == device.STATE_INACTIVE: + return _("Disconnected") + return self._model.props.name + + def _strength_changed_cb(self, model, pspec): + self._update_icon() + # Only update frequency periodically + if self._counter % 4 == 0: + self.palette.set_frequency(self._model.props.frequency) + self._counter += 1 + + def _name_changed_cb(self, model, pspec): + self.palette.set_primary_text(self._get_palette_primary_text()) + + def _state_changed_cb(self, model, pspec): + self._update_icon() + self._update_state() + self.palette.set_primary_text(self._get_palette_primary_text()) + + def _update_icon(self): + # keep this code in sync with view/home/MeshBox.py + strength = self._model.props.strength + if self._model.props.state == device.STATE_INACTIVE: + strength = 0 + if self._model.props.state == device.STATE_ACTIVATED: + icon_name = '%s-connected' % _ICON_NAME + else: + icon_name = _ICON_NAME + icon_name = get_icon_state(icon_name, strength) + if icon_name: + self.icon.props.icon_name = icon_name + + def _update_state(self): + # FIXME Change icon colors once we have real icons + state = self._model.props.state + if state == device.STATE_ACTIVATING: + self.icon.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg() + self.icon.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg() + elif state == device.STATE_ACTIVATED: + (stroke, fill) = self._model.get_active_network_colors() + self.icon.props.stroke_color = stroke + self.icon.props.fill_color = fill + elif state == device.STATE_INACTIVE: + self.icon.props.fill_color = style.COLOR_INACTIVE_FILL.get_svg() + self.icon.props.stroke_color = style.COLOR_INACTIVE_STROKE.get_svg() + +class WirelessPalette(Palette): + def __init__(self, primary_text, meshdev): + Palette.__init__(self, primary_text, menu_after_content=True) + self._meshdev = meshdev + + self._chan_label = gtk.Label() + self._chan_label.show() + + vbox = gtk.VBox() + vbox.pack_start(self._chan_label) + vbox.show() + + if meshdev: + disconnect_item = gtk.MenuItem(_('Disconnect...')) + disconnect_item.connect('activate', self._disconnect_activate_cb) + self.menu.append(disconnect_item) + disconnect_item.show() + + self.set_content(vbox) + + def _disconnect_activate_cb(self, menuitem): + # Disconnection for an AP means activating the default mesh device + network_manager = hardwaremanager.get_network_manager() + if network_manager and self._meshdev: + network_manager.set_active_device(self._meshdev) + + def set_frequency(self, freq): + try: + chan = wireless.freq_to_channel(freq) + except KeyError: + chan = 0 + self._chan_label.set_text("%s: %d" % (_("Channel"), chan)) + diff --git a/src/jarabe/view/devices/speaker.py b/src/jarabe/view/devices/speaker.py new file mode 100644 index 0000000..3c907e8 --- /dev/null +++ b/src/jarabe/view/devices/speaker.py @@ -0,0 +1,160 @@ +# Copyright (C) 2008 Martin Dengler +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +import gtk + +from jarabe.hardware import hardwaremanager +from sugar import profile +from sugar.graphics import style +from sugar.graphics.icon import get_icon_state, Icon +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.tray import TrayIcon +from sugar.graphics.palette import Palette +from sugar.graphics.xocolor import XoColor + +from jarabe.view.frame.frameinvoker import FrameWidgetInvoker + +_ICON_NAME = 'speaker' + +class DeviceView(TrayIcon): + + FRAME_POSITION_RELATIVE = 800 + + def __init__(self, model): + TrayIcon.__init__(self, + icon_name=_ICON_NAME, + xo_color=profile.get_color()) + + self._model = model + self.palette = SpeakerPalette(_('My Speakers'), model=model) + self.palette.props.invoker = FrameWidgetInvoker(self) + self.palette.set_group_id('frame') + + model.connect('notify::level', self.__speaker_status_changed_cb) + model.connect('notify::muted', self.__speaker_status_changed_cb) + self.connect('expose-event', self.__expose_event_cb) + + self._icon_widget.connect('button-press-event', + self.__update_mute_status) + + self._update_info() + + def _update_info(self): + name = _ICON_NAME + current_level = self._model.props.level + xo_color = profile.get_color() + + if self._model.props.muted: + name += '-muted' + xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), + style.COLOR_WHITE.get_svg())) + + self.icon.props.icon_name = get_icon_state(name, current_level, step=-1) + self.icon.props.xo_color = xo_color + + def __update_mute_status(self, *args): + self._model.props.muted = not self._model.props.muted + + def __expose_event_cb(self, *args): + self._update_info() + + def __speaker_status_changed_cb(self, pspec_, param_): + self._update_info() + +class SpeakerPalette(Palette): + + def __init__(self, primary_text, model): + Palette.__init__(self, label=primary_text) + + self._model = model + + self.set_size_request(style.zoom(style.GRID_CELL_SIZE * 4), -1) + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + vol_step = hardwaremanager.VOL_CHANGE_INCREMENT_RECOMMENDATION + self._adjustment = gtk.Adjustment(value=self._model.props.level, + lower=0, + upper=100 + vol_step, + step_incr=vol_step, + page_incr=vol_step, + page_size=vol_step) + self._hscale = gtk.HScale(self._adjustment) + self._hscale.set_digits(0) + self._hscale.set_draw_value(False) + vbox.add(self._hscale) + self._hscale.show() + + self._mute_item = MenuItem('') + self._mute_icon = Icon(icon_size=gtk.ICON_SIZE_MENU) + self._mute_item.set_image(self._mute_icon) + self.menu.append(self._mute_item) + self._mute_item.show() + + self._adjustment_handler_id = \ + self._adjustment.connect('value_changed', + self.__adjustment_changed_cb) + + self._model_notify_level_handler_id = \ + self._model.connect('notify::level', self.__level_changed_cb) + self._model.connect('notify::muted', self.__muted_changed_cb) + + self._mute_item.connect('activate', self.__mute_activate_cb) + + self.connect('popup', self.__popup_cb) + + def _update_muted(self): + if self._model.props.muted: + mute_item_text = _('Unmute') + mute_item_icon_name = 'dialog-ok' + else: + mute_item_text = _('Mute') + mute_item_icon_name = 'dialog-cancel' + self._mute_item.get_child().set_text(mute_item_text) + self._mute_icon.props.icon_name = mute_item_icon_name + + def _update_level(self): + if self._adjustment.value != self._model.props.level: + self._adjustment.handler_block(self._adjustment_handler_id) + try: + self._adjustment.value = self._model.props.level + finally: + self._adjustment.handler_unblock(self._adjustment_handler_id) + + def __adjustment_changed_cb(self, adj_): + self._model.handler_block(self._model_notify_level_handler_id) + try: + self._model.props.level = self._adjustment.value + finally: + self._model.handler_unblock(self._model_notify_level_handler_id) + self._model.props.muted = self._adjustment.value == 0 + + def __level_changed_cb(self, pspec_, param_): + self._update_level() + + def __mute_activate_cb(self, menuitem_): + self._model.props.muted = not self._model.props.muted + + def __muted_changed_cb(self, pspec_, param_): + self._update_muted() + + def __popup_cb(self, palette_): + self._update_level() + self._update_muted() diff --git a/src/jarabe/view/frame/Makefile.am b/src/jarabe/view/frame/Makefile.am new file mode 100644 index 0000000..4e6e237 --- /dev/null +++ b/src/jarabe/view/frame/Makefile.am @@ -0,0 +1,14 @@ +sugardir = $(pythondir)/jarabe/view/frame +sugar_PYTHON = \ + __init__.py \ + activitiestray.py \ + clipboardpanelwindow.py \ + clipboardtray.py \ + devicestray.py \ + frameinvoker.py \ + friendstray.py \ + eventarea.py \ + frame.py \ + notification.py \ + framewindow.py \ + zoomtoolbar.py diff --git a/src/jarabe/view/frame/__init__.py b/src/jarabe/view/frame/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/view/frame/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/view/frame/activitiestray.py b/src/jarabe/view/frame/activitiestray.py new file mode 100644 index 0000000..735fa96 --- /dev/null +++ b/src/jarabe/view/frame/activitiestray.py @@ -0,0 +1,395 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import gtk + +from sugar.graphics import style +from sugar.graphics.tray import HTray +from sugar.graphics.xocolor import XoColor +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.icon import Icon +from sugar.graphics.palette import Palette, WidgetInvoker +from sugar.graphics.menuitem import MenuItem +from sugar import activity +from sugar import profile + +from jarabe.model import shellmodel +from jarabe.view.palettes import JournalPalette, CurrentActivityPalette +from jarabe.view.pulsingicon import PulsingIcon +from jarabe.view.frame.frameinvoker import FrameWidgetInvoker +from jarabe.view.frame.notification import NotificationIcon +import view.frame.frame + +class ActivityButton(RadioToolButton): + def __init__(self, home_activity, group): + RadioToolButton.__init__(self, group=group) + + self._home_activity = home_activity + + self._icon = PulsingIcon() + self._icon.props.base_color = home_activity.get_icon_color() + self._icon.props.pulse_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TOOLBAR_GREY.get_svg())) + if home_activity.get_icon_path(): + self._icon.props.file = home_activity.get_icon_path() + else: + self._icon.props.icon_name = 'image-missing' + self.set_icon_widget(self._icon) + self._icon.show() + + if home_activity.props.launching: + self._icon.props.pulsing = True + self._notify_launching_hid = home_activity.connect( \ + 'notify::launching', self.__notify_launching_cb) + else: + self._notify_launching_hid = None + self._notif_icon = None + + def create_palette(self): + if self._home_activity.is_journal(): + palette = JournalPalette(self._home_activity) + else: + palette = CurrentActivityPalette(self._home_activity) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + self.set_palette(palette) + + def __notify_launching_cb(self, home_activity, pspec): + if not home_activity.props.launching: + self._icon.props.pulsing = False + home_activity.disconnect(self._notify_launching_hid) + + +class BaseInviteButton(ToolButton): + def __init__(self, invite): + ToolButton.__init__(self) + self._invite = invite + self._icon = Icon() + self.connect('clicked', self.__clicked_cb) + self.connect('destroy', self.__destroy_cb) + self._notif_icon = NotificationIcon() + self._notif_icon.connect('button-release-event', + self.__button_release_event_cb) + + def __button_release_event_cb(self, icon, event): + self.emit('clicked') + + def __clicked_cb(self, button): + if self._notif_icon is not None: + frame = view.frame.frame.get_instance() + frame.remove_notification(self._notif_icon) + self._notif_icon = None + self._launch() + + def _launch(self): + """Launch the target of the invite""" + raise NotImplementedError + + def __destroy_cb(self, button): + frame = view.frame.frame.get_instance() + frame.remove_notification(self._notif_icon) + +class ActivityInviteButton(BaseInviteButton): + """Invite to shared activity""" + def __init__(self, invite): + BaseInviteButton.__init__(self, invite) + mesh = shellmodel.get_instance().get_mesh() + activity_model = mesh.get_activity(invite.get_activity_id()) + self._activity_model = activity_model + self._bundle_id = activity_model.get_bundle_id() + + self._icon.props.xo_color = activity_model.get_color() + if activity_model.get_icon_name(): + self._icon.props.file = activity_model.get_icon_name() + else: + self._icon.props.icon_name = 'image-missing' + self.set_icon_widget(self._icon) + self._icon.show() + + palette = ActivityInvitePalette(invite) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + self.set_palette(palette) + + self._notif_icon.props.xo_color = activity_model.get_color() + if activity_model.get_icon_name(): + icon_name = activity_model.get_icon_name() + self._notif_icon.props.icon_filename = icon_name + else: + self._notif_icon.props.icon_name = 'image-missing' + + palette = ActivityInvitePalette(invite) + palette.props.invoker = WidgetInvoker(self._notif_icon) + palette.set_group_id('frame') + self._notif_icon.palette = palette + + frame = view.frame.frame.get_instance() + frame.add_notification(self._notif_icon, + view.frame.frame.TOP_LEFT) + + def _launch(self): + """Join the activity in the invite.""" + shell = view.Shell.get_instance() + shell.join_activity(self._activity_model.get_bundle_id(), + self._activity_model.get_id()) + + +class PrivateInviteButton(BaseInviteButton): + """Invite to a private one to one channel""" + def __init__(self, invite): + BaseInviteButton.__init__(self, invite) + self._private_channel = invite.get_private_channel() + self._bundle_id = invite.get_bundle_id() + + self._icon.props.xo_color = profile.get_color() + registry = activity.get_registry() + activity_info = registry.get_activity(self._bundle_id) + if activity_info: + self._icon.props.file = activity_info.icon + else: + self._icon.props.icon_name = 'image-missing' + self.set_icon_widget(self._icon) + self._icon.show() + + palette = PrivateInvitePalette(invite) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + self.set_palette(palette) + + self._notif_icon.props.xo_color = profile.get_color() + registry = activity.get_registry() + activity_info = registry.get_activity(self._bundle_id) + if activity_info: + self._notif_icon.props.icon_filename = activity_info.icon + else: + self._notif_icon.props.icon_name = 'image-missing' + + palette = PrivateInvitePalette(invite) + palette.props.invoker = WidgetInvoker(self._notif_icon) + palette.set_group_id('frame') + self._notif_icon.palette = palette + + frame = view.frame.frame.get_instance() + frame.add_notification(self._notif_icon, + view.frame.frame.TOP_LEFT) + + def _launch(self): + """Start the activity with private channel.""" + shell = view.Shell.get_instance() + shell.start_activity_with_uri(self._bundle_id, + self._private_channel) + + +class BaseInvitePalette(Palette): + """Palette for frame or notification icon for invites.""" + def __init__(self): + Palette.__init__(self, '') + + menu_item = MenuItem(_('Join'), icon_name='dialog-ok') + menu_item.connect('activate', self.__join_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__decline_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __join_activate_cb(self, menu_item): + self._join() + + def __decline_activate_cb(self, menu_item): + self._decline() + + def _join(self): + raise NotImplementedError + + def _decline(self): + raise NotImplementedError + + +class ActivityInvitePalette(BaseInvitePalette): + """Palette for shared activity invites.""" + + def __init__(self, invite): + BaseInvitePalette.__init__(self) + + mesh = shellmodel.get_instance().get_mesh() + activity_model = mesh.get_activity(invite.get_activity_id()) + self._activity_model = activity_model + self._bundle_id = activity_model.get_bundle_id() + + registry = activity.get_registry() + activity_info = registry.get_activity(self._bundle_id) + if activity_info: + self.set_primary_text(activity_info.name) + else: + self.set_primary_text(self._bundle_id) + + def _join(self): + shell = view.Shell.get_instance() + shell.join_activity(self._activity_model.get_bundle_id(), + self._activity_model.get_id()) + + def _decline(self): + invites = shellmodel.get_instance().get_invites() + activity_id = self._activity_model.get_id() + invites.remove_activity(activity_id) + + +class PrivateInvitePalette(BaseInvitePalette): + """Palette for private channel invites.""" + + def __init__(self, invite): + BaseInvitePalette.__init__(self) + + self._private_channel = invite.get_private_channel() + self._bundle_id = invite.get_bundle_id() + + registry = activity.get_registry() + activity_info = registry.get_activity(self._bundle_id) + if activity_info: + self.set_primary_text(activity_info.name) + else: + self.set_primary_text(self._bundle_id) + + def _join(self): + shell = view.Shell.get_instance() + shell.start_activity_with_uri(self._bundle_id, + self._private_channel) + invites = shellmodel.get_instance().get_invites() + invites.remove_private_channel(self._private_channel) + + def _decline(self): + invites = shellmodel.get_instance().get_invites() + invites.remove_private_channel(self._private_channel) + + +class ActivitiesTray(HTray): + def __init__(self): + HTray.__init__(self) + + self._buttons = {} + self._invite_to_item = {} + self._freeze_button_clicks = False + + self._home_model = shellmodel.get_instance().get_home() + self._home_model.connect('activity-added', self.__activity_added_cb) + self._home_model.connect('activity-removed', self.__activity_removed_cb) + self._home_model.connect('active-activity-changed', + self.__activity_changed_cb) + self._home_model.connect('tabbing-activity-changed', + self.__tabbing_activity_changed_cb) + + self._invites = shellmodel.get_instance().get_invites() + for invite in self._invites: + self._add_invite(invite) + self._invites.connect('invite-added', self.__invite_added_cb) + self._invites.connect('invite-removed', self.__invite_removed_cb) + + def __activity_added_cb(self, home_model, home_activity): + logging.debug('__activity_added_cb: %r' % home_activity) + if self.get_children(): + group = self.get_children()[0] + else: + group = None + + button = ActivityButton(home_activity, group) + self.add_item(button) + self._buttons[home_activity.get_activity_id()] = button + button.connect('clicked', self.__activity_clicked_cb, home_activity) + button.show() + + def __activity_removed_cb(self, home_model, home_activity): + logging.debug('__activity_removed_cb: %r' % home_activity) + button = self._buttons[home_activity.get_activity_id()] + self.remove_item(button) + del self._buttons[home_activity.get_activity_id()] + + def _activate_activity(self, home_activity): + button = self._buttons[home_activity.get_activity_id()] + self._freeze_button_clicks = True + button.props.active = True + self._freeze_button_clicks = False + + self.scroll_to_item(button) + # Redraw immediately. + # The widget may not be realized yet, and then there is no window. + if self.window: + self.window.process_updates(True) + + def __activity_changed_cb(self, home_model, home_activity): + logging.debug('__activity_changed_cb: %r' % home_activity) + + # Only select the new activity, if there is no tabbing activity. + if home_model.get_tabbing_activity() is None: + self._activate_activity(home_activity) + + def __tabbing_activity_changed_cb(self, home_model, home_activity): + logging.debug('__tabbing_activity_changed_cb: %r' % home_activity) + # If the tabbing_activity is set to None just do nothing. + # The active activity will be updated a bit later (and it will + # be set to the activity that is currently selected). + if home_activity is None: + return + + self._activate_activity(home_activity) + + def __activity_clicked_cb(self, button, home_activity): + if not self._freeze_button_clicks and button.props.active: + logging.debug('ActivitiesTray.__activity_clicked_cb') + window = home_activity.get_window() + if window: + window.activate(gtk.get_current_event_time()) + + def __invite_clicked_cb(self, icon, invite): + if hasattr(invite, 'get_activity_id'): + self._invites.remove_invite(invite) + else: + self._invites.remove_private_invite(invite) + + def __invite_added_cb(self, invites, invite): + self._add_invite(invite) + + def __invite_removed_cb(self, invites, invite): + self._remove_invite(invite) + + def _add_invite(self, invite): + """Add an invite (SugarInvite or PrivateInvite)""" + item = None + if hasattr(invite, 'get_activity_id'): + mesh = shellmodel.get_instance().get_mesh() + activity_model = mesh.get_activity(invite.get_activity_id()) + if activity_model is not None: + item = ActivityInviteButton(invite) + else: + item = PrivateInviteButton(invite) + if item is not None: + item.connect('clicked', self.__invite_clicked_cb, invite) + self.add_item(item) + item.show() + self._invite_to_item[invite] = item + + def _remove_invite(self, invite): + self.remove_item(self._invite_to_item[invite]) + self._invite_to_item[invite].destroy() + del self._invite_to_item[invite] + diff --git a/src/jarabe/view/frame/clipboardpanelwindow.py b/src/jarabe/view/frame/clipboardpanelwindow.py new file mode 100644 index 0000000..bd1959e --- /dev/null +++ b/src/jarabe/view/frame/clipboardpanelwindow.py @@ -0,0 +1,102 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from urlparse import urlparse + +import gtk +import hippo + +from jarabe.view.frame.framewindow import FrameWindow +from jarabe.view.frame.clipboardtray import ClipboardTray + +from jarabe.model import clipboard + +class ClipboardPanelWindow(FrameWindow): + def __init__(self, frame, orientation): + FrameWindow.__init__(self, orientation) + + self._frame = frame + + # Listening for new clipboard objects + # NOTE: we need to keep a reference to gtk.Clipboard in order to keep + # listening to it. + self._clipboard = gtk.Clipboard() + self._clipboard.connect("owner-change", self._owner_change_cb) + + self._clipboard_tray = ClipboardTray() + canvas_widget = hippo.CanvasWidget(widget=self._clipboard_tray) + self.append(canvas_widget, hippo.PACK_EXPAND) + + # Receiving dnd drops + self.drag_dest_set(0, [], 0) + self.connect("drag_motion", self._clipboard_tray.drag_motion_cb) + self.connect("drag_drop", self._clipboard_tray.drag_drop_cb) + self.connect("drag_data_received", + self._clipboard_tray.drag_data_received_cb) + + def _owner_change_cb(self, x_clipboard, event): + logging.debug("owner_change_cb") + + if self._clipboard_tray.owns_clipboard(): + return + + cb_service = clipboard.get_instance() + key = cb_service.add_object(name="") + cb_service.set_object_percent(key, percent=0) + + targets = x_clipboard.wait_for_targets() + for target in targets: + if target not in ('TIMESTAMP', 'TARGETS', + 'MULTIPLE', 'SAVE_TARGETS'): + logging.debug('Asking for target %s.' % target) + selection = x_clipboard.wait_for_contents(target) + if not selection: + logging.warning('no data for selection target %s.' % target) + continue + self._add_selection(key, selection) + + cb_service.set_object_percent(key, percent=100) + + def _add_selection(self, key, selection): + if not selection.data: + logging.warning('no data for selection target %s.' % selection.type) + return + + logging.debug('adding type ' + selection.type + '.') + + cb_service = clipboard.get_instance() + if selection.type == 'text/uri-list': + uris = selection.get_uris() + + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list' \ + ' still not supported.') + uri = uris[0] + scheme, netloc_, path_, parameters_, query_, fragment_ = \ + urlparse(uri) + on_disk = (scheme == 'file') + + cb_service.add_object_format(key, + selection.type, + uri, + on_disk) + else: + cb_service.add_object_format(key, + selection.type, + selection.data, + on_disk=False) + diff --git a/src/jarabe/view/frame/clipboardtray.py b/src/jarabe/view/frame/clipboardtray.py new file mode 100644 index 0000000..ca67b82 --- /dev/null +++ b/src/jarabe/view/frame/clipboardtray.py @@ -0,0 +1,193 @@ +# Copyright (C) 2007, 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import tempfile + +import gtk + +from sugar import util +from sugar.graphics import tray +from sugar.graphics import style + +from jarabe.model import clipboard +from jarabe.view.clipboardicon import ClipboardIcon + +class _ContextMap(object): + """Maps a drag context to the clipboard object involved in the dragging.""" + def __init__(self): + self._context_map = {} + + def add_context(self, context, object_id, data_types): + """Establishes the mapping. data_types will serve us for reference- + counting this mapping. + """ + self._context_map[context] = [object_id, data_types] + + def get_object_id(self, context): + """Retrieves the object_id associated with context. + Will release the association when this function was called as many times + as the number of data_types that this clipboard object contains. + """ + [object_id, data_types_left] = self._context_map[context] + + data_types_left = data_types_left - 1 + if data_types_left == 0: + del self._context_map[context] + else: + self._context_map[context] = [object_id, data_types_left] + + return object_id + + def has_context(self, context): + return context in self._context_map + +class ClipboardTray(tray.VTray): + + MAX_ITEMS = gtk.gdk.screen_height() / style.GRID_CELL_SIZE - 2 + + def __init__(self): + tray.VTray.__init__(self, align=tray.ALIGN_TO_END) + self._icons = {} + self._context_map = _ContextMap() + + cb_service = clipboard.get_instance() + cb_service.connect('object-added', self._object_added_cb) + cb_service.connect('object-deleted', self._object_deleted_cb) + + def owns_clipboard(self): + for icon in self._icons.values(): + if icon.owns_clipboard: + return True + return False + + def _add_selection(self, object_id, selection): + if not selection.data: + return + + logging.debug('ClipboardTray: adding type %r' % selection.type) + + cb_service = clipboard.get_instance() + if selection.type == 'text/uri-list': + uris = selection.data.split('\n') + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list' \ + ' still not supported.') + + cb_service.add_object_format(object_id, + selection.type, + uris[0], + on_disk=True) + else: + cb_service.add_object_format(object_id, + selection.type, + selection.data, + on_disk=False) + + def _object_added_cb(self, cb_service, cb_object): + if self._icons: + group = self._icons.values()[0] + else: + group = None + + icon = ClipboardIcon(cb_object, group) + self.add_item(icon) + icon.show() + self._icons[cb_object.get_id()] = icon + + objects_to_delete = self.get_children()[:-self.MAX_ITEMS] + for icon in objects_to_delete: + logging.debug('ClipboardTray: deleting surplus object') + cb_service = clipboard.get_instance() + cb_service.delete_object(icon.get_object_id()) + + logging.debug('ClipboardTray: %r was added' % cb_object.get_id()) + + def _object_deleted_cb(self, cb_service, object_id): + icon = self._icons[object_id] + self.remove_item(icon) + del self._icons[object_id] + logging.debug('ClipboardTray: %r was deleted' % object_id) + + def drag_motion_cb(self, widget, context, x, y, time): + logging.debug('ClipboardTray._drag_motion_cb') + context.drag_status(gtk.gdk.ACTION_COPY, time) + return True + + def drag_drop_cb(self, widget, context, x, y, time): + logging.debug('ClipboardTray._drag_drop_cb') + cb_service = clipboard.get_instance() + object_id = cb_service.add_object(name="") + + self._context_map.add_context(context, object_id, len(context.targets)) + + if 'XdndDirectSave0' in context.targets: + window = context.source_window + prop_type, format, filename = \ + window.property_get('XdndDirectSave0','text/plain') + + # FIXME query the clipboard service for a filename? + base_dir = tempfile.gettempdir() + dest_filename = util.unique_id() + + name_, dot, extension = filename.rpartition('.') + dest_filename += dot + extension + + dest_uri = 'file://' + os.path.join(base_dir, dest_filename) + + window.property_change('XdndDirectSave0', prop_type, format, + gtk.gdk.PROP_MODE_REPLACE, dest_uri) + + widget.drag_get_data(context, 'XdndDirectSave0', time) + else: + for target in context.targets: + if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'): + widget.drag_get_data(context, target, time) + + cb_service.set_object_percent(object_id, percent=100) + + return True + + def drag_data_received_cb(self, widget, context, x, y, selection, + targetType, time): + logging.debug('ClipboardTray: got data for target %r' + % selection.target) + + object_id = self._context_map.get_object_id(context) + try: + if selection is None: + logging.warn('ClipboardTray: empty selection for target %s' + % selection.target) + elif selection.target == 'XdndDirectSave0': + if selection.data == 'S': + window = context.source_window + + prop_type, format_, dest = \ + window.property_get('XdndDirectSave0', 'text/plain') + + clipboardservice = clipboard.get_instance() + clipboardservice.add_object_format( \ + object_id, 'XdndDirectSave0', dest, on_disk=True) + else: + self._add_selection(object_id, selection) + + finally: + # If it's the last target to be processed, finish + # the dnd transaction + if not self._context_map.has_context(context): + context.drop_finish(True, gtk.get_current_event_time()) + diff --git a/src/jarabe/view/frame/devicestray.py b/src/jarabe/view/frame/devicestray.py new file mode 100644 index 0000000..eb6707e --- /dev/null +++ b/src/jarabe/view/frame/devicestray.py @@ -0,0 +1,66 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +from sugar.graphics import tray + +from jarabe.view.devices import deviceview +from jarabe.model import shellmodel + +_logger = logging.getLogger('DevicesTray') + +class DevicesTray(tray.HTray): + def __init__(self): + tray.HTray.__init__(self, align=tray.ALIGN_TO_END) + self._device_icons = {} + + devices_model = shellmodel.get_instance().get_devices() + + for device in devices_model: + self._add_device(device) + + devices_model.connect('device-appeared', + self.__device_appeared_cb) + devices_model.connect('device-disappeared', + self.__device_disappeared_cb) + + def _add_device(self, device): + try: + view = deviceview.create(device) + index = 0 + for item in self.get_children(): + index = self.get_item_index(item) + view_pos = getattr(view, "FRAME_POSITION_RELATIVE", -1) + item_pos = getattr(item, "FRAME_POSITION_RELATIVE", 0) + if view_pos < item_pos: + break + self.add_item(view, index=index) + view.show() + self._device_icons[device.get_id()] = view + except Exception, message: + _logger.warn("Not able to add icon for device [%r], because of " + "an error (%s). Continuing." % (device, message)) + + def _remove_device(self, device): + self.remove_item(self._device_icons[device.get_id()]) + del self._device_icons[device.get_id()] + + def __device_appeared_cb(self, model, device): + self._add_device(device) + + def __device_disappeared_cb(self, model, device): + self._remove_device(device) diff --git a/src/jarabe/view/frame/eventarea.py b/src/jarabe/view/frame/eventarea.py new file mode 100644 index 0000000..0eba150 --- /dev/null +++ b/src/jarabe/view/frame/eventarea.py @@ -0,0 +1,148 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +import wnck + +from sugar import profile + +_MAX_DELAY = 1000 + +class EventArea(gobject.GObject): + __gsignals__ = { + 'enter': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'leave': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._windows = [] + self._hover = False + self._sids = {} + pro = profile.get_profile() + self._hot_delay = int(pro.hot_corners_delay) + self._warm_delay = int(pro.warm_edges_delay) + + right = gtk.gdk.screen_width() - 1 + bottom = gtk.gdk.screen_height() -1 + width = gtk.gdk.screen_width() - 2 + height = gtk.gdk.screen_height() - 2 + + if self._warm_delay != _MAX_DELAY: + invisible = self._create_invisible(1, 0, width, 1, self._warm_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(1, bottom, width, 1, + self._warm_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(0, 1, 1, height, + self._warm_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, 1, 1, height, + self._warm_delay) + self._windows.append(invisible) + + if self._hot_delay != _MAX_DELAY: + invisible = self._create_invisible(0, 0, 1, 1, self._hot_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, 0, 1, 1, self._hot_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(0, bottom, 1, 1, self._hot_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, bottom, 1, 1, + self._hot_delay) + self._windows.append(invisible) + + screen = wnck.screen_get_default() + screen.connect('window-stacking-changed', + self._window_stacking_changed_cb) + + def _create_invisible(self, x, y, width, height, delay): + invisible = gtk.Invisible() + if delay >= 0: + invisible.connect('enter-notify-event', self._enter_notify_cb, + delay) + invisible.connect('leave-notify-event', self._leave_notify_cb) + + invisible.drag_dest_set(0, [], 0) + invisible.connect('drag_motion', self._drag_motion_cb) + invisible.connect('drag_leave', self._drag_leave_cb) + + invisible.realize() + invisible.window.set_events(gtk.gdk.POINTER_MOTION_MASK | + gtk.gdk.ENTER_NOTIFY_MASK | + gtk.gdk.LEAVE_NOTIFY_MASK) + invisible.window.move_resize(x, y, width, height) + + return invisible + + def _notify_enter(self): + if not self._hover: + self._hover = True + self.emit('enter') + + def _notify_leave(self): + if self._hover: + self._hover = False + self.emit('leave') + + def _enter_notify_cb(self, widget, event, delay): + if widget in self._sids: + gobject.source_remove(self._sids[widget]) + self._sids[widget] = gobject.timeout_add(delay, + self.__delay_cb, + widget) + + def __delay_cb(self, widget): + del self._sids[widget] + self._notify_enter() + return False + + def _leave_notify_cb(self, widget, event): + if widget in self._sids: + gobject.source_remove(self._sids[widget]) + del self._sids[widget] + self._notify_leave() + + def _drag_motion_cb(self, widget, drag_context, x, y, timestamp): + drag_context.drag_status(0, timestamp) + self._notify_enter() + return True + + def _drag_leave_cb(self, widget, drag_context, timestamp): + self._notify_leave() + return True + + def show(self): + for window in self._windows: + window.show() + + def hide(self): + for window in self._windows: + window.hide() + + def _window_stacking_changed_cb(self, screen): + for window in self._windows: + window.window.raise_() diff --git a/src/jarabe/view/frame/frame.py b/src/jarabe/view/frame/frame.py new file mode 100644 index 0000000..10dcf26 --- /dev/null +++ b/src/jarabe/view/frame/frame.py @@ -0,0 +1,327 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gtk +import gobject +import hippo + +from sugar.graphics import animator +from sugar.graphics import style +from sugar.graphics import palettegroup + +from jarabe.view.frame.eventarea import EventArea +from jarabe.view.frame.activitiestray import ActivitiesTray +from jarabe.view.frame.zoomtoolbar import ZoomToolbar +from jarabe.view.frame.friendstray import FriendsTray +from jarabe.view.frame.devicestray import DevicesTray +from jarabe.view.frame.framewindow import FrameWindow +from jarabe.view.frame.clipboardpanelwindow import ClipboardPanelWindow +from jarabe.view.frame.notification import NotificationIcon, NotificationWindow + +TOP_RIGHT = 0 +TOP_LEFT = 1 +BOTTOM_RIGHT = 2 +BOTTOM_LEFT = 3 + +_FRAME_HIDING_DELAY = 500 +_NOTIFICATION_DURATION = 5000 + +class _Animation(animator.Animation): + def __init__(self, frame, end): + start = frame.current_position + animator.Animation.__init__(self, start, end) + self._frame = frame + + def next_frame(self, current): + self._frame.move(current) + +class _MouseListener(object): + def __init__(self, frame): + self._frame = frame + self._hide_sid = 0 + + def mouse_enter(self): + self._show_frame() + + def mouse_leave(self): + if self._frame.mode == Frame.MODE_MOUSE: + self._hide_frame() + + def _show_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._frame.show(Frame.MODE_MOUSE) + + def _hide_frame_timeout_cb(self): + self._frame.hide() + return False + + def _hide_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._hide_sid = gobject.timeout_add( + _FRAME_HIDING_DELAY, self._hide_frame_timeout_cb) + +class _KeyListener(object): + def __init__(self, frame): + self._frame = frame + + def key_press(self): + if self._frame.visible: + if self._frame.mode == Frame.MODE_KEYBOARD: + self._frame.hide() + else: + self._frame.show(Frame.MODE_KEYBOARD) + +class Frame(object): + MODE_MOUSE = 0 + MODE_KEYBOARD = 1 + MODE_NON_INTERACTIVE = 2 + + def __init__(self): + self.mode = None + + self._palette_group = palettegroup.get_group('frame') + self._palette_group.connect('popdown', self._palette_group_popdown_cb) + + self._left_panel = None + self._right_panel = None + self._top_panel = None + self._bottom_panel = None + + self.current_position = 0.0 + self._animator = None + + self._event_area = EventArea() + self._event_area.connect('enter', self._enter_corner_cb) + self._event_area.show() + + self._top_panel = self._create_top_panel() + self._bottom_panel = self._create_bottom_panel() + self._left_panel = self._create_left_panel() + self._right_panel = self._create_right_panel() + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + self._key_listener = _KeyListener(self) + self._mouse_listener = _MouseListener(self) + + self._notif_by_icon = {} + + def is_visible(self): + return self.current_position != 0.0 + + def hide(self): + if self._animator: + self._animator.stop() + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 0.0)) + self._animator.start() + + self._event_area.show() + + self.mode = None + + def show(self, mode): + if self.visible: + return + if self._animator: + self._animator.stop() + + self.mode = mode + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 1.0)) + self._animator.start() + + self._event_area.hide() + + def move(self, pos): + self.current_position = pos + self._update_position() + + def _is_hover(self): + return (self._top_panel.hover or \ + self._bottom_panel.hover or \ + self._left_panel.hover or \ + self._right_panel.hover) + + def _create_top_panel(self): + panel = self._create_panel(gtk.POS_TOP) + + # TODO: setting box_width and hippo.PACK_EXPAND looks like a hack to me. + # Why hippo isn't respecting the request size of these controls? + + zoom_toolbar = ZoomToolbar() + panel.append(hippo.CanvasWidget(widget=zoom_toolbar, + box_width=4*style.GRID_CELL_SIZE)) + zoom_toolbar.show() + + activities_tray = ActivitiesTray() + panel.append(hippo.CanvasWidget(widget=activities_tray), + hippo.PACK_EXPAND) + activities_tray.show() + + return panel + + def _create_bottom_panel(self): + panel = self._create_panel(gtk.POS_BOTTOM) + + # TODO: same issue as in _create_top_panel() + devices_tray = DevicesTray() + panel.append(hippo.CanvasWidget(widget=devices_tray), hippo.PACK_EXPAND) + devices_tray.show() + + return panel + + def _create_right_panel(self): + panel = self._create_panel(gtk.POS_RIGHT) + + tray = FriendsTray() + panel.append(hippo.CanvasWidget(widget=tray), hippo.PACK_EXPAND) + tray.show() + + return panel + + def _create_left_panel(self): + panel = ClipboardPanelWindow(self, gtk.POS_LEFT) + + self._connect_to_panel(panel) + panel.connect('drag-motion', self._drag_motion_cb) + panel.connect('drag-leave', self._drag_leave_cb) + + return panel + + def _create_panel(self, orientation): + panel = FrameWindow(orientation) + self._connect_to_panel(panel) + + return panel + + def _move_panel(self, panel, pos, x1, y1, x2, y2): + x = (x2 - x1) * pos + x1 + y = (y2 - y1) * pos + y1 + + panel.move(int(x), int(y)) + + # FIXME we should hide and show as necessary to free memory + if not panel.props.visible: + panel.show() + + def _connect_to_panel(self, panel): + panel.connect('enter-notify-event', self._enter_notify_cb) + panel.connect('leave-notify-event', self._leave_notify_cb) + + def _update_position(self): + screen_h = gtk.gdk.screen_height() + screen_w = gtk.gdk.screen_width() + + self._move_panel(self._top_panel, self.current_position, + 0, - self._top_panel.size, 0, 0) + + self._move_panel(self._bottom_panel, self.current_position, + 0, screen_h, 0, screen_h - self._bottom_panel.size) + + self._move_panel(self._left_panel, self.current_position, + - self._left_panel.size, 0, 0, 0) + + self._move_panel(self._right_panel, self.current_position, + screen_w, 0, screen_w - self._right_panel.size, 0) + + def _size_changed_cb(self, screen): + self._update_position() + + def _enter_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self._mouse_listener.mouse_enter() + + def _leave_notify_cb(self, window, event): + if event.detail == gtk.gdk.NOTIFY_INFERIOR: + return + + if not self._is_hover() and not self._palette_group.is_up(): + self._mouse_listener.mouse_leave() + + def _palette_group_popdown_cb(self, group): + if not self._is_hover(): + self._mouse_listener.mouse_leave() + + def _drag_motion_cb(self, window, context, x, y, time): + self._mouse_listener.mouse_enter() + + def _drag_leave_cb(self, window, drag_context, timestamp): + self._mouse_listener.mouse_leave() + + def _enter_corner_cb(self, event_area): + self._mouse_listener.mouse_enter() + + def notify_key_press(self): + self._key_listener.key_press() + + def add_notification(self, icon, corner=TOP_LEFT): + if not isinstance(icon, NotificationIcon): + raise TypeError('icon must be a NotificationIcon.') + + window = NotificationWindow() + + screen = gtk.gdk.screen_get_default() + if corner == TOP_LEFT: + window.move(0, 0) + elif corner == TOP_RIGHT: + window.move(screen.get_width() - style.GRID_CELL_SIZE, 0) + elif corner == BOTTOM_LEFT: + window.move(0, screen.get_height() - style.GRID_CELL_SIZE) + elif corner == BOTTOM_RIGHT: + window.move(screen.get_width() - style.GRID_CELL_SIZE, + screen.get_height() - style.GRID_CELL_SIZE) + else: + raise ValueError('Inalid corner: %r' % corner) + + window.add(icon) + icon.show() + window.show() + + self._notif_by_icon[icon] = window + + gobject.timeout_add(_NOTIFICATION_DURATION, + lambda: self.remove_notification(icon)) + + def remove_notification(self, icon): + if not isinstance(icon, NotificationIcon): + raise TypeError('icon must be a NotificationIcon.') + + if icon not in self._notif_by_icon: + logging.debug('icon %r not in list of notifications.' % icon) + return + + window = self._notif_by_icon[icon] + window.destroy() + del self._notif_by_icon[icon] + + visible = property(is_visible, None) + +_instance = None + +def get_instance(): + global _instance + if not _instance: + _instance = Frame() + return _instance + diff --git a/src/jarabe/view/frame/frameinvoker.py b/src/jarabe/view/frame/frameinvoker.py new file mode 100644 index 0000000..e4a13e1 --- /dev/null +++ b/src/jarabe/view/frame/frameinvoker.py @@ -0,0 +1,36 @@ +# Copyright (C) 2007, Eduardo Silva <edsiper@gmail.com> +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk + +from sugar.graphics import style +from sugar.graphics.palette import WidgetInvoker + +def _get_screen_area(): + frame_thickness = style.GRID_CELL_SIZE + + x = y = frame_thickness + width = gtk.gdk.screen_width() - frame_thickness + height = gtk.gdk.screen_height() - frame_thickness + + return gtk.gdk.Rectangle(x, y, width, height) + +class FrameWidgetInvoker(WidgetInvoker): + def __init__(self, widget): + WidgetInvoker.__init__(self, widget, widget.child) + + self._position_hint = self.ANCHORED + self._screen_area = _get_screen_area() diff --git a/src/jarabe/view/frame/framewindow.py b/src/jarabe/view/frame/framewindow.py new file mode 100644 index 0000000..623d162 --- /dev/null +++ b/src/jarabe/view/frame/framewindow.py @@ -0,0 +1,104 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import hippo + +from sugar.graphics import style + +class FrameWindow(gtk.Window): + __gtype_name__ = 'SugarFrameWindow' + + def __init__(self, position): + gtk.Window.__init__(self) + self.hover = False + self.size = style.GRID_CELL_SIZE + style.LINE_WIDTH + + self._position = position + + self.set_decorated(False) + self.connect('realize', self._realize_cb) + self.connect('enter-notify-event', self._enter_notify_cb) + self.connect('leave-notify-event', self._leave_notify_cb) + + self._canvas = hippo.Canvas() + self.add(self._canvas) + self._canvas.show() + + box = hippo.CanvasBox() + self._canvas.set_root(box) + + padding = style.GRID_CELL_SIZE + if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM: + box.props.orientation = hippo.ORIENTATION_HORIZONTAL + box.props.padding_left = padding + box.props.padding_right = padding + box.props.padding_top = 0 + box.props.padding_bottom = 0 + else: + box.props.orientation = hippo.ORIENTATION_VERTICAL + box.props.padding_left = 0 + box.props.padding_right = 0 + box.props.padding_top = padding + box.props.padding_bottom = padding + + self._bg = hippo.CanvasBox( + border_color=style.COLOR_BUTTON_GREY.get_int()) + + border = style.LINE_WIDTH + if position == gtk.POS_TOP: + self._bg.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._bg.props.border_bottom = border + elif position == gtk.POS_BOTTOM: + self._bg.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._bg.props.border_top = border + elif position == gtk.POS_LEFT: + self._bg.props.orientation = hippo.ORIENTATION_VERTICAL + self._bg.props.border_right = border + elif position == gtk.POS_RIGHT: + self._bg.props.orientation = hippo.ORIENTATION_VERTICAL + self._bg.props.border_left = border + + box.append(self._bg, hippo.PACK_EXPAND) + + self._update_size() + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + def append(self, child, flags=0): + self._bg.append(child, flags) + + def _update_size(self): + if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM: + self.resize(gtk.gdk.screen_width(), self.size) + else: + self.resize(self.size, gtk.gdk.screen_height()) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + + def _enter_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self.hover = True + + def _leave_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self.hover = False + + def _size_changed_cb(self, screen): + self._update_size() diff --git a/src/jarabe/view/frame/friendstray.py b/src/jarabe/view/frame/friendstray.py new file mode 100644 index 0000000..93d023e --- /dev/null +++ b/src/jarabe/view/frame/friendstray.py @@ -0,0 +1,144 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.presence import presenceservice +from sugar.graphics.tray import VTray, TrayIcon + +import view.Shell +from jarabe.view.BuddyMenu import BuddyMenu +from jarabe.view.frame.frameinvoker import FrameWidgetInvoker +from jarabe.model import shellmodel +from jarabe.model.BuddyModel import BuddyModel + +class FriendIcon(TrayIcon): + def __init__(self, buddy): + TrayIcon.__init__(self, icon_name='computer-xo', + xo_color=buddy.get_color()) + + palette = BuddyMenu(buddy) + palette.props.icon_visible = False + self.set_palette(palette) + palette.set_group_id('frame') + palette.props.invoker = FrameWidgetInvoker(self) + +class FriendsTray(VTray): + def __init__(self): + VTray.__init__(self) + + self._activity_ps = None + self._joined_hid = -1 + self._left_hid = -1 + self._buddies = {} + + self._pservice = presenceservice.get_instance() + self._pservice.connect('activity-appeared', + self.__activity_appeared_cb) + + self._owner = self._pservice.get_owner() + + # Add initial activities the PS knows about + self._pservice.get_activities_async( \ + reply_handler=self._get_activities_cb) + + home_model = shellmodel.get_instance().get_home() + home_model.connect('active-activity-changed', + self._active_activity_changed_cb) + + def _get_activities_cb(self, activities_list): + for act in activities_list: + self.__activity_appeared_cb(self._pservice, act) + + def add_buddy(self, buddy): + if self._buddies.has_key(buddy.props.key): + return + + model = BuddyModel(buddy=buddy) + + icon = FriendIcon(model) + self.add_item(icon) + icon.show() + + self._buddies[buddy.props.key] = icon + + def remove_buddy(self, buddy): + if not self._buddies.has_key(buddy.props.key): + return + + self.remove_item(self._buddies[buddy.props.key]) + del self._buddies[buddy.props.key] + + def clear(self): + for item in self.get_children(): + self.remove_item(item) + item.destroy() + self._buddies = {} + + def __activity_appeared_cb(self, pservice, activity_ps): + activity = view.Shell.get_instance().get_current_activity() + if activity and activity_ps.props.id == activity.get_id(): + self._set_activity_ps(activity_ps, True) + + def _set_activity_ps(self, activity_ps, shared_activity): + if self._activity_ps == activity_ps: + return + + if self._joined_hid > 0: + self._activity_ps.disconnect(self._joined_hid) + self._joined_hid = -1 + if self._left_hid > 0: + self._activity_ps.disconnect(self._left_hid) + self._left_hid = -1 + + self._activity_ps = activity_ps + + self.clear() + + # always display ourselves + self.add_buddy(self._owner) + + if shared_activity is True: + for buddy in activity_ps.get_joined_buddies(): + self.add_buddy(buddy) + + self._joined_hid = activity_ps.connect( + 'buddy-joined', self.__buddy_joined_cb) + self._left_hid = activity_ps.connect( + 'buddy-left', self.__buddy_left_cb) + + def _active_activity_changed_cb(self, home_model, home_activity): + if home_activity is None: + return + + activity_id = home_activity.get_activity_id() + if activity_id is None: + return + + # check if activity is shared + activity = None + for act in self._pservice.get_activities(): + if activity_id == act.props.id: + activity = act + break + if activity: + self._set_activity_ps(activity, True) + else: + self._set_activity_ps(home_activity, False) + + def __buddy_joined_cb(self, activity, buddy): + self.add_buddy(buddy) + + def __buddy_left_cb(self, activity, buddy): + self.remove_buddy(buddy) diff --git a/src/jarabe/view/frame/notification.py b/src/jarabe/view/frame/notification.py new file mode 100644 index 0000000..68107ba --- /dev/null +++ b/src/jarabe/view/frame/notification.py @@ -0,0 +1,100 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk + +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + +from jarabe.view.pulsingicon import PulsingIcon + +class NotificationIcon(gtk.EventBox): + __gtype_name__ = 'SugarNotificationIcon' + + __gproperties__ = { + 'xo-color' : (object, None, None, gobject.PARAM_READWRITE), + 'icon-name' : (str, None, None, None, gobject.PARAM_READWRITE), + 'icon-filename' : (str, None, None, None, gobject.PARAM_READWRITE) + } + + _PULSE_TIMEOUT = 3000 + + def __init__(self, **kwargs): + self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE) + gobject.GObject.__init__(self, **kwargs) + self.props.visible_window = False + + self._icon.props.pulse_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self._icon.props.pulsing = True + self.add(self._icon) + self._icon.show() + + gobject.timeout_add(self._PULSE_TIMEOUT, self.__stop_pulsing_cb) + + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + + def __stop_pulsing_cb(self): + self._icon.props.pulsing = False + return False + + def do_set_property(self, pspec, value): + if pspec.name == 'xo-color': + if self._icon.props.base_color != value: + self._icon.props.base_color = value + elif pspec.name == 'icon-name': + if self._icon.props.icon_name != value: + self._icon.props.icon_name = value + elif pspec.name == 'icon-filename': + if self._icon.props.file != value: + self._icon.props.file = value + + def do_get_property(self, pspec): + if pspec.name == 'xo-color': + return self._icon.props.base_color + elif pspec.name == 'icon-name': + return self._icon.props.icon_name + elif pspec.name == 'icon-filename': + return self._icon.props.file + + def _set_palette(self, palette): + self._icon.palette = palette + + def _get_palette(self): + return self._icon.palette + + palette = property(_get_palette, _set_palette) + +class NotificationWindow(gtk.Window): + __gtype_name__ = 'SugarNotificationWindow' + + def __init__(self, **kwargs): + + gtk.Window.__init__(self, **kwargs) + + self.set_decorated(False) + self.set_resizable(False) + self.connect('realize', self._realize_cb) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + + color = gtk.gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html()) + self.modify_bg(gtk.STATE_NORMAL, color) + diff --git a/src/jarabe/view/frame/zoomtoolbar.py b/src/jarabe/view/frame/zoomtoolbar.py new file mode 100644 index 0000000..8829790 --- /dev/null +++ b/src/jarabe/view/frame/zoomtoolbar.py @@ -0,0 +1,88 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging + +import gtk + +from sugar.graphics.palette import Palette +from sugar.graphics.radiotoolbutton import RadioToolButton + +import view.Shell +from jarabe.view.frame.frameinvoker import FrameWidgetInvoker +from jarabe.model import shellmodel + +class ZoomToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + + # we shouldn't be mirrored in RTL locales + self.set_direction(gtk.TEXT_DIR_LTR) + + self._mesh_button = self._add_button('zoom-neighborhood', + _('Neighborhood'), shellmodel.ShellModel.ZOOM_MESH) + self._groups_button = self._add_button('zoom-groups', + _('Group'), shellmodel.ShellModel.ZOOM_FRIENDS) + self._home_button = self._add_button('zoom-home', + _('Home'), shellmodel.ShellModel.ZOOM_HOME) + self._activity_button = self._add_button('zoom-activity', + _('Activity'), shellmodel.ShellModel.ZOOM_ACTIVITY) + + shell_model = shellmodel.get_instance() + self._set_zoom_level(shell_model.props.zoom_level) + shell_model.connect('notify::zoom-level', self.__notify_zoom_level_cb) + + def _add_button(self, icon_name, label, zoom_level): + if self.get_children(): + group = self.get_children()[0] + else: + group = None + + button = RadioToolButton(named_icon=icon_name, group=group) + button.connect('clicked', self.__level_clicked_cb, zoom_level) + self.add(button) + button.show() + + palette = Palette(label) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + return button + + def __level_clicked_cb(self, button, level): + if not button.get_active(): + return + if shellmodel.get_instance().props.zoom_level != level: + view.Shell.get_instance().set_zoom_level(level) + + def __notify_zoom_level_cb(self, model, pspec): + self._set_zoom_level(model.props.zoom_level) + + def _set_zoom_level(self, new_level): + logging.debug('new zoom level: %r' % new_level) + if new_level == shellmodel.ShellModel.ZOOM_MESH: + self._mesh_button.props.active = True + elif new_level == shellmodel.ShellModel.ZOOM_FRIENDS: + self._groups_button.props.active = True + elif new_level == shellmodel.ShellModel.ZOOM_HOME: + self._home_button.props.active = True + elif new_level == shellmodel.ShellModel.ZOOM_ACTIVITY: + self._activity_button.props.active = True + else: + raise ValueError('Invalid zoom level: %r' % (new_level)) + diff --git a/src/jarabe/view/home/FriendView.py b/src/jarabe/view/home/FriendView.py new file mode 100644 index 0000000..f1e00b1 --- /dev/null +++ b/src/jarabe/view/home/FriendView.py @@ -0,0 +1,87 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.presence import presenceservice +from sugar import activity + +from jarabe.view.BuddyIcon import BuddyIcon + +class FriendView(hippo.CanvasBox): + def __init__(self, buddy, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + + self._pservice = presenceservice.get_instance() + + self._buddy = buddy + self._buddy_icon = BuddyIcon(buddy) + self._buddy_icon.props.size = style.LARGE_ICON_SIZE + self.append(self._buddy_icon) + + self._activity_icon = CanvasIcon(size=style.LARGE_ICON_SIZE) + self._activity_icon_visible = False + + if self._buddy.is_present(): + self._buddy_appeared_cb(buddy) + + self._buddy.connect('current-activity-changed', + self._buddy_activity_changed_cb) + self._buddy.connect('appeared', self._buddy_appeared_cb) + self._buddy.connect('disappeared', self._buddy_disappeared_cb) + self._buddy.connect('color-changed', self._buddy_color_changed_cb) + + def _get_new_icon_name(self, ps_activity): + registry = activity.get_registry() + activity_info = registry.get_activity(ps_activity.props.type) + if activity_info: + return activity_info.icon + return None + + def _remove_activity_icon(self): + if self._activity_icon_visible: + self.remove(self._activity_icon) + self._activity_icon_visible = False + + def _buddy_activity_changed_cb(self, buddy, ps_activity=None): + if not ps_activity: + self._remove_activity_icon() + return + + # FIXME: use some sort of "unknown activity" icon rather + # than hiding the icon? + name = self._get_new_icon_name(ps_activity) + if name: + self._activity_icon.props.file_name = name + self._activity_icon.props.xo_color = buddy.get_color() + if not self._activity_icon_visible: + self.append(self._activity_icon, hippo.PACK_EXPAND) + self._activity_icon_visible = True + else: + self._remove_activity_icon() + + def _buddy_appeared_cb(self, buddy): + home_activity = self._buddy.get_current_activity() + self._buddy_activity_changed_cb(buddy, home_activity) + + def _buddy_disappeared_cb(self, buddy): + self._buddy_activity_changed_cb(buddy, None) + + def _buddy_color_changed_cb(self, buddy, color): + # TODO: shouldn't this change self._buddy_icon instead? + self._activity_icon.props.xo_color = buddy.get_color() diff --git a/src/jarabe/view/home/FriendsBox.py b/src/jarabe/view/home/FriendsBox.py new file mode 100644 index 0000000..42e5709 --- /dev/null +++ b/src/jarabe/view/home/FriendsBox.py @@ -0,0 +1,89 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk +import hippo + +from sugar import profile +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon, Icon +from sugar.graphics.palette import Palette + +from jarabe.model import shellmodel +from jarabe.view.home.FriendView import FriendView +from jarabe.view.home.spreadlayout import SpreadLayout + +class FriendsBox(hippo.Canvas): + __gtype_name__ = 'SugarFriendsBox' + def __init__(self): + gobject.GObject.__init__(self) + + self._box = hippo.CanvasBox() + self._box.props.background_color = style.COLOR_WHITE.get_int() + self.set_root(self._box) + + self._friends = {} + + self._layout = SpreadLayout() + self._box.set_layout(self._layout) + + self._owner_icon = CanvasIcon(icon_name='computer-xo', cache=True, + xo_color=profile.get_color()) + self._owner_icon.props.size = style.LARGE_ICON_SIZE + palette_icon = Icon(icon_name='computer-xo', + xo_color=profile.get_color()) + palette_icon.props.icon_size = gtk.ICON_SIZE_LARGE_TOOLBAR + palette = Palette(None, primary_text=profile.get_nick_name(), + icon=palette_icon) + self._owner_icon.set_palette(palette) + self._layout.add(self._owner_icon) + + friends = shellmodel.get_instance().get_friends() + + for friend in friends: + self.add_friend(friend) + + friends.connect('friend-added', self._friend_added_cb) + friends.connect('friend-removed', self._friend_removed_cb) + + def add_friend(self, buddy_info): + icon = FriendView(buddy_info) + self._layout.add(icon) + + self._friends[buddy_info.get_key()] = icon + + def _friend_added_cb(self, data_model, buddy_info): + self.add_friend(buddy_info) + + def _friend_removed_cb(self, data_model, key): + icon = self._friends[key] + self._layout.remove(icon) + del self._friends[key] + icon.destroy() + + def do_size_allocate(self, allocation): + width = allocation.width + height = allocation.height + + min_w_, icon_width = self._owner_icon.get_width_request() + min_h_, icon_height = self._owner_icon.get_height_request(icon_width) + x = (width - icon_width) / 2 + y = (height - icon_height) / 2 + self._layout.move(self._owner_icon, x, y) + + hippo.Canvas.do_size_allocate(self, allocation) + diff --git a/src/jarabe/view/home/HomeBox.py b/src/jarabe/view/home/HomeBox.py new file mode 100644 index 0000000..400ee8f --- /dev/null +++ b/src/jarabe/view/home/HomeBox.py @@ -0,0 +1,367 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging +import os + +import gobject +import gtk + +from sugar.graphics import style +from sugar.graphics import iconentry +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.alert import Alert +from sugar.graphics.icon import Icon +from sugar import profile +from sugar import activity +from sugar.bundle.activitybundle import ActivityBundle + +from jarabe.view.home import favoritesview +from jarabe.view.home.activitieslist import ActivitiesList + +_FAVORITES_VIEW = 0 +_LIST_VIEW = 1 + +_AUTOSEARCH_TIMEOUT = 1000 + +def _convert_layout_constant(profile_constant): + for layoutid, layoutclass in favoritesview.LAYOUT_MAP.items(): + if profile_constant == layoutclass.profile_key: + return layoutid + logging.warning('Incorrect favorites_layout value: %r' % \ + profile_constant) + return favoritesview.RING_LAYOUT + +class HomeBox(gtk.VBox): + __gtype_name__ = 'SugarHomeBox' + + def __init__(self): + gobject.GObject.__init__(self) + + self._favorites_view = favoritesview.FavoritesView() + self._list_view = ActivitiesList() + self._enable_xo_palette = False + + self._favorites_view.connect('erase-activated', + self.__erase_activated_cb) + self._list_view.connect('erase-activated', self.__erase_activated_cb) + + self._toolbar = HomeToolbar() + self._toolbar.connect('query-changed', self.__toolbar_query_changed_cb) + self._toolbar.connect('view-changed', self.__toolbar_view_changed_cb) + self.pack_start(self._toolbar, expand=False) + self._toolbar.show() + + profile_layout_constant = profile.get_profile().favorites_layout + layout = _convert_layout_constant(profile_layout_constant) + self._set_view(_FAVORITES_VIEW, layout) + + def __erase_activated_cb(self, view, bundle_id): + registry = activity.get_registry() + activity_info = registry.get_activity(bundle_id) + + alert = Alert() + alert.props.title = _('Confirm erase') + alert.props.msg = \ + _('Confirm erase: Do you want to permanently erase %s?') \ + % activity_info.name + + cancel_icon = Icon(icon_name='dialog-cancel') + alert.add_button(gtk.RESPONSE_CANCEL, _('Keep'), cancel_icon) + + erase_icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_OK, _('Erase'), erase_icon) + + if self._list_view in self.get_children(): + self._list_view.add_alert(alert) + else: + self._favorites_view.add_alert(alert) + # TODO: If the favorite layouts didn't hardcoded the box size, we could + # just pack an alert between the toolbar and the canvas. + #self.pack_start(alert, False) + #self.reorder_child(alert, 1) + alert.connect('response', self.__erase_confirmation_dialog_response_cb, + bundle_id) + + def __erase_confirmation_dialog_response_cb(self, alert, response_id, + bundle_id): + if self._list_view in self.get_children(): + self._list_view.remove_alert() + else: + self._favorites_view.remove_alert() + if response_id == gtk.RESPONSE_OK: + registry = activity.get_registry() + activity_info = registry.get_activity(bundle_id) + ActivityBundle(activity_info.path).uninstall() + + def show_software_updates_alert(self): + alert = Alert() + updater_icon = Icon(icon_name='module-updater', + pixel_size = style.STANDARD_ICON_SIZE) + alert.props.icon = updater_icon + updater_icon.show() + alert.props.title = _('Software Update') + alert.props.msg = _('Update your activities to ensure' + ' compatibility with your new software') + + cancel_icon = Icon(icon_name='dialog-cancel') + alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon) + + alert.add_button(gtk.RESPONSE_REJECT, _('Later')) + + erase_icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_OK, _('Check now'), erase_icon) + + if self._list_view in self.get_children(): + self._list_view.add_alert(alert) + else: + self._favorites_view.add_alert(alert) + alert.connect('response', self.__software_update_response_cb) + + def __software_update_response_cb(self, alert, response_id): + if self._list_view in self.get_children(): + self._list_view.remove_alert() + else: + self._favorites_view.remove_alert() + + if response_id != gtk.RESPONSE_REJECT: + update_trigger_file = os.path.expanduser('~/.sugar-update') + try: + os.unlink(update_trigger_file) + except OSError: + logging.error('Software-update: Can not remove file %s' % + update_trigger_file) + + if response_id == gtk.RESPONSE_OK: + from jarabe.controlpanel.gui import ControlPanel + panel = ControlPanel() + panel.set_transient_for(self.get_toplevel()) + panel.show() + panel.show_section_view('updater') + panel.set_section_view_auto_close() + + def __toolbar_query_changed_cb(self, toolbar, query): + query = query.lower() + self._list_view.set_filter(query) + + def __toolbar_view_changed_cb(self, toolbar, view, layout): + self._set_view(view, layout) + if layout is not None: + current_profile = profile.get_profile() + profile_key = favoritesview.LAYOUT_MAP[layout].profile_key + if profile_key != current_profile.favorites_layout: + current_profile.favorites_layout = profile_key + current_profile.save() + else: + logging.warning('Incorrect layout requested: %r' % layout) + + def _set_view(self, view, layout): + if view == _FAVORITES_VIEW: + if self._list_view in self.get_children(): + self.remove(self._list_view) + + self._favorites_view.layout = layout + + if self._enable_xo_palette: + self._favorites_view.enable_xo_palette() + + if self._favorites_view not in self.get_children(): + self.add(self._favorites_view) + self._favorites_view.show() + elif view == _LIST_VIEW: + if self._favorites_view in self.get_children(): + self.remove(self._favorites_view) + + if self._list_view not in self.get_children(): + self.add(self._list_view) + self._list_view.show() + else: + raise ValueError('Invalid view: %r' % view) + + _REDRAW_TIMEOUT = 5 * 60 * 1000 # 5 minutes + + def resume(self): + pass + + def suspend(self): + pass + + def has_activities(self): + # TODO: Do we need this? + #return self._donut.has_activities() + return False + + def enable_xo_palette(self): + self._enable_xo_palette = True + if self._favorites_view is not None: + self._favorites_view.enable_xo_palette() + + def focus_search_entry(self): + self._toolbar.search_entry.grab_focus() + + +class HomeToolbar(gtk.Toolbar): + __gtype_name__ = 'SugarHomeToolbar' + + __gsignals__ = { + 'query-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + 'view-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([object, object])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._query = None + self._autosearch_timer = None + + self._add_separator() + + tool_item = gtk.ToolItem() + self.insert(tool_item, -1) + tool_item.show() + + self.search_entry = iconentry.IconEntry() + self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self.search_entry.add_clear_button() + self.search_entry.set_width_chars(25) + self.search_entry.connect('activate', self.__entry_activated_cb) + self.search_entry.connect('changed', self.__entry_changed_cb) + tool_item.add(self.search_entry) + self.search_entry.set_sensitive(False) + self.search_entry.show() + + self._add_separator(expand=True) + + favorites_button = FavoritesButton() + favorites_button.connect('toggled', self.__view_button_toggled_cb, + _FAVORITES_VIEW) + self.insert(favorites_button, -1) + favorites_button.show() + + self._list_button = RadioToolButton(named_icon='view-list') + self._list_button.props.group = favorites_button + self._list_button.props.tooltip = _('List view') + self._list_button.props.accelerator = _('<Ctrl>2') + self._list_button.connect('toggled', self.__view_button_toggled_cb, + _LIST_VIEW) + self.insert(self._list_button, -1) + self._list_button.show() + + self._add_separator() + + def __view_button_toggled_cb(self, button, view): + if button.props.active: + if view == _FAVORITES_VIEW: + self.search_entry.set_text('') + self.search_entry.set_sensitive(False) + self.emit('view-changed', view, button.layout) + else: + self.search_entry.set_sensitive(True) + self.search_entry.grab_focus() + self.emit('view-changed', view, None) + + def _add_separator(self, expand=False): + separator = gtk.SeparatorToolItem() + separator.props.draw = False + if expand: + separator.set_expand(True) + else: + separator.set_size_request(style.GRID_CELL_SIZE, + style.GRID_CELL_SIZE) + self.insert(separator, -1) + separator.show() + + def __entry_activated_cb(self, entry): + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + new_query = entry.props.text + if self._query != new_query: + self._query = new_query + + if self._query is not '': + self._list_button.props.active = True + self.emit('query-changed', self._query) + + def __entry_changed_cb(self, entry): + if not entry.props.text: + entry.activate() + return + + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT, + self.__autosearch_timer_cb) + + def __autosearch_timer_cb(self): + self._autosearch_timer = None + self.search_entry.activate() + return False + +class FavoritesButton(RadioToolButton): + __gtype_name__ = 'SugarFavoritesButton' + + def __init__(self): + RadioToolButton.__init__(self) + + self.props.tooltip = _('Favorites view') + self.props.accelerator = _('<Ctrl>1') + self.props.group = None + + profile_layout_constant = profile.get_profile().favorites_layout + self._layout = _convert_layout_constant(profile_layout_constant) + self._update_icon() + + # someday, this will be a gtk.Table() + layouts_grid = gtk.HBox() + layout_item = None + for layoutid, layoutclass in sorted(favoritesview.LAYOUT_MAP.items()): + layout_item = RadioToolButton(icon_name=layoutclass.icon_name, + group=layout_item, active=False) + if layoutid == self._layout: + layout_item.set_active(True) + layouts_grid.add(layout_item) + layout_item.connect('toggled', self.__layout_activate_cb, + layoutid) + layouts_grid.show_all() + self.props.palette.set_content(layouts_grid) + + def __layout_activate_cb(self, menu_item, layout): + if not menu_item.get_active(): + return + if self._layout == layout and self.props.active: + return + elif self._layout != layout: + self._layout = layout + self._update_icon() + if not self.props.active: + self.props.active = True + else: + self.emit('toggled') + + def _update_icon(self): + self.props.named_icon = favoritesview.LAYOUT_MAP[self._layout]\ + .icon_name + + def _get_layout(self): + return self._layout + layout = property(_get_layout, None) + diff --git a/src/jarabe/view/home/HomeWindow.py b/src/jarabe/view/home/HomeWindow.py new file mode 100644 index 0000000..0bc56a0 --- /dev/null +++ b/src/jarabe/view/home/HomeWindow.py @@ -0,0 +1,153 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk + +from sugar.graphics import style +from sugar.graphics import palettegroup + +from jarabe.view.home.MeshBox import MeshBox +from jarabe.view.home.HomeBox import HomeBox +from jarabe.view.home.FriendsBox import FriendsBox +from jarabe.view.home.transitionbox import TransitionBox +from jarabe.model.shellmodel import ShellModel +from jarabe.model import shellmodel + +_HOME_PAGE = 0 +_FRIENDS_PAGE = 1 +_MESH_PAGE = 2 +_TRANSITION_PAGE = 3 + +class HomeWindow(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + + accel_group = gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.add_accel_group(accel_group) + + self._active = False + self._level = ShellModel.ZOOM_HOME + + self.set_default_size(gtk.gdk.screen_width(), + gtk.gdk.screen_height()) + + self.realize() + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DESKTOP) + self.connect('visibility-notify-event', + self._visibility_notify_event_cb) + + self._enter_sid = self.connect('enter-notify-event', + self._enter_notify_event_cb) + self._leave_sid = self.connect('leave-notify-event', + self._leave_notify_event_cb) + self._motion_sid = self.connect('motion-notify-event', + self._motion_notify_event_cb) + + self._home_box = HomeBox() + self._friends_box = FriendsBox() + self._mesh_box = MeshBox() + self._transition_box = TransitionBox() + + self._activate_view() + self.add(self._home_box) + self._home_box.show() + + self._transition_box.connect('completed', + self._transition_completed_cb) + + model = shellmodel.get_instance() + model.connect('notify::zoom-level', self.__zoom_level_changed_cb) + + def _enter_notify_event_cb(self, window, event): + if event.x != gtk.gdk.screen_width() / 2 or \ + event.y != gtk.gdk.screen_height() / 2: + self._mouse_moved() + + def _leave_notify_event_cb(self, window, event): + self._mouse_moved() + + def _motion_notify_event_cb(self, window, event): + self._mouse_moved() + + # We want to enable the XO palette only when the user + # moved away from the default mouse position (screen center). + def _mouse_moved(self): + self._home_box.enable_xo_palette() + self.disconnect(self._leave_sid) + self.disconnect(self._motion_sid) + self.disconnect(self._enter_sid) + + def _deactivate_view(self): + group = palettegroup.get_group("default") + group.popdown() + if self._level == ShellModel.ZOOM_HOME: + self._home_box.suspend() + elif self._level == ShellModel.ZOOM_MESH: + self._mesh_box.suspend() + + def _activate_view(self): + if self._level == ShellModel.ZOOM_HOME: + self._home_box.resume() + elif self._level == ShellModel.ZOOM_MESH: + self._mesh_box.resume() + + def _visibility_notify_event_cb(self, window, event): + if event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED: + self._deactivate_view() + else: + self._activate_view() + + def __zoom_level_changed_cb(self, model, pspec): + level = model.props.zoom_level + if level == ShellModel.ZOOM_ACTIVITY: + return + + self._deactivate_view() + self._level = level + self._activate_view() + + self.remove(self.get_child()) + self.add(self._transition_box) + self._transition_box.show() + + if self._level == ShellModel.ZOOM_HOME: + size = style.XLARGE_ICON_SIZE + elif self._level == ShellModel.ZOOM_FRIENDS: + size = style.LARGE_ICON_SIZE + elif self._level == ShellModel.ZOOM_MESH: + size = style.STANDARD_ICON_SIZE + + self._transition_box.set_size(size) + + def _transition_completed_cb(self, transition_box): + current_child = self.get_child() + self.remove(current_child) + + if self._level == ShellModel.ZOOM_HOME: + self.add(self._home_box) + self._home_box.show() + self._home_box.focus_search_entry() + elif self._level == ShellModel.ZOOM_FRIENDS: + self.add(self._friends_box) + self._friends_box.show() + elif self._level == ShellModel.ZOOM_MESH: + self.add(self._mesh_box) + self._mesh_box.show() + self._mesh_box.focus_search_entry() + + def get_home_box(self): + return self._home_box diff --git a/src/jarabe/view/home/Makefile.am b/src/jarabe/view/home/Makefile.am new file mode 100644 index 0000000..c7d9a98 --- /dev/null +++ b/src/jarabe/view/home/Makefile.am @@ -0,0 +1,17 @@ +sugardir = $(pythondir)/jarabe/view/home +sugar_PYTHON = \ + __init__.py \ + activitieslist.py \ + favoritesview.py \ + favoriteslayout.py \ + grid.py \ + FriendView.py \ + FriendsBox.py \ + HomeBox.py \ + HomeWindow.py \ + MeshBox.py \ + MyIcon.py \ + proc_smaps.py \ + snowflakelayout.py \ + spreadlayout.py \ + transitionbox.py diff --git a/src/jarabe/view/home/MeshBox.py b/src/jarabe/view/home/MeshBox.py new file mode 100644 index 0000000..f94b930 --- /dev/null +++ b/src/jarabe/view/home/MeshBox.py @@ -0,0 +1,646 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging + +import hippo +import gobject +import gtk + +from sugar.graphics.icon import CanvasIcon, Icon +from sugar.graphics.xocolor import XoColor +from sugar.graphics import style +from sugar.graphics.icon import get_icon_state +from sugar.graphics import palette +from sugar.graphics import iconentry +from sugar.graphics.menuitem import MenuItem +from sugar import profile + +from jarabe.model import accesspointmodel +from jarabe.model.devices.network import wireless +from jarabe.model import shellmodel +from jarabe.hardware import hardwaremanager +from jarabe.hardware import nmclient +from jarabe.view.BuddyIcon import BuddyIcon +from jarabe.view.pulsingicon import CanvasPulsingIcon +from jarabe.view.home.snowflakelayout import SnowflakeLayout +from jarabe.view.home.spreadlayout import SpreadLayout +import view.Shell + +from jarabe.hardware.nmclient import NM_802_11_CAP_PROTO_WEP, \ + NM_802_11_CAP_PROTO_WPA, NM_802_11_CAP_PROTO_WPA2 + + +_ICON_NAME = 'network-wireless' + +class AccessPointView(CanvasPulsingIcon): + def __init__(self, model, mesh_device=None): + CanvasPulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE, + cache=True) + self._model = model + self._meshdev = mesh_device + self._disconnect_item = None + self._connect_item = None + self._greyed_out = False + + self.connect('activated', self._activate_cb) + + model.connect('notify::strength', self._strength_changed_cb) + model.connect('notify::name', self._name_changed_cb) + model.connect('notify::state', self._state_changed_cb) + + pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.pulse_color = pulse_color + + # Update badge + caps = model.props.capabilities + if model.get_nm_network().is_favorite(): + self.props.badge_name = "emblem-favorite" + elif (caps & NM_802_11_CAP_PROTO_WEP) or \ + (caps & NM_802_11_CAP_PROTO_WPA) or \ + (caps & NM_802_11_CAP_PROTO_WPA2): + self.props.badge_name = "emblem-locked" + + self._palette = self._create_palette() + self.set_palette(self._palette) + + self._update_icon() + self._update_name() + self._update_state() + + def _create_palette(self): + icon_name = get_icon_state(_ICON_NAME, self._model.props.strength) + palette_icon = Icon(icon_name=icon_name, + icon_size=style.STANDARD_ICON_SIZE, + badge_name=self.props.badge_name) + ap_color = self._model.get_nm_network().get_colors() + palette_icon.props.xo_color = XoColor('%s,%s' % ap_color) + + p = palette.Palette(primary_text=self._model.props.name, + icon=palette_icon) + + self._connect_item = MenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('activate', self._activate_cb) + p.menu.append(self._connect_item) + + # Only show disconnect when there's a mesh device, because mesh takes + # priority over the normal wireless device. NM doesn't have a + # "disconnect" method for a device either (for various reasons) + # so this doesn't have a good mapping + if self._meshdev: + self._disconnect_item = MenuItem(_('Disconnect'), 'media-eject') + self._disconnect_item.connect('activate', + self._disconnect_activate_cb) + p.menu.append(self._disconnect_item) + + return p + + def _disconnect_activate_cb(self, menuitem): + # Disconnection for an AP means activating the default mesh device + network_manager = hardwaremanager.get_network_manager() + if network_manager and self._meshdev: + network_manager.set_active_device(self._meshdev) + self._palette.props.secondary_text = _('Disconnecting...') + self.props.pulsing = False + + def _strength_changed_cb(self, model, pspec): + self._update_icon() + + def _name_changed_cb(self, model, pspec): + self._update_name() + + def _state_changed_cb(self, model, pspec): + self._update_icon() + self._update_state() + + def _activate_cb(self, icon): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + device = self._model.get_nm_device() + network = self._model.get_nm_network() + network_manager.set_active_device(device, network) + + def _update_name(self): + self._palette.props.primary_text = self._model.props.name + + def _update_icon(self): + # keep this code in sync with view/devices/network/wireless.py + strength = self._model.props.strength + if self._model.props.state == accesspointmodel.STATE_CONNECTED: + icon_name = '%s-connected' % _ICON_NAME + else: + icon_name = _ICON_NAME + icon_name = get_icon_state(icon_name, strength) + if icon_name: + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + + def _update_state(self): + if self._model.props.state == accesspointmodel.STATE_CONNECTING: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connecting...') + self.props.pulsing = True + elif self._model.props.state == accesspointmodel.STATE_CONNECTED: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + # TODO: show the channel number + self._palette.props.secondary_text = _('Connected') + self.props.pulsing = False + elif self._model.props.state == accesspointmodel.STATE_NOTCONNECTED: + if self._disconnect_item: + self._disconnect_item.hide() + self._connect_item.show() + # TODO: show the channel number + self._palette.props.secondary_text = None + self.props.pulsing = False + + if self._greyed_out: + self.props.pulsing = False + self.props.base_color = XoColor('#D5D5D5,#D5D5D5') + else: + self.props.base_color = XoColor('%s,%s' % \ + self._model.get_nm_network().get_colors()) + + def set_filter(self, query): + self._greyed_out = self._model.props.name.lower().find(query) == -1 + self._update_state() + +_MESH_ICON_NAME = 'network-mesh' + +class MeshDeviceView(CanvasPulsingIcon): + def __init__(self, nm_device, channel): + if not channel in [1, 6, 11]: + raise ValueError("Invalid channel %d" % channel) + + CanvasPulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE, + icon_name=_MESH_ICON_NAME, cache=True) + + self._nm_device = nm_device + self.channel = channel + self.props.badge_name = "badge-channel-%d" % self.channel + self._greyed_out = False + + self._disconnect_item = None + self._palette = self._create_palette() + self.set_palette(self._palette) + + pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.pulse_color = pulse_color + + self.connect('activated', self._activate_cb) + + self._nm_device.connect('state-changed', self._state_changed_cb) + self._nm_device.connect('activation-stage-changed', + self._state_changed_cb) + self._update_state() + + def _create_palette(self): + p = palette.Palette(_("Mesh Network") + " " + str(self.channel), + menu_after_content=True) + + self._disconnect_item = gtk.MenuItem(_('Disconnect...')) + self._disconnect_item.connect('activate', self._disconnect_activate_cb) + p.menu.append(self._disconnect_item) + + state = self._nm_device.get_state() + chan = wireless.freq_to_channel(self._nm_device.get_frequency()) + if state == nmclient.DEVICE_STATE_ACTIVATED and chan == self.channel: + self._disconnect_item.show() + return p + + def _disconnect_activate_cb(self, menuitem): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + network_manager.set_active_device(self._nm_device) + + def _activate_cb(self, icon): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + freq = wireless.channel_to_freq(self.channel) + network_manager.set_active_device(self._nm_device, mesh_freq=freq) + + def _state_changed_cb(self, model): + self._update_state() + + def _update_state(self): + state = self._nm_device.get_state() + chan = wireless.freq_to_channel(self._nm_device.get_frequency()) + if state == nmclient.DEVICE_STATE_ACTIVATING and chan == self.channel: + self._disconnect_item.hide() + self.props.pulsing = True + elif state == nmclient.DEVICE_STATE_ACTIVATED and chan == self.channel: + self._disconnect_item.show() + self.props.pulsing = False + elif state == nmclient.DEVICE_STATE_INACTIVE or chan != self.channel: + self._disconnect_item.hide() + self.props.pulsing = False + + if self._greyed_out: + self.props.pulsing = False + self.props.base_color = XoColor('#D5D5D5,#D5D5D5') + else: + self.props.base_color = profile.get_color() + + def set_filter(self, query): + self._greyed_out = (query != '') + self._update_state() + +class ActivityView(hippo.CanvasBox): + def __init__(self, model): + hippo.CanvasBox.__init__(self) + + self._model = model + self._icons = {} + self._palette = None + + self._layout = SnowflakeLayout() + self.set_layout(self._layout) + + self._icon = self._create_icon() + self._layout.add(self._icon, center=True) + + self._update_palette() + + activity = self._model.activity + activity.connect('notify::name', self._name_changed_cb) + activity.connect('notify::color', self._color_changed_cb) + activity.connect('notify::private', self._private_changed_cb) + activity.connect('joined', self._joined_changed_cb) + #FIXME: 'joined' signal not working, see #5032 + + def _create_icon(self): + icon = CanvasIcon(file_name=self._model.get_icon_name(), + xo_color=self._model.get_color(), cache=True, + size=style.STANDARD_ICON_SIZE) + icon.connect('activated', self._clicked_cb) + return icon + + def _create_palette(self): + p_icon = Icon(file=self._model.get_icon_name(), + xo_color=self._model.get_color()) + p_icon.props.icon_size = gtk.ICON_SIZE_LARGE_TOOLBAR + p = palette.Palette(None, primary_text=self._model.activity.props.name, + icon=p_icon) + + private = self._model.activity.props.private + joined = self._model.activity.props.joined + + if joined: + item = MenuItem(_('Resume'), 'activity-start') + item.connect('activate', self._clicked_cb) + item.show() + p.menu.append(item) + elif not private: + item = MenuItem(_('Join'), 'activity-start') + item.connect('activate', self._clicked_cb) + item.show() + p.menu.append(item) + + return p + + def _update_palette(self): + self._palette = self._create_palette() + self._icon.set_palette(self._palette) + + def has_buddy_icon(self, key): + return self._icons.has_key(key) + + def add_buddy_icon(self, key, icon): + self._icons[key] = icon + self._layout.add(icon) + + def remove_buddy_icon(self, key): + icon = self._icons[key] + del self._icons[key] + icon.destroy() + + def _clicked_cb(self, item): + bundle_id = self._model.get_bundle_id() + view.Shell.get_instance().join_activity(bundle_id, self._model.get_id()) + + def set_filter(self, query): + text_to_check = self._model.activity.props.name.lower() + \ + self._model.activity.props.type.lower() + if text_to_check.find(query) == -1: + self._icon.props.stroke_color = '#D5D5D5' + self._icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + else: + self._icon.props.xo_color = self._model.get_color() + + for icon in self._icons.itervalues(): + if hasattr(icon, 'set_filter'): + icon.set_filter(query) + + def _name_changed_cb(self, activity, pspec): + self._update_palette() + + def _color_changed_cb(self, activity, pspec): + self._layout.remove(self._icon) + self._icon = self._create_icon() + self._layout.add(self._icon, center=True) + self._icon.set_palette(self._palette) + + def _private_changed_cb(self, activity, pspec): + self._update_palette() + + def _joined_changed_cb(self, widget, event): + logging.debug('ActivityView._joined_changed_cb: AAAA!!!!') + +_AUTOSEARCH_TIMEOUT = 1000 + +class MeshToolbar(gtk.Toolbar): + __gtype_name__ = 'MeshToolbar' + + __gsignals__ = { + 'query-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._query = None + self._autosearch_timer = None + + self._add_separator() + + tool_item = gtk.ToolItem() + self.insert(tool_item, -1) + tool_item.show() + + self.search_entry = iconentry.IconEntry() + self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self.search_entry.add_clear_button() + self.search_entry.set_width_chars(25) + self.search_entry.connect('activate', self._entry_activated_cb) + self.search_entry.connect('changed', self._entry_changed_cb) + tool_item.add(self.search_entry) + self.search_entry.show() + + self._add_separator(expand=True) + + def _add_separator(self, expand=False): + separator = gtk.SeparatorToolItem() + separator.props.draw = False + if expand: + separator.set_expand(True) + else: + separator.set_size_request(style.GRID_CELL_SIZE, + style.GRID_CELL_SIZE) + self.insert(separator, -1) + separator.show() + + def _entry_activated_cb(self, entry): + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + new_query = entry.props.text + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def _entry_changed_cb(self, entry): + if not entry.props.text: + entry.activate() + return + + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT, + self._autosearch_timer_cb) + + def _autosearch_timer_cb(self): + logging.debug('_autosearch_timer_cb') + self._autosearch_timer = None + self.search_entry.activate() + return False + +class MeshBox(gtk.VBox): + __gtype_name__ = 'SugarMeshBox' + def __init__(self): + gobject.GObject.__init__(self) + + self._model = shellmodel.get_instance().get_mesh() + self._buddies = {} + self._activities = {} + self._access_points = {} + self._mesh = {} + self._buddy_to_activity = {} + self._suspended = True + self._query = '' + self._owner_icon = None + + self._toolbar = MeshToolbar() + self._toolbar.connect('query-changed', self._toolbar_query_changed_cb) + self.pack_start(self._toolbar, expand=False) + self._toolbar.show() + + canvas = hippo.Canvas() + self.add(canvas) + canvas.show() + + self._layout_box = hippo.CanvasBox( \ + background_color=style.COLOR_WHITE.get_int()) + canvas.set_root(self._layout_box) + + self._layout = SpreadLayout() + self._layout_box.set_layout(self._layout) + + for buddy_model in self._model.get_buddies(): + self._add_alone_buddy(buddy_model) + + self._model.connect('buddy-added', self._buddy_added_cb) + self._model.connect('buddy-removed', self._buddy_removed_cb) + self._model.connect('buddy-moved', self._buddy_moved_cb) + + for activity_model in self._model.get_activities(): + self._add_activity(activity_model) + + self._model.connect('activity-added', self._activity_added_cb) + self._model.connect('activity-removed', self._activity_removed_cb) + + for ap_model in self._model.get_access_points(): + self._add_access_point(ap_model) + + self._model.connect('access-point-added', + self._access_point_added_cb) + self._model.connect('access-point-removed', + self._access_point_removed_cb) + + if self._model.get_mesh(): + self.__mesh_added_cb(self._model, self._model.get_mesh()) + + self._model.connect('mesh-added', self.__mesh_added_cb) + self._model.connect('mesh-removed', self.__mesh_removed_cb) + + def __mesh_added_cb(self, model, meshdev): + self._add_mesh_icon(meshdev, 1) + self._add_mesh_icon(meshdev, 6) + self._add_mesh_icon(meshdev, 11) + + def __mesh_removed_cb(self, model): + self._remove_mesh_icon(1) + self._remove_mesh_icon(6) + self._remove_mesh_icon(11) + + def do_size_allocate(self, allocation): + width = allocation.width + height = allocation.height + + min_w_, icon_width = self._owner_icon.get_width_request() + min_h_, icon_height = self._owner_icon.get_height_request(icon_width) + x = (width - icon_width) / 2 + y = (height - icon_height) / 2 - style.GRID_CELL_SIZE + self._layout.move(self._owner_icon, x, y) + + gtk.VBox.do_size_allocate(self, allocation) + + def _buddy_added_cb(self, model, buddy_model): + self._add_alone_buddy(buddy_model) + + def _buddy_removed_cb(self, model, buddy_model): + self._remove_buddy(buddy_model) + + def _buddy_moved_cb(self, model, buddy_model, activity_model): + # Owner doesn't move from the center + if buddy_model.is_owner(): + return + self._move_buddy(buddy_model, activity_model) + + def _activity_added_cb(self, model, activity_model): + self._add_activity(activity_model) + + def _activity_removed_cb(self, model, activity_model): + self._remove_activity(activity_model) + + def _access_point_added_cb(self, model, ap_model): + self._add_access_point(ap_model) + + def _access_point_removed_cb(self, model, ap_model): + self._remove_access_point(ap_model) + + def _add_mesh_icon(self, meshdev, channel): + if self._mesh.has_key(channel): + self._remove_mesh_icon(channel) + if not meshdev: + return + self._mesh[channel] = MeshDeviceView(meshdev, channel) + self._layout.add(self._mesh[channel]) + + def _remove_mesh_icon(self, channel): + if not self._mesh.has_key(channel): + return + self._layout.remove(self._mesh[channel]) + del self._mesh[channel] + + def _add_alone_buddy(self, buddy_model): + icon = BuddyIcon(buddy_model) + if buddy_model.is_owner(): + self._owner_icon = icon + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._buddies[buddy_model.get_buddy().object_path()] = icon + + def _remove_alone_buddy(self, buddy_model): + icon = self._buddies[buddy_model.get_buddy().object_path()] + self._layout.remove(icon) + del self._buddies[buddy_model.get_buddy().object_path()] + icon.destroy() + + def _remove_buddy(self, buddy_model): + object_path = buddy_model.get_buddy().object_path() + if self._buddies.has_key(object_path): + self._remove_alone_buddy(buddy_model) + else: + for activity in self._activities.values(): + if activity.has_buddy_icon(object_path): + activity.remove_buddy_icon(object_path) + + def _move_buddy(self, buddy_model, activity_model): + self._remove_buddy(buddy_model) + + if activity_model == None: + self._add_alone_buddy(buddy_model) + elif activity_model.get_id() in self._activities: + activity = self._activities[activity_model.get_id()] + + icon = BuddyIcon(buddy_model, style.STANDARD_ICON_SIZE) + activity.add_buddy_icon(buddy_model.get_buddy().object_path(), icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + def _add_activity(self, activity_model): + icon = ActivityView(activity_model) + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._activities[activity_model.get_id()] = icon + + def _remove_activity(self, activity_model): + icon = self._activities[activity_model.get_id()] + self._layout.remove(icon) + del self._activities[activity_model.get_id()] + icon.destroy() + + def _add_access_point(self, ap_model): + meshdev = self._model.get_mesh() + icon = AccessPointView(ap_model, meshdev) + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._access_points[ap_model.get_id()] = icon + + def _remove_access_point(self, ap_model): + icon = self._access_points[ap_model.get_id()] + self._layout.remove(icon) + del self._access_points[ap_model.get_id()] + + def suspend(self): + if not self._suspended: + self._suspended = True + for ap in self._access_points.values(): + ap.props.paused = True + + def resume(self): + if self._suspended: + self._suspended = False + for ap in self._access_points.values(): + ap.props.paused = False + + def _toolbar_query_changed_cb(self, toolbar, query): + self._query = query.lower() + for icon in self._layout_box.get_children(): + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + def focus_search_entry(self): + self._toolbar.search_entry.grab_focus() diff --git a/src/jarabe/view/home/MyIcon.py b/src/jarabe/view/home/MyIcon.py new file mode 100644 index 0000000..af0f6ce --- /dev/null +++ b/src/jarabe/view/home/MyIcon.py @@ -0,0 +1,24 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.graphics.icon import CanvasIcon +from sugar import profile + +class MyIcon(CanvasIcon): + def __init__(self, size): + CanvasIcon.__init__(self, size=size, + icon_name='computer-xo', + xo_color=profile.get_color()) diff --git a/src/jarabe/view/home/__init__.py b/src/jarabe/view/home/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/jarabe/view/home/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + diff --git a/src/jarabe/view/home/activitieslist.py b/src/jarabe/view/home/activitieslist.py new file mode 100644 index 0000000..54bd023 --- /dev/null +++ b/src/jarabe/view/home/activitieslist.py @@ -0,0 +1,302 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk +import hippo + +from sugar import profile +from sugar import activity +from sugar import util +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon + +import view.Shell +from jarabe.view.palettes import ActivityPalette + +class ActivitiesList(gtk.VBox): + __gtype_name__ = 'SugarActivitiesList' + + __gsignals__ = { + 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([str])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scrolled_window.set_shadow_type(gtk.SHADOW_NONE) + scrolled_window.connect('key-press-event', self.__key_press_event_cb) + self.pack_start(scrolled_window) + scrolled_window.show() + + canvas = hippo.Canvas() + scrolled_window.add_with_viewport(canvas) + scrolled_window.child.set_shadow_type(gtk.SHADOW_NONE) + canvas.show() + + self._alert = None + self._query = '' + self._box = hippo.CanvasBox() + self._box.props.background_color = style.COLOR_WHITE.get_int() + canvas.set_root(self._box) + + registry = activity.get_registry() + registry.get_activities_async(reply_handler=self._get_activities_cb) + registry.connect('activity-added', self.__activity_added_cb) + registry.connect('activity-removed', self.__activity_removed_cb) + + def _get_activities_cb(self, activity_list): + if activity_list: + gobject.idle_add(self._add_activity_list, activity_list) + + def _add_activity_list(self, activity_list): + info = activity_list.pop() + if info.bundle_id != 'org.laptop.JournalActivity': + self._add_activity(info) + return len(activity_list) > 0 + + def __activity_added_cb(self, activity_registry, activity_info): + self._add_activity(activity_info) + + def __activity_removed_cb(self, activity_registry, activity_info): + for entry in self._box.get_children(): + if entry.get_bundle_id() == activity_info.bundle_id and \ + entry.get_version() == activity_info.version: + self._box.remove(entry) + return + + def _compare_activities(self, entry_a, entry_b): + return entry_b.get_installation_time() - entry_a.get_installation_time() + + def _add_activity(self, activity_info): + entry = ActivityEntry(activity_info) + entry.icon.connect('erase-activated', self.__erase_activated_cb) + self._box.insert_sorted(entry, 0, self._compare_activities) + entry.set_visible(entry.matches(self._query)) + + def __erase_activated_cb(self, activity_icon, bundle_id): + self.emit('erase-activated', bundle_id) + + def set_filter(self, query): + self._query = query + for entry in self._box.get_children(): + entry.set_visible(entry.matches(query)) + + def __key_press_event_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + + vadjustment = self.props.vadjustment + if keyname == 'Up': + if vadjustment.props.value > vadjustment.props.lower: + vadjustment.props.value -= vadjustment.props.step_increment + elif keyname == 'Down': + max_value = vadjustment.props.upper - vadjustment.props.page_size + if vadjustment.props.value < max_value: + vadjustment.props.value = min( + vadjustment.props.value + vadjustment.props.step_increment, + max_value) + else: + return False + + return True + + def add_alert(self, alert): + if self._alert is not None: + self.remove_alert() + self._alert = alert + self.pack_start(alert, False) + self.reorder_child(alert, 0) + + def remove_alert(self): + self.remove(self._alert) + self._alert = None + +class ActivityIcon(CanvasIcon): + __gtype_name__ = 'SugarListActivityIcon' + + __gsignals__ = { + 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([str])), + } + + def __init__(self, activity_info): + CanvasIcon.__init__(self, size=style.STANDARD_ICON_SIZE, cache=True, + file_name=activity_info.icon) + self._activity_info = activity_info + self._uncolor() + self.connect('hovering-changed', self.__hovering_changed_event_cb) + self.connect('button-release-event', self.__button_release_event_cb) + + def create_palette(self): + palette = ActivityPalette(self._activity_info) + palette.connect('erase-activated', self.__erase_activated_cb) + return palette + + def __erase_activated_cb(self, palette): + self.emit('erase-activated', self._activity_info.bundle_id) + + def _color(self): + self.props.xo_color = profile.get_color() + + def _uncolor(self): + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + + def __hovering_changed_event_cb(self, icon, hovering): + if hovering: + self._color() + else: + self._uncolor() + + def __button_release_event_cb(self, icon, event): + self.palette.popdown(immediate=True) + self._uncolor() + + +class ActivityEntry(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarActivityEntry' + + _TITLE_COL_WIDTH = style.GRID_CELL_SIZE * 3 + _VERSION_COL_WIDTH = style.GRID_CELL_SIZE * 1 + _DATE_COL_WIDTH = style.GRID_CELL_SIZE * 5 + + def __init__(self, activity_info): + hippo.CanvasBox.__init__(self, spacing=style.DEFAULT_SPACING, + padding_top=style.DEFAULT_PADDING, + padding_bottom=style.DEFAULT_PADDING, + padding_left=style.DEFAULT_PADDING * 2, + padding_right=style.DEFAULT_PADDING * 2, + box_height=style.GRID_CELL_SIZE, + orientation=hippo.ORIENTATION_HORIZONTAL) + + registry = activity.get_registry() + registry.connect('activity-changed', self.__activity_changed_cb) + + self._bundle_id = activity_info.bundle_id + self._version = activity_info.version + self._favorite = activity_info.favorite + self._title = activity_info.name + self._installation_time = activity_info.installation_time + + self._favorite_icon = FavoriteIcon(self._favorite) + self._favorite_icon.connect('notify::favorite', + self.__favorite_changed_cb) + self.append(self._favorite_icon) + + self.icon = ActivityIcon(activity_info) + self.icon.connect('button-release-event', + self.__icon_button_release_event_cb) + self.append(self.icon) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + align = hippo.ALIGNMENT_END + else: + align = hippo.ALIGNMENT_START + + title = hippo.CanvasText(text=activity_info.name, + xalign=align, + font_desc=style.FONT_BOLD.get_pango_desc(), + box_width=ActivityEntry._TITLE_COL_WIDTH) + self.append(title) + + version = hippo.CanvasText(text=activity_info.version, + xalign=hippo.ALIGNMENT_END, + font_desc=style.FONT_NORMAL.get_pango_desc(), + box_width=ActivityEntry._VERSION_COL_WIDTH) + self.append(version) + + expander = hippo.CanvasBox() + self.append(expander, hippo.PACK_EXPAND) + + timestamp = activity_info.installation_time + date = hippo.CanvasText( + text=util.timestamp_to_elapsed_string(timestamp), + xalign=align, + font_desc=style.FONT_NORMAL.get_pango_desc(), + box_width=ActivityEntry._DATE_COL_WIDTH) + self.append(date) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + self.reverse() + + def __favorite_changed_cb(self, favorite_icon, pspec): + registry = activity.get_registry() + registry.set_activity_favorite(self._bundle_id, self._version, + favorite_icon.props.favorite) + + def __activity_changed_cb(self, activity_registry, activity_info): + if self._bundle_id == activity_info.bundle_id and \ + self._version == activity_info.version: + self._title = activity_info.name + self._favorite = activity_info.favorite + self._favorite_icon.props.favorite = self._favorite + + def __icon_button_release_event_cb(self, icon, event): + view.Shell.get_instance().start_activity(self._bundle_id) + + def get_bundle_id(self): + return self._bundle_id + + def get_version(self): + return self._version + + def get_installation_time(self): + return self._installation_time + + def matches(self, query): + if not query: + return True + return self._title.lower().find(query) > -1 + +class FavoriteIcon(CanvasIcon): + def __init__(self, favorite): + CanvasIcon.__init__(self, icon_name='emblem-favorite', + box_width=style.GRID_CELL_SIZE*3/5, + size=style.SMALL_ICON_SIZE) + self._favorite = None + self.set_favorite(favorite) + self.connect('button-release-event', self.__release_event_cb) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + def set_favorite(self, favorite): + if favorite == self._favorite: + return + + self._favorite = favorite + if favorite: + self.props.xo_color = profile.get_color() + else: + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.fill_color = style.COLOR_WHITE.get_svg() + + def get_favorite(self): + return self._favorite + + favorite = gobject.property( + type=bool, default=False, getter=get_favorite, setter=set_favorite) + + def __release_event_cb(self, icon, event): + self.props.favorite = not self.props.favorite + + def __motion_notify_event_cb(self, icon, event): + if not self._favorite: + if event.detail == hippo.MOTION_DETAIL_ENTER: + icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() diff --git a/src/jarabe/view/home/favoriteslayout.py b/src/jarabe/view/home/favoriteslayout.py new file mode 100644 index 0000000..e6b5c83 --- /dev/null +++ b/src/jarabe/view/home/favoriteslayout.py @@ -0,0 +1,485 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import math +import hashlib +from gettext import gettext as _ + +import gobject +import gtk +import hippo + +from sugar.graphics import style +from sugar import activity + +from jarabe.view.home.grid import Grid + +_logger = logging.getLogger('FavoritesLayout') + +_CELL_SIZE = 4 +_BASE_SCALE = 1000 + +class FavoritesLayout(gobject.GObject, hippo.CanvasLayout): + """Base class of the different layout types.""" + + __gtype_name__ = 'FavoritesLayout' + + def __init__(self): + gobject.GObject.__init__(self) + self.box = None + self.fixed_positions = {} + + def do_set_box(self, box): + self.box = box + + def do_get_height_request(self, for_width): + return 0, gtk.gdk.screen_height() - style.GRID_CELL_SIZE + + def do_get_width_request(self): + return 0, gtk.gdk.screen_width() + + def compare_activities(self, icon_a, icon_b): + return 0 + + def append(self, icon, locked=False): + self.box.insert_sorted(icon, 0, self.compare_activities) + if hasattr(icon, 'fixed_position'): + relative_x, relative_y = icon.fixed_position + if relative_x >= 0 and relative_y >= 0: + min_width_, width = self.box.get_width_request() + min_height_, height = self.box.get_height_request(width) + self.fixed_positions[icon] = \ + (int(relative_x * _BASE_SCALE / float(width)), + int(relative_y * _BASE_SCALE / float(height))) + + def remove(self, icon): + if icon in self.fixed_positions: + del self.fixed_positions[icon] + self.box.remove(icon) + + def move_icon(self, icon, x, y, locked=False): + if icon not in self.box.get_children(): + raise ValueError('Child not in box.') + + if hasattr(icon, 'get_bundle_id') and hasattr(icon, 'get_version'): + min_width_, width = self.box.get_width_request() + min_height_, height = self.box.get_height_request(width) + registry = activity.get_registry() + registry.set_activity_position( + icon.get_bundle_id(), icon.get_version(), + x * width / float(_BASE_SCALE), + y * height / float(_BASE_SCALE)) + self.fixed_positions[icon] = (x, y) + + def do_allocate(self, x, y, width, height, req_width, req_height, + origin_changed): + raise NotImplementedError() + + def allow_dnd(self): + return False + +class RandomLayout(FavoritesLayout): + """Lay out icons randomly; try to nudge them around to resolve overlaps.""" + + __gtype_name__ = 'RandomLayout' + + icon_name = 'view-freeform' + """Name of icon used in home view dropdown palette.""" + + profile_key = 'random-layout' + """String used in profile to represent this view.""" + + # TRANS: label for the freeform layout in the favorites view + palette_name = _('Freeform') + """String used to identify this layout in home view dropdown palette.""" + + def __init__(self): + FavoritesLayout.__init__(self) + + min_width_, width = self.do_get_width_request() + min_height_, height = self.do_get_height_request(width) + + self._grid = Grid(width / _CELL_SIZE, height / _CELL_SIZE) + self._grid.connect('child-changed', self.__grid_child_changed_cb) + + def __grid_child_changed_cb(self, grid, child): + child.emit_request_changed() + + def append(self, icon, locked=False): + FavoritesLayout.append(self, icon, locked) + + min_width_, child_width = icon.get_width_request() + min_height_, child_height = icon.get_height_request(child_width) + min_width_, width = self.box.get_width_request() + min_height_, height = self.box.get_height_request(width) + + if icon in self.fixed_positions: + x, y = self.fixed_positions[icon] + x = min(x, width - child_width) + y = min(y, height - child_height) + elif hasattr(icon, 'get_bundle_id'): + name_hash = hashlib.md5(icon.get_bundle_id()) + x = int(name_hash.hexdigest()[:5], 16) % (width - child_width) + y = int(name_hash.hexdigest()[-5:], 16) % (height - child_height) + else: + x = None + y = None + + if x is None or y is None: + self._grid.add(icon, + child_width / _CELL_SIZE, child_height / _CELL_SIZE) + else: + self._grid.add(icon, + child_width / _CELL_SIZE, child_height / _CELL_SIZE, + x / _CELL_SIZE, y / _CELL_SIZE) + + def remove(self, icon): + self._grid.remove(icon) + FavoritesLayout.remove(self, icon) + + def move_icon(self, icon, x, y, locked=False): + self._grid.move(icon, x / _CELL_SIZE, y / _CELL_SIZE, locked) + FavoritesLayout.move_icon(self, icon, x, y, locked) + + def do_allocate(self, x, y, width, height, req_width, req_height, + origin_changed): + for child in self.box.get_layout_children(): + # We need to always get requests to not confuse hippo + min_w_, child_width = child.get_width_request() + min_h_, child_height = child.get_height_request(child_width) + + rect = self._grid.get_child_rect(child.item) + child.allocate(rect.x * _CELL_SIZE, + rect.y * _CELL_SIZE, + child_width, + child_height, + origin_changed) + + def allow_dnd(self): + return True + +_MINIMUM_RADIUS = style.XLARGE_ICON_SIZE / 2 + style.DEFAULT_SPACING + \ + style.STANDARD_ICON_SIZE * 2 +_MAXIMUM_RADIUS = (gtk.gdk.screen_height() - style.GRID_CELL_SIZE) / 2 - \ + style.STANDARD_ICON_SIZE - style.DEFAULT_SPACING + +class RingLayout(FavoritesLayout): + """Lay out icons in a ring around the XO man.""" + + __gtype_name__ = 'RingLayout' + icon_name = 'view-radial' + """Name of icon used in home view dropdown palette.""" + profile_key = 'ring-layout' + """String used in profile to represent this view.""" + # TRANS: label for the ring layout in the favorites view + palette_name = _('Ring') + """String used to identify this layout in home view dropdown palette.""" + + def __init__(self): + FavoritesLayout.__init__(self) + self._locked_children = {} + + def append(self, icon, locked=False): + FavoritesLayout.append(self, icon, locked) + if locked: + child = self.box.find_box_child(icon) + self._locked_children[child] = (0, 0) + + def remove(self, icon): + child = self.box.find_box_child(icon) + if child in self._locked_children: + del self._locked_children[child] + FavoritesLayout.remove(self, icon) + + def move_icon(self, icon, x, y, locked=False): + FavoritesLayout.move_icon(self, icon, x, y, locked) + if locked: + child = self.box.find_box_child(icon) + self._locked_children[child] = (x, y) + + def _calculate_radius_and_icon_size(self, children_count): + # what's the radius required without downscaling? + distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING + icon_size = style.STANDARD_ICON_SIZE + # circumference is 2*pi*r; we want this to be at least + # 'children_count * distance' + radius = children_count * distance / (2 * math.pi) + # limit computed radius to reasonable bounds. + radius = max(radius, _MINIMUM_RADIUS) + radius = min(radius, _MAXIMUM_RADIUS) + # recompute icon size from limited radius + if children_count > 0: + icon_size = (2 * math.pi * radius / children_count) \ + - style.DEFAULT_SPACING + # limit adjusted icon size. + icon_size = max(icon_size, style.SMALL_ICON_SIZE) + icon_size = min(icon_size, style.MEDIUM_ICON_SIZE) + return radius, icon_size + + def _calculate_position(self, radius, icon_size, index, children_count, + sin=math.sin, cos=math.cos): + width, height = self.box.get_allocation() + angle = index * (2 * math.pi / children_count) - math.pi / 2 + x = radius * cos(angle) + (width - icon_size) / 2 + y = radius * sin(angle) + (height - icon_size - + (style.GRID_CELL_SIZE/2) ) / 2 + return x, y + + def _get_children_in_ring(self): + children_in_ring = [child for child in self.box.get_layout_children() \ + if child not in self._locked_children] + return children_in_ring + + def _update_icon_sizes(self): + # XXX: THIS METHOD IS NEVER CALLED + children_in_ring = self._get_children_in_ring() + radius_, icon_size = \ + self._calculate_radius_and_icon_size(len(children_in_ring)) + + for child in children_in_ring: + child.item.props.size = icon_size + + def do_allocate(self, x, y, width, height, req_width, req_height, + origin_changed): + children_in_ring = self._get_children_in_ring() + if children_in_ring: + radius, icon_size = \ + self._calculate_radius_and_icon_size(len(children_in_ring)) + + for n in range(len(children_in_ring)): + child = children_in_ring[n] + + x, y = self._calculate_position(radius, icon_size, n, + len(children_in_ring)) + + # We need to always get requests to not confuse hippo + min_w_, child_width = child.get_width_request() + min_h_, child_height = child.get_height_request(child_width) + + child.allocate(int(x), int(y), child_width, child_height, + origin_changed) + child.item.props.size = icon_size + + for child in self._locked_children.keys(): + x, y = self._locked_children[child] + + # We need to always get requests to not confuse hippo + min_w_, child_width = child.get_width_request() + min_h_, child_height = child.get_height_request(child_width) + + child.allocate(int(x), int(y), child_width, child_height, + origin_changed) + + def compare_activities(self, icon_a, icon_b): + if hasattr(icon_a, 'installation_time') and \ + hasattr(icon_b, 'installation_time'): + return icon_b.installation_time - icon_a.installation_time + else: + return 0 + +_SUNFLOWER_CONSTANT = style.STANDARD_ICON_SIZE * .75 +"""Chose a constant such that STANDARD_ICON_SIZE icons are nicely spaced.""" + +_SUNFLOWER_OFFSET = \ + math.pow((style.XLARGE_ICON_SIZE / 2 + style.STANDARD_ICON_SIZE) / + _SUNFLOWER_CONSTANT, 2) +""" +Compute a starting index for the `SunflowerLayout` which leaves space for +the XO man in the center. Since r = _SUNFLOWER_CONSTANT * sqrt(n), +solve for n when r is (XLARGE_ICON_SIZE + STANDARD_ICON_SIZE)/2. +""" + +_GOLDEN_RATIO = 1.6180339887498949 +""" +Golden ratio: http://en.wikipedia.org/wiki/Golden_ratio +Calculation: (math.sqrt(5) + 1) / 2 +""" + +_SUNFLOWER_ANGLE = 2.3999632297286531 +""" +The sunflower angle is approximately 137.5 degrees. +This is the golden angle: http://en.wikipedia.org/wiki/Golden_angle +Calculation: math.radians(360) / ( _GOLDEN_RATIO * _GOLDEN_RATIO ) +""" + +class SunflowerLayout(RingLayout): + """Spiral layout based on Fibonacci ratio in phyllotaxis. + + See http://algorithmicbotany.org/papers/abop/abop-ch4.pdf + for details of Vogel's model of florets in a sunflower head.""" + + __gtype_name__ = 'SunflowerLayout' + + icon_name = 'view-spiral' + """Name of icon used in home view dropdown palette.""" + + profile_key = 'spiral-layout' + """String used in profile to represent this view.""" + + # TRANS: label for the spiral layout in the favorites view + palette_name = _('Spiral') + """String used to identify this layout in home view dropdown palette.""" + + def __init__(self): + RingLayout.__init__(self) + self.skipped_indices = [] + + def _calculate_radius_and_icon_size(self, children_count): + """Stub out this method; not used in `SunflowerLayout`.""" + return None, style.STANDARD_ICON_SIZE + + def adjust_index(self, i): + """Skip floret indices which end up outside the desired bounding box.""" + for idx in self.skipped_indices: + if i < idx: + break + i += 1 + return i + + def _calculate_position(self, radius, icon_size, oindex, children_count, + sin=math.sin, cos=math.cos): + """Calculate the position of sunflower floret number 'oindex'. + If the result is outside the bounding box, use the next index which + is inside the bounding box.""" + + width, height = self.box.get_allocation() + + while True: + + index = self.adjust_index(oindex) + + # tweak phi to get a nice gap lined up where the "active activity" + # icon is, below the central XO man. + phi = index * _SUNFLOWER_ANGLE + math.radians(-130) + + # we offset index when computing r to make space for the XO man. + r = _SUNFLOWER_CONSTANT * math.sqrt(index + _SUNFLOWER_OFFSET) + + # x,y are the top-left corner of the icon, so remove icon_size + # from width/height to compensate. y has an extra GRID_CELL_SIZE/2 + # removed to make room for the "active activity" icon. + x = r * cos(phi) + (width - icon_size) / 2 + y = r * sin(phi) + (height - icon_size - \ + (style.GRID_CELL_SIZE / 2) ) / 2 + + # skip allocations outside the allocation box. + # give up once we can't fit + if r < math.hypot(width / 2, height / 2): + if y < 0 or y > (height - icon_size) or \ + x < 0 or x > (width - icon_size): + self.skipped_indices.append(index) + continue # try again + + return x, y + +class BoxLayout(RingLayout): + """Lay out icons in a square around the XO man.""" + + __gtype_name__ = 'BoxLayout' + + icon_name = 'view-box' + """Name of icon used in home view dropdown palette.""" + + profile_key = 'box-layout' + """String used in profile to represent this view.""" + + # TRANS: label for the box layout in the favorites view + palette_name = _('Box') + """String used to identify this layout in home view dropdown palette.""" + + def __init__(self): + RingLayout.__init__(self) + + def _calculate_position(self, radius, icon_size, index, children_count, + sin=None, cos=None): + + # use "orthogonal" versions of cos and sin in order to square the + # circle and turn the 'ring view' into a 'box view' + def cos_d(d): + while d < 0: + d += 360 + if d < 45: + return 1 + if d < 135: + return (90 - d) / 45. + if d < 225: + return -1 + return cos_d(360 - d) # mirror around 180 + + cos = lambda r: cos_d(math.degrees(r)) + sin = lambda r: cos_d(math.degrees(r) - 90) + + return RingLayout._calculate_position\ + (self, radius, icon_size, index, children_count, + sin=sin, cos=cos) + +class TriangleLayout(RingLayout): + """Lay out icons in a triangle around the XO man.""" + + __gtype_name__ = 'TriangleLayout' + + icon_name = 'view-triangle' + """Name of icon used in home view dropdown palette.""" + + profile_key = 'triangle-layout' + """String used in profile to represent this view.""" + + # TRANS: label for the box layout in the favorites view + palette_name = _('Triangle') + """String used to identify this layout in home view dropdown palette.""" + + def __init__(self): + RingLayout.__init__(self) + + def _calculate_radius_and_icon_size(self, children_count): + # use slightly larger minimum radius than parent, because sides + # of triangle come awful close to the center. + radius, icon_size = \ + RingLayout._calculate_radius_and_icon_size(self, children_count) + return max(radius, _MINIMUM_RADIUS + style.MEDIUM_ICON_SIZE), icon_size + + def _calculate_position(self, radius, icon_size, index, children_count, + sin=math.sin, cos=math.cos): + # tweak cos and sin in order to make the 'ring' into an equilateral + # triangle. + + def cos_d(d): + while d < -90: + d += 360 + if d <= 30: + return (d + 90) / 120. + if d <= 90: + return (90 - d) / 60. + return -cos_d(180 - d) # mirror around 90 + + sqrt_3 = math.sqrt(3) + + def sin_d(d): + while d < -90: + d += 360 + if d <= 30: + return ((d + 90) / 120.) * sqrt_3 - 1 + if d <= 90: + return sqrt_3 - 1 + return sin_d(180 - d) # mirror around 90 + + cos = lambda r: cos_d(math.degrees(r)) + sin = lambda r: sin_d(math.degrees(r)) + + return RingLayout._calculate_position\ + (self, radius, icon_size, index, children_count, + sin=sin, cos=cos) diff --git a/src/jarabe/view/home/favoritesview.py b/src/jarabe/view/home/favoritesview.py new file mode 100644 index 0000000..3dae714 --- /dev/null +++ b/src/jarabe/view/home/favoritesview.py @@ -0,0 +1,456 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ + +import gobject +import gtk +import hippo + +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.graphics.icon import Icon, CanvasIcon +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.alert import Alert +from sugar.profile import get_profile +from sugar import activity + +import view.Shell +from jarabe.view.palettes import JournalPalette +from jarabe.view.palettes import CurrentActivityPalette, ActivityPalette +from jarabe.view.home.MyIcon import MyIcon +from jarabe.view.home import favoriteslayout +from jarabe.model import shellmodel +from jarabe.hardware import schoolserver +from jarabe.hardware.schoolserver import RegisterError +from jarabe.controlpanel.gui import ControlPanel +from session import get_session_manager + +_logger = logging.getLogger('FavoritesView') + +_ICON_DND_TARGET = ('activity-icon', gtk.TARGET_SAME_WIDGET, 0) + +# enumerate the various layout types we will display in the dropdown palette. +# add a constant for your layout here, and add it to the LAYOUT_MAP to get +# it to appear in the palette. +RING_LAYOUT, BOX_LAYOUT, TRIANGLE_LAYOUT, SUNFLOWER_LAYOUT, RANDOM_LAYOUT = \ + xrange(5) + +LAYOUT_MAP = {RING_LAYOUT: favoriteslayout.RingLayout, + #BOX_LAYOUT: favoriteslayout.BoxLayout, + #TRIANGLE_LAYOUT: favoriteslayout.TriangleLayout, + #SUNFLOWER_LAYOUT: favoriteslayout.SunflowerLayout, + RANDOM_LAYOUT: favoriteslayout.RandomLayout} +"""Map numeric layout identifiers to uninstantiated subclasses of +`FavoritesLayout` which implement the layouts. Additional information +about the layout can be accessed with fields of the class.""" + +class FavoritesView(hippo.Canvas): + __gtype_name__ = 'SugarFavoritesView' + + __gsignals__ = { + 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([str])), + } + + def __init__(self, **kwargs): + gobject.GObject.__init__(self, **kwargs) + + # DND stuff + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._last_clicked_icon = None + + self._box = hippo.CanvasBox() + self._box.props.background_color = style.COLOR_WHITE.get_int() + self.set_root(self._box) + + self._my_icon = None + self._current_activity = None + self._layout = None + self._alert = None + + registry = activity.get_registry() + registry.connect('activity-added', self.__activity_added_cb) + registry.connect('activity-removed', self.__activity_removed_cb) + registry.connect('activity-changed', self.__activity_changed_cb) + + # More DND stuff + self.add_events(gtk.gdk.BUTTON_PRESS_MASK | + gtk.gdk.POINTER_MOTION_HINT_MASK) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + self.connect('button-press-event', self.__button_press_event_cb) + self.connect('drag-begin', self.__drag_begin_cb) + self.connect('drag-motion', self.__drag_motion_cb) + self.connect('drag-drop', self.__drag_drop_cb) + self.connect('drag-data-received', self.__drag_data_received_cb) + + def _add_activity(self, activity_info): + icon = ActivityIcon(activity_info) + icon.connect('erase-activated', self.__erase_activated_cb) + icon.props.size = style.STANDARD_ICON_SIZE + self._layout.append(icon) + + def __erase_activated_cb(self, activity_icon, bundle_id): + self.emit('erase-activated', bundle_id) + + def _get_activities_cb(self, activity_list): + for info in activity_list: + if info.favorite and info.bundle_id != "org.laptop.JournalActivity": + self._add_activity(info) + + def __activity_added_cb(self, activity_registry, activity_info): + if activity_info.favorite and \ + activity_info.bundle_id != "org.laptop.JournalActivity": + self._add_activity(activity_info) + + def _find_activity_icon(self, bundle_id, version): + for icon in self._box.get_children(): + if isinstance(icon, ActivityIcon) and \ + icon.bundle_id == bundle_id and icon.version == version: + return icon + return None + + def __activity_removed_cb(self, activity_registry, activity_info): + icon = self._find_activity_icon(activity_info.bundle_id, + activity_info.version) + if icon is not None: + self._layout.remove(icon) + + def __activity_changed_cb(self, activity_registry, activity_info): + if activity_info.bundle_id == 'org.laptop.JournalActivity': + return + icon = self._find_activity_icon(activity_info.bundle_id, + activity_info.version) + if icon is not None: + self._box.remove(icon) + if activity_info.favorite: + self._add_activity(activity_info) + + def do_size_allocate(self, allocation): + width = allocation.width + height = allocation.height + + min_w_, my_icon_width = self._my_icon.get_width_request() + min_h_, my_icon_height = self._my_icon.get_height_request(my_icon_width) + x = (width - my_icon_width) / 2 + y = (height - my_icon_height - style.GRID_CELL_SIZE) / 2 + self._layout.move_icon(self._my_icon, x, y, locked=True) + + min_w_, icon_width = self._current_activity.get_width_request() + min_h_, icon_height = \ + self._current_activity.get_height_request(icon_width) + x = (width - icon_width) / 2 + y = (height - my_icon_height - style.GRID_CELL_SIZE) / 2 + \ + my_icon_height + style.DEFAULT_PADDING + self._layout.move_icon(self._current_activity, x, y, locked=True) + + hippo.Canvas.do_size_allocate(self, allocation) + + def enable_xo_palette(self): + self._my_icon.enable_palette() + if self._my_icon.register_menu is not None: + self._my_icon.register_menu.connect('activate', + self.__register_activate_cb) + + # TODO: Dnd methods. This should be merged somehow inside hippo-canvas. + def __button_press_event_cb(self, widget, event): + if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS: + self._last_clicked_icon = self._get_icon_at_coords(event.x, event.y) + if self._last_clicked_icon is not None: + self._pressed_button = event.button + self._press_start_x = event.x + self._press_start_y = event.y + + return False + + def _get_icon_at_coords(self, x, y): + for icon in self._box.get_children(): + icon_x, icon_y = icon.get_context().translate_to_widget(icon) + icon_width, icon_height = icon.get_allocation() + + if (x >= icon_x ) and (x <= icon_x + icon_width) and \ + (y >= icon_y ) and (y <= icon_y + icon_height) and \ + isinstance(icon, ActivityIcon): + return icon + return None + + def __motion_notify_event_cb(self, widget, event): + if not self._pressed_button: + return False + + # if the mouse button is not pressed, no drag should occurr + if not event.state & gtk.gdk.BUTTON1_MASK: + self._pressed_button = None + return False + + if event.is_hint: + x, y, state_ = event.window.get_pointer() + else: + x = event.x + y = event.y + + if widget.drag_check_threshold(int(self._press_start_x), + int(self._press_start_y), + int(x), + int(y)): + context_ = widget.drag_begin([_ICON_DND_TARGET], + gtk.gdk.ACTION_MOVE, + 1, + event) + return False + + def __drag_begin_cb(self, widget, context): + icon_file_name = self._last_clicked_icon.props.file_name + # TODO: we should get the pixbuf from the widget, so it has colors, etc + pixbuf = gtk.gdk.pixbuf_new_from_file(icon_file_name) + + hot_spot = style.zoom(10) + context.set_icon_pixbuf(pixbuf, hot_spot, hot_spot) + + def __drag_motion_cb(self, widget, context, x, y, time): + if self._last_clicked_icon is not None: + context.drag_status(context.suggested_action, time) + return True + else: + return False + + def __drag_drop_cb(self, widget, context, x, y, time): + if self._last_clicked_icon is not None: + self.drag_get_data(context, _ICON_DND_TARGET[0]) + + self._layout.move_icon(self._last_clicked_icon, x, y) + + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._last_clicked_icon = None + + return True + else: + return False + + def __drag_data_received_cb(self, widget, context, x, y, selection_data, + info, time): + context.drop_finish(success=True, time=time) + + def _set_layout(self, layout): + if layout not in LAYOUT_MAP: + raise ValueError('Unknown favorites layout: %r' % layout) + if type(self._layout) != LAYOUT_MAP[layout]: + self._box.clear() + self._layout = LAYOUT_MAP[layout]() + self._box.set_layout(self._layout) + + self._my_icon = _MyIcon(style.XLARGE_ICON_SIZE) + self._layout.append(self._my_icon, locked=True) + + self._current_activity = CurrentActivityIcon() + self._layout.append(self._current_activity, locked=True) + + registry = activity.get_registry() + registry.get_activities_async(reply_handler=self._get_activities_cb) + + if self._layout.allow_dnd(): + self.drag_source_set(0, [], 0) + self.drag_dest_set(0, [], 0) + else: + self.drag_source_unset() + self.drag_dest_unset() + + layout = property(None, _set_layout) + + def add_alert(self, alert): + if self._alert is not None: + self.remove_alert() + alert.set_size_request(gtk.gdk.screen_width(), -1) + self._alert = hippo.CanvasWidget(widget=alert) + self._box.append(self._alert, hippo.PACK_FIXED) + + def remove_alert(self): + self._box.remove(self._alert) + self._alert = None + + def __register_activate_cb(self, menuitem): + alert = Alert() + try: + schoolserver.register_laptop() + except RegisterError, e: + alert.props.title = _('Registration Failed') + alert.props.msg = _('%s') % e + else: + alert.props.title = _('Registration Successful') + alert.props.msg = _('You are now registered ' \ + 'with your school server.') + palette = self._my_icon.get_palette() + palette.menu.remove(menuitem) + + ok_icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_OK, _('Ok'), ok_icon) + + self.add_alert(alert) + alert.connect('response', self.__register_alert_response_cb) + + def __register_alert_response_cb(self, alert, response_id): + self.remove_alert() + +class ActivityIcon(CanvasIcon): + __gtype_name__ = 'SugarFavoriteActivityIcon' + + __gsignals__ = { + 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([str])), + } + + def __init__(self, activity_info): + CanvasIcon.__init__(self, cache=True, file_name=activity_info.icon) + self._activity_info = activity_info + self._uncolor() + self.connect('hovering-changed', self.__hovering_changed_event_cb) + self.connect('button-release-event', self.__button_release_event_cb) + + def create_palette(self): + palette = ActivityPalette(self._activity_info) + palette.connect('erase-activated', self.__erase_activated_cb) + return palette + + def __erase_activated_cb(self, palette): + self.emit('erase-activated', self._activity_info.bundle_id) + + def _color(self): + self.props.xo_color = get_profile().color + + def _uncolor(self): + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + + def __hovering_changed_event_cb(self, icon, hovering): + if hovering: + self._color() + else: + self._uncolor() + + def __button_release_event_cb(self, icon, event): + self.palette.popdown(immediate=True) + self._uncolor() + view.Shell.get_instance().start_activity(self._activity_info.bundle_id) + + def get_bundle_id(self): + return self._activity_info.bundle_id + bundle_id = property(get_bundle_id, None) + + def get_version(self): + return self._activity_info.version + version = property(get_version, None) + + def _get_installation_time(self): + return self._activity_info.installation_time + installation_time = property(_get_installation_time, None) + + def _get_fixed_position(self): + return self._activity_info.position + fixed_position = property(_get_fixed_position, None) + +class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem): + def __init__(self): + CanvasIcon.__init__(self, cache=True) + self._home_model = shellmodel.get_instance().get_home() + + if self._home_model.get_active_activity() is not None: + self._update(self._home_model.get_active_activity()) + + self._home_model.connect('active-activity-changed', + self.__active_activity_changed_cb) + + self.connect('button-release-event', self.__button_release_event_cb) + + def __button_release_event_cb(self, icon, event): + self._home_model.get_active_activity().get_window().activate(1) + + def _update(self, home_activity): + self.props.file_name = home_activity.get_icon_path() + self.props.xo_color = home_activity.get_icon_color() + self.props.size = style.STANDARD_ICON_SIZE + + if self.palette is not None: + self.palette.destroy() + self.palette = None + + if home_activity.is_journal(): + palette = JournalPalette(home_activity) + else: + palette = CurrentActivityPalette(home_activity) + self.set_palette(palette) + + def __active_activity_changed_cb(self, home_model, home_activity): + self._update(home_activity) + +class _MyIcon(MyIcon): + def __init__(self, scale): + MyIcon.__init__(self, scale) + + self._power_manager = None + self._profile = get_profile() + self.register_menu = None + + def enable_palette(self): + palette_icon = Icon(icon_name='computer-xo', + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR, + xo_color=self._profile.color) + palette = Palette(self._profile.nick_name, + #secondary_text='Sample secondary label', + icon=palette_icon) + + item = MenuItem(_('Settings'), 'preferences-system') + item.connect('activate', self.__controlpanel_activate_cb) + palette.menu.append(item) + item.show() + + item = MenuItem(_('Restart'), 'system-restart') + item.connect('activate', self._reboot_activate_cb) + palette.menu.append(item) + item.show() + + item = MenuItem(_('Shutdown'), 'system-shutdown') + item.connect('activate', self._shutdown_activate_cb) + palette.menu.append(item) + item.show() + + if not self._profile.is_registered(): + self.register_menu = MenuItem(_('Register'), 'media-record') + palette.menu.append(self.register_menu) + self.register_menu.show() + + self.set_palette(palette) + + def _reboot_activate_cb(self, menuitem): + session_manager = get_session_manager() + session_manager.reboot() + + def _shutdown_activate_cb(self, menuitem): + session_manager = get_session_manager() + session_manager.shutdown() + + def get_toplevel(self): + return hippo.get_canvas_for_item(self).get_toplevel() + + def __controlpanel_activate_cb(self, menuitem): + panel = ControlPanel() + panel.set_transient_for(self.get_toplevel()) + panel.show() diff --git a/src/jarabe/view/home/grid.py b/src/jarabe/view/home/grid.py new file mode 100644 index 0000000..eb7fabc --- /dev/null +++ b/src/jarabe/view/home/grid.py @@ -0,0 +1,201 @@ +# Copyright (C) 2007 Red Hat, Inc. +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import random + +import gobject +import gtk + +from sugar import _sugarext + +_PLACE_TRIALS = 20 +_MAX_WEIGHT = 255 +_REFRESH_RATE = 200 +_MAX_COLLISIONS_PER_REFRESH = 20 + +class Grid(_sugarext.Grid): + __gsignals__ = { + 'child-changed' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + def __init__(self, width, height): + gobject.GObject.__init__(self) + + self.width = width + self.height = height + self._children = [] + self._child_rects = {} + self._locked_children = set() + self._collisions = [] + self._collisions_sid = 0 + + self.setup(self.width, self.height) + + def add(self, child, width, height, x=None, y=None, locked=False): + if x is not None and y is not None: + rect = gtk.gdk.Rectangle(x, y, width, height) + weight = self.compute_weight(rect) + else: + trials = _PLACE_TRIALS + weight = _MAX_WEIGHT + while trials > 0 and weight: + x = int(random.random() * (self.width - width)) + y = int(random.random() * (self.height - height)) + + rect = gtk.gdk.Rectangle(x, y, width, height) + new_weight = self.compute_weight(rect) + if weight > new_weight: + weight = new_weight + + trials -= 1 + + self._child_rects[child] = rect + self._children.append(child) + self.add_weight(self._child_rects[child]) + if locked: + self._locked_children.add(child) + + if weight > 0: + self._detect_collisions(child) + + def remove(self, child): + self._children.remove(child) + self.remove_weight(self._child_rects[child]) + self._locked_children.discard(child) + del self._child_rects[child] + + if child in self._collisions: + self._collisions.remove(child) + + def move(self, child, x, y, locked=False): + self.remove_weight(self._child_rects[child]) + + rect = self._child_rects[child] + rect.x = x + rect.y = y + + weight = self.compute_weight(rect) + self.add_weight(self._child_rects[child]) + + if locked: + self._locked_children.add(child) + else: + self._locked_children.discard(child) + + if weight > 0: + self._detect_collisions(child) + + def _shift_child(self, child, weight): + rect = self._child_rects[child] + + new_rects = [] + + # Get rects right, left, bottom and top + if (rect.x + rect.width < self.width - 1): + new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y, + rect.width, rect.height)) + + if (rect.x - 1 > 0): + new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y, + rect.width, rect.height)) + + if (rect.y + rect.height < self.height - 1): + new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y + 1, + rect.width, rect.height)) + + if (rect.y - 1 > 0): + new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y - 1, + rect.width, rect.height)) + + # Get diagonal rects + if rect.x + rect.width < self.width - 1 and \ + rect.y + rect.height < self.height - 1: + new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y + 1, + rect.width, rect.height)) + + if rect.x - 1 > 0 and rect.y + rect.height < self.height - 1: + new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y + 1, + rect.width, rect.height)) + + if rect.x + rect.width < self.width - 1 and rect.y - 1 > 0: + new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y - 1, + rect.width, rect.height)) + + if rect.x - 1 > 0 and rect.y - 1 > 0: + new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y - 1, + rect.width, rect.height)) + + random.shuffle(new_rects) + + best_rect = None + for new_rect in new_rects: + new_weight = self.compute_weight(new_rect) + if new_weight < weight: + best_rect = new_rect + weight = new_weight + + if best_rect: + self._child_rects[child] = best_rect + weight = self._shift_child(child, weight) + + return weight + + def __solve_collisions_cb(self): + for i_ in range(_MAX_COLLISIONS_PER_REFRESH): + collision = self._collisions.pop(0) + + old_rect = self._child_rects[collision] + self.remove_weight(old_rect) + weight = self.compute_weight(old_rect) + weight = self._shift_child(collision, weight) + self.add_weight(self._child_rects[collision]) + + # TODO: we shouldn't give up the first time we failed to find a + # better position. + if old_rect != self._child_rects[collision]: + self._detect_collisions(collision) + self.emit('child-changed', collision) + if weight > 0: + self._collisions.append(collision) + + if not self._collisions: + self._collisions_sid = 0 + return False + + return True + + def _detect_collisions(self, child): + collision_found = False + child_rect = self._child_rects[child] + for c in self._children: + intersection = child_rect.intersect(self._child_rects[c]) + if c != child and intersection.width > 0: + if c not in self._locked_children and c not in self._collisions: + collision_found = True + self._collisions.append(c) + + if collision_found: + if child not in self._collisions: + self._collisions.append(child) + + if self._collisions and not self._collisions_sid: + self._collisions_sid = gobject.timeout_add(_REFRESH_RATE, + self.__solve_collisions_cb, priority=gobject.PRIORITY_LOW) + + def get_child_rect(self, child): + return self._child_rects[child] diff --git a/src/jarabe/view/home/proc_smaps.py b/src/jarabe/view/home/proc_smaps.py new file mode 100755 index 0000000..090a4cf --- /dev/null +++ b/src/jarabe/view/home/proc_smaps.py @@ -0,0 +1,107 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 +# USA + +import os + +# /proc/PID/maps consists of a number of lines like this: +# 00400000-004b1000 r-xp 00000000 fd:00 5767206 /bin/bash +# 006b1000-006bb000 rw-p 000b1000 fd:00 5767206 /bin/bash +# 006bb000-006c0000 rw-p 006bb000 00:00 0 +# ... +# The fields are: address, permissions, offset, device, inode, and +# (for non-anonymous mappings) pathname. +# +# /proc/PID/smaps gives additional information for each mapping: +# 00400000-004b1000 r-xp 00000000 fd:00 5767206 /bin/bash +# Size: 708 kB +# Rss: 476 kB +# Shared_Clean: 468 kB +# Shared_Dirty: 0 kB +# Private_Clean: 8 kB +# Private_Dirty: 0 kB +# Referenced: 0 kb +# +# The "Referenced" line only appears in kernel 2.6.22 and later. + +def get_shared_mapping_names(pid): + """Returns a set of the files for which PID has a shared mapping""" + + mappings = set() + infile = open("/proc/%s/maps" % pid, "r") + for line in infile: + # sharable mappings are non-anonymous and either read-only + # (permissions "r-..") or writable but explicitly marked + # shared ("rw.s") + fields = line.split() + if len(fields) < 6 or not fields[5].startswith('/'): + continue + if fields[1][0] != 'r' or (fields[1][1] == 'w' and fields[1][3] != 's'): + continue + mappings.add(fields[5]) + infile.close() + return mappings + +_smaps_lines_per_entry = None + +def get_mappings(pid, ignored_shared_mappings): + """Returns a list of (name, private, shared) tuples describing the + memory mappings of PID. Shared mappings named in + ignored_shared_mappings are ignored + """ + + global _smaps_lines_per_entry + if _smaps_lines_per_entry is None: + if os.path.isfile('/proc/%s/clear_refs' % os.getpid()): + _smaps_lines_per_entry = 8 + else: + _smaps_lines_per_entry = 7 + + mappings = [] + + smapfile = "/proc/%s/smaps" % pid + infile = open(smapfile, "r") + data = infile.read() + infile.close() + lines = data.splitlines() + + for line_idx in range(0, len(lines), _smaps_lines_per_entry): + name_idx = lines[line_idx].find('/') + if name_idx == -1: + name = None + else: + name = lines[line_idx][name_idx:] + + private_clean = int(lines[line_idx + 5][14:-3]) + private_dirty = int(lines[line_idx + 6][14:-3]) + if name in ignored_shared_mappings: + shared_clean = 0 + shared_dirty = 0 + else: + shared_clean = int(lines[line_idx + 3][14:-3]) + shared_dirty = int(lines[line_idx + 4][14:-3]) + + mapping = Mapping(name, private_clean + private_dirty, + shared_clean + shared_dirty) + mappings.append (mapping) + + return mappings + +class Mapping: + def __init__ (self, name, private, shared): + self.name = name + self.private = private + self.shared = shared diff --git a/src/jarabe/view/home/snowflakelayout.py b/src/jarabe/view/home/snowflakelayout.py new file mode 100644 index 0000000..5782cff --- /dev/null +++ b/src/jarabe/view/home/snowflakelayout.py @@ -0,0 +1,108 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import math + +import gobject +import hippo + +from sugar.graphics import style + +_BASE_DISTANCE = style.zoom(25) +_CHILDREN_FACTOR = style.zoom(3) + +class SnowflakeLayout(gobject.GObject, hippo.CanvasLayout): + __gtype_name__ = 'SugarSnowflakeLayout' + def __init__(self): + gobject.GObject.__init__(self) + self._nflakes = 0 + self._box = None + + def add(self, child, center=False): + if not center: + self._nflakes += 1 + + self._box.append(child) + + box_child = self._box.find_box_child(child) + box_child.is_center = center + + def remove(self, child): + box_child = self._box.find_box_child(child) + if not box_child.is_center: + self._nflakes -= 1 + + self._box.remove(child) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + size = self._calculate_size() + return (size, size) + + def do_get_width_request(self): + size = self._calculate_size() + return (size, size) + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + r = self._get_radius() + index = 0 + + for child in self._box.get_layout_children(): + min_width, child_width = child.get_width_request() + min_height, child_height = child.get_height_request(child_width) + + if child.is_center: + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + child_width, child_height, origin_changed) + else: + angle = 2 * math.pi * index / self._nflakes + + if self._nflakes != 2: + angle -= math.pi / 2 + + dx = math.cos(angle) * r + dy = math.sin(angle) * r + + child_x = int(x + (width - child_width) / 2 + dx) + child_y = int(y + (height - child_height) / 2 + dy) + + child.allocate(child_x, child_y, child_width, + child_height, origin_changed) + + index += 1 + + def _get_radius(self): + radius = int(_BASE_DISTANCE + _CHILDREN_FACTOR * self._nflakes) + for child in self._box.get_layout_children(): + if child.is_center: + [min_w, child_w] = child.get_width_request() + [min_h, child_h] = child.get_height_request(child_w) + radius += max(child_w, child_h) / 2 + + return radius + + def _calculate_size(self): + thickness = 0 + for child in self._box.get_layout_children(): + [min_width, child_width] = child.get_width_request() + [min_height, child_height] = child.get_height_request(child_width) + thickness = max(thickness, max(child_width, child_height)) + + return self._get_radius() * 2 + thickness diff --git a/src/jarabe/view/home/spreadlayout.py b/src/jarabe/view/home/spreadlayout.py new file mode 100644 index 0000000..72261e7 --- /dev/null +++ b/src/jarabe/view/home/spreadlayout.py @@ -0,0 +1,83 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo +import gobject +import gtk + +from sugar.graphics import style + +from jarabe.view.home.grid import Grid + +_CELL_SIZE = 4.0 + +class SpreadLayout(gobject.GObject, hippo.CanvasLayout): + __gtype_name__ = 'SugarSpreadLayout' + def __init__(self): + gobject.GObject.__init__(self) + self._box = None + + min_width, width = self.do_get_width_request() + min_height, height = self.do_get_height_request(width) + + self._grid = Grid(int(width / _CELL_SIZE), int(height / _CELL_SIZE)) + self._grid.connect('child-changed', self._grid_child_changed_cb) + + def add(self, child): + self._box.append(child) + + width, height = self._get_child_grid_size(child) + self._grid.add(child, width, height) + + def remove(self, child): + self._grid.remove(child) + self._box.remove(child) + + def move(self, child, x, y): + self._grid.move(child, x / _CELL_SIZE, y / _CELL_SIZE, locked=True) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + return 0, gtk.gdk.screen_height() - style.GRID_CELL_SIZE + + def do_get_width_request(self): + return 0, gtk.gdk.screen_width() + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + for child in self._box.get_layout_children(): + # We need to always get requests to not confuse hippo + min_w, child_width = child.get_width_request() + min_h, child_height = child.get_height_request(child_width) + + rect = self._grid.get_child_rect(child.item) + child.allocate(int(round(rect.x * _CELL_SIZE)), + int(round(rect.y * _CELL_SIZE)), + child_width, + child_height, + origin_changed) + + def _get_child_grid_size(self, child): + min_width, width = child.get_width_request() + min_height, height = child.get_height_request(width) + + return int(width / _CELL_SIZE), int(height / _CELL_SIZE) + + def _grid_child_changed_cb(self, grid, child): + child.emit_request_changed() + diff --git a/src/jarabe/view/home/transitionbox.py b/src/jarabe/view/home/transitionbox.py new file mode 100644 index 0000000..230b887 --- /dev/null +++ b/src/jarabe/view/home/transitionbox.py @@ -0,0 +1,97 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo +import gobject + +from sugar.graphics import style +from sugar.graphics import animator + +from jarabe.view.home.MyIcon import MyIcon + +class _Animation(animator.Animation): + def __init__(self, icon, start_size, end_size): + animator.Animation.__init__(self, 0.0, 1.0) + + self._icon = icon + self.start_size = start_size + self.end_size = end_size + + def next_frame(self, current): + d = (self.end_size - self.start_size) * current + self._icon.props.size = self.start_size + d + +class _Layout(gobject.GObject, hippo.CanvasLayout): + __gtype_name__ = 'SugarTransitionBoxLayout' + def __init__(self): + gobject.GObject.__init__(self) + self._box = None + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + return 0, 0 + + def do_get_width_request(self): + return 0, 0 + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + for child in self._box.get_layout_children(): + min_width, child_width = child.get_width_request() + min_height, child_height = child.get_height_request(child_width) + + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + child_width, child_height, origin_changed) + +class TransitionBox(hippo.Canvas): + __gtype_name__ = 'SugarTransitionBox' + + __gsignals__ = { + 'completed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._box = hippo.CanvasBox() + self._box.props.background_color = style.COLOR_WHITE.get_int() + self.set_root(self._box) + + self._size = style.XLARGE_ICON_SIZE + + self._layout = _Layout() + self._box.set_layout(self._layout) + + self._my_icon = MyIcon(self._size) + self._box.append(self._my_icon) + + self._animator = animator.Animator(0.3) + self._animator.connect('completed', self._animation_completed_cb) + + def _animation_completed_cb(self, anim): + self.emit('completed') + + def set_size(self, size): + self._animator.remove_all() + self._animator.add(_Animation(self._my_icon, self._size, size)) + self._animator.start() + + self._size = size + diff --git a/src/jarabe/view/keyhandler.py b/src/jarabe/view/keyhandler.py new file mode 100644 index 0000000..d6b7565 --- /dev/null +++ b/src/jarabe/view/keyhandler.py @@ -0,0 +1,289 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import signal +import logging +import subprocess +import errno + +import dbus +import gtk + +from sugar._sugarext import KeyGrabber + +from jarabe.hardware import hardwaremanager +import view.Shell +from jarabe.view.tabbinghandler import TabbingHandler +from jarabe.model.shellmodel import ShellModel + +_BRIGHTNESS_STEP = 2 +_VOLUME_STEP = hardwaremanager.VOL_CHANGE_INCREMENT_RECOMMENDATION +_BRIGHTNESS_MAX = 15 +_VOLUME_MAX = 100 +_TABBING_MODIFIER = gtk.gdk.MOD1_MASK + +_actions_table = { + 'F1' : 'zoom_mesh', + 'F2' : 'zoom_friends', + 'F3' : 'zoom_home', + 'F4' : 'zoom_activity', + 'F9' : 'brightness_down', + 'F10' : 'brightness_up', + '<alt>F9' : 'brightness_min', + '<alt>F10' : 'brightness_max', + 'F11' : 'volume_down', + 'F12' : 'volume_up', + '<alt>F11' : 'volume_min', + '<alt>F12' : 'volume_max', + '<alt>1' : 'screenshot', + '0x93' : 'frame', + '0xEB' : 'rotate', + '<alt>Tab' : 'next_window', + '<alt><shift>Tab': 'previous_window', + '<alt>Escape' : 'close_window', + '0xDC' : 'open_search', +# the following are intended for emulator users + '<alt><shift>f' : 'frame', + '<alt><shift>q' : 'quit_emulator', + '<alt><shift>o' : 'open_search', + '<alt><shift>r' : 'rotate', + '<alt><shift>s' : 'say_text', +} + +J_DBUS_SERVICE = 'org.laptop.Journal' +J_DBUS_PATH = '/org/laptop/Journal' +J_DBUS_INTERFACE = 'org.laptop.Journal' + +SPEECH_DBUS_SERVICE = 'org.laptop.Speech' +SPEECH_DBUS_PATH = '/org/laptop/Speech' +SPEECH_DBUS_INTERFACE = 'org.laptop.Speech' + +class KeyHandler(object): + def __init__(self): + self._screen_rotation = 0 + self._key_pressed = None + self._keycode_pressed = 0 + self._keystate_pressed = 0 + self._speech_proxy = None + + self._key_grabber = KeyGrabber() + self._key_grabber.connect('key-pressed', + self._key_pressed_cb) + self._key_grabber.connect('key-released', + self._key_released_cb) + + self._tabbing_handler = TabbingHandler(_TABBING_MODIFIER) + + self._key_grabber.grab_keys(_actions_table.keys()) + + def _change_volume(self, step=None, value=None): + hw_manager = hardwaremanager.get_manager() + + if step is not None: + volume = hw_manager.get_volume() + step + elif value is not None: + volume = value + + volume = min(max(0, volume), _VOLUME_MAX) + + hw_manager.set_volume(volume) + hw_manager.set_muted(volume == 0) + + def _change_brightness(self, step=None, value=None): + hw_manager = hardwaremanager.get_manager() + + if step is not None: + level = hw_manager.get_display_brightness() + step + elif value is not None: + level = value + + level = min(max(0, level), _BRIGHTNESS_MAX) + + hw_manager.set_display_brightness(level) + if level == 0: + hw_manager.set_display_mode(hardwaremanager.B_AND_W_MODE) + else: + hw_manager.set_display_mode(hardwaremanager.COLOR_MODE) + + def _get_speech_proxy(self): + if self._speech_proxy is None: + bus = dbus.SessionBus() + speech_obj = bus.get_object(SPEECH_DBUS_SERVICE, SPEECH_DBUS_PATH, + follow_name_owner_changes=True) + self._speech_proxy = dbus.Interface(speech_obj, + SPEECH_DBUS_INTERFACE) + return self._speech_proxy + + def _on_speech_err(self, ex): + logging.error("An error occurred with the ESpeak service: %r" % (ex, )) + + def _primary_selection_cb(self, clipboard, text, user_data): + logging.debug('KeyHandler._primary_selection_cb: %r' % text) + if text: + self._get_speech_proxy().SayText(text, reply_handler=lambda: None, \ + error_handler=self._on_speech_err) + + def handle_say_text(self): + clipboard = gtk.clipboard_get(selection="PRIMARY") + clipboard.request_text(self._primary_selection_cb) + + def handle_previous_window(self): + self._tabbing_handler.previous_activity() + + def handle_next_window(self): + self._tabbing_handler.next_activity() + + def handle_close_window(self): + view.Shell.get_instance().close_current_activity() + + def handle_zoom_mesh(self): + view.Shell.get_instance().set_zoom_level(ShellModel.ZOOM_MESH) + + def handle_zoom_friends(self): + view.Shell.get_instance().set_zoom_level(ShellModel.ZOOM_FRIENDS) + + def handle_zoom_home(self): + view.Shell.get_instance().set_zoom_level(ShellModel.ZOOM_HOME) + + def handle_zoom_activity(self): + view.Shell.get_instance().set_zoom_level(ShellModel.ZOOM_ACTIVITY) + + def handle_brightness_max(self): + self._change_brightness(value=_BRIGHTNESS_MAX) + + def handle_brightness_min(self): + self._change_brightness(value=0) + + def handle_volume_max(self): + self._change_volume(value=_VOLUME_MAX) + + def handle_volume_min(self): + self._change_volume(value=0) + + def handle_brightness_up(self): + self._change_brightness(step=_BRIGHTNESS_STEP) + + def handle_brightness_down(self): + self._change_brightness(step=-_BRIGHTNESS_STEP) + + def handle_volume_up(self): + self._change_volume(step=_VOLUME_STEP) + + def handle_volume_down(self): + self._change_volume(step=-_VOLUME_STEP) + + def handle_screenshot(self): + view.Shell.get_instance().take_screenshot() + + def handle_frame(self): + view.Shell.get_instance().get_frame().notify_key_press() + + + def handle_rotate(self): + """ + Handles rotation of the display (using xrandr) and of the d-pad. + + Notes: default mappings for keypad on MP + KP_Up 80 + KP_Right 85 + KP_Down 88 + KP_Left 83 + """ + + states = [ 'normal', 'left', 'inverted', 'right'] + keycodes = (80, 85, 88, 83, 80, 85, 88, 83) + keysyms = ("KP_Up", "KP_Right", "KP_Down", "KP_Left") + + self._screen_rotation -= 1 + self._screen_rotation %= 4 + + actual_keycodes = keycodes[self._screen_rotation:self._screen_rotation + + 4] + # code_pairs now contains a mapping of keycode -> keysym in the current + # orientation + code_pairs = zip(actual_keycodes, keysyms) + + # Using the mappings in code_pairs, we dynamically build up an xmodmap + # command to rotate the dpad keys. + argv = ['xmodmap'] + for arg in [('-e', 'keycode %i = %s' % p) for p in code_pairs]: + argv.extend(arg) + + # If either the xmodmap or xrandr command fails, check_call will fail + # with CalledProcessError, which we raise. + try: + subprocess.check_call(argv) + subprocess.check_call(['xrandr', '-o', + states[self._screen_rotation]]) + except OSError, e: + if e.errno != errno.EINTR: + raise + + + def handle_quit_emulator(self): + if os.environ.has_key('SUGAR_EMULATOR_PID'): + pid = int(os.environ['SUGAR_EMULATOR_PID']) + os.kill(pid, signal.SIGTERM) + + def focus_journal_search(self): + bus = dbus.SessionBus() + obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) + journal = dbus.Interface(obj, J_DBUS_INTERFACE) + journal.FocusSearch() + + def handle_open_search(self): + self.focus_journal_search() + + def _key_pressed_cb(self, grabber, keycode, state): + key = grabber.get_key(keycode, state) + logging.debug('_key_pressed_cb: %i %i %s' % (keycode, state, key)) + if key: + self._key_pressed = key + self._keycode_pressed = keycode + self._keystate_pressed = state + + action = _actions_table[key] + if self._tabbing_handler.is_tabbing(): + # Only accept window tabbing events, everything else + # cancels the tabbing operation. + if not action in ["next_window", "previous_window"]: + self._tabbing_handler.stop() + return True + + method = getattr(self, 'handle_' + action) + method() + + return True + else: + # If this is not a registered key, then cancel tabbing. + if self._tabbing_handler.is_tabbing(): + if not grabber.is_modifier(keycode): + self._tabbing_handler.stop() + return True + + return False + + def _key_released_cb(self, grabber, keycode, state): + if self._tabbing_handler.is_tabbing(): + # We stop tabbing and switch to the new window as soon as the + # modifier key is raised again. + if grabber.is_modifier(keycode, mask=_TABBING_MODIFIER): + self._tabbing_handler.stop() + + return True + return False + diff --git a/src/jarabe/view/launchwindow.py b/src/jarabe/view/launchwindow.py new file mode 100644 index 0000000..384e6ec --- /dev/null +++ b/src/jarabe/view/launchwindow.py @@ -0,0 +1,113 @@ +# Copyright (C) 2008, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import hippo +import gobject + +from sugar import wm +from sugar.graphics import style +from sugar.graphics import animator +from sugar.graphics.xocolor import XoColor + +from jarabe.model import shellmodel +from jarabe.view.pulsingicon import CanvasPulsingIcon + +class LaunchWindow(hippo.CanvasWindow): + def __init__(self, home_activity): + gobject.GObject.__init__( + self, type_hint=gtk.gdk.WINDOW_TYPE_HINT_NORMAL) + + self._activity_id = home_activity.get_activity_id() + self._box = LaunchBox(home_activity) + self.set_root(self._box) + + self.connect('realize', self.__realize_cb) + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self.__size_changed_cb) + + self._update_size() + + def show(self): + self.present() + self._box.zoom_in() + + def _update_size(self): + self.resize(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + + def __realize_cb(self, widget): + wm.set_activity_id(widget.window, str(self._activity_id)) + widget.window.property_change('_SUGAR_WINDOW_TYPE', 'STRING', 8, + gtk.gdk.PROP_MODE_REPLACE, 'launcher') + + def __size_changed_cb(self, screen): + self._update_size() + +class LaunchBox(hippo.CanvasBox): + def __init__(self, home_activity): + gobject.GObject.__init__(self, orientation=hippo.ORIENTATION_VERTICAL, + background_color=style.COLOR_WHITE.get_int()) + + self._home_activity = home_activity + self._activity_icon = CanvasPulsingIcon( + file_name=home_activity.get_icon_path(), + pulse_color=home_activity.get_icon_color()) + self.append(self._activity_icon, hippo.PACK_EXPAND) + + # FIXME support non-xo colors in CanvasPulsingIcon + self._activity_icon.props.base_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + + self._animator = animator.Animator(1.0) + + self._home = shellmodel.get_instance().get_home() + self._home.connect('active-activity-changed', + self.__active_activity_changed_cb) + + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, box): + self._home.disconnect_by_func(self.__active_activity_changed_cb) + + def zoom_in(self): + self._activity_icon.props.size = style.STANDARD_ICON_SIZE + + self._animator.remove_all() + self._animator.add(_Animation(self._activity_icon, + style.STANDARD_ICON_SIZE, + style.XLARGE_ICON_SIZE)) + self._animator.start() + self._activity_icon.props.pulsing = True + + def __active_activity_changed_cb(self, model, activity): + if activity == self._home_activity: + self._activity_icon.props.paused = False + else: + self._activity_icon.props.paused = True + +class _Animation(animator.Animation): + def __init__(self, icon, start_size, end_size): + animator.Animation.__init__(self, 0.0, 1.0) + + self._icon = icon + self.start_size = start_size + self.end_size = end_size + + def next_frame(self, current): + d = (self.end_size - self.start_size) * current + self._icon.props.size = self.start_size + d diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py new file mode 100644 index 0000000..416ff0f --- /dev/null +++ b/src/jarabe/view/palettes.py @@ -0,0 +1,217 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import statvfs +from gettext import gettext as _ + +import gobject +import gtk + +from sugar import env +from sugar import profile +from sugar import activity +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + +import view.Shell + +class BasePalette(Palette): + def __init__(self, home_activity): + Palette.__init__(self) + + if home_activity.props.launching: + home_activity.connect('notify::launching', + self._launching_changed_cb) + self.set_primary_text(_('Starting...')) + else: + self.setup_palette() + + def _launching_changed_cb(self, home_activity, pspec): + if not home_activity.props.launching: + self.setup_palette() + + def setup_palette(self): + raise NotImplementedError + +class CurrentActivityPalette(BasePalette): + def __init__(self, home_activity): + self._home_activity = home_activity + BasePalette.__init__(self, home_activity) + + def setup_palette(self): + self.set_primary_text(self._home_activity.get_title()) + + menu_item = MenuItem(_('Resume'), 'activity-start') + menu_item.connect('activate', self.__resume_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + # TODO: share-with, keep + + separator = gtk.SeparatorMenuItem() + self.menu.append(separator) + separator.show() + + menu_item = MenuItem(_('Stop'), 'activity-stop') + menu_item.connect('activate', self.__stop_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __resume_activate_cb(self, menu_item): + self._home_activity.get_window().activate(gtk.get_current_event_time()) + + def __stop_activate_cb(self, menu_item): + self._home_activity.get_window().close(1) + + +class ActivityPalette(Palette): + __gtype_name__ = 'SugarActivityPalette' + + __gsignals__ = { + 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + } + + def __init__(self, activity_info): + activity_icon = Icon(file=activity_info.icon, + xo_color=profile.get_color(), + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + + Palette.__init__(self, primary_text=activity_info.name, + icon=activity_icon) + + self._bundle_id = activity_info.bundle_id + self._version = activity_info.version + self._favorite = activity_info.favorite + + menu_item = MenuItem(_('Start'), 'activity-start') + menu_item.connect('activate', self.__start_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + # TODO: start-with + + self._favorite_item = MenuItem('') + self._favorite_icon = Icon(icon_name='emblem-favorite', + icon_size=gtk.ICON_SIZE_MENU) + self._favorite_item.set_image(self._favorite_icon) + self._favorite_item.connect('activate', + self.__change_favorite_activate_cb) + self.menu.append(self._favorite_item) + self._favorite_item.show() + + menu_item = MenuItem(_('Erase'), 'list-remove') + menu_item.connect('activate', self.__erase_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + registry = activity.get_registry() + self._activity_changed_sid = registry.connect('activity_changed', + self.__activity_changed_cb) + self._update_favorite_item() + + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, palette): + self.disconnect(self._activity_changed_sid) + + def _update_favorite_item(self): + label = self._favorite_item.child + if self._favorite: + label.set_text(_('Remove favorite')) + xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + else: + label.set_text(_('Make favorite')) + xo_color = profile.get_color() + + self._favorite_icon.props.xo_color = xo_color + + def __start_activate_cb(self, menu_item): + view.Shell.get_instance().start_activity(self._bundle_id) + + def __change_favorite_activate_cb(self, menu_item): + registry = activity.get_registry() + registry.set_activity_favorite(self._bundle_id, + self._version, + not self._favorite) + + def __activity_changed_cb(self, activity_registry, activity_info): + if activity_info.bundle_id == self._bundle_id and \ + activity_info.version == self._version: + self._favorite = activity_info.favorite + self._update_favorite_item() + + def __erase_activate_cb(self, menu_item): + self.emit('erase-activated') + +class JournalPalette(BasePalette): + def __init__(self, home_activity): + self._home_activity = home_activity + self._progress_bar = None + self._free_space_label = None + + BasePalette.__init__(self, home_activity) + + def setup_palette(self): + self.set_primary_text(self._home_activity.get_title()) + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self._progress_bar = gtk.ProgressBar() + vbox.add(self._progress_bar) + self._progress_bar.show() + + self._free_space_label = gtk.Label() + self._free_space_label.set_alignment(0.5, 0.5) + vbox.add(self._free_space_label) + self._free_space_label.show() + + self.connect('popup', self.__popup_cb) + + menu_item = MenuItem(_('Show contents')) + + icon = Icon(file=self._home_activity.get_icon_path(), + icon_size=gtk.ICON_SIZE_MENU, + xo_color=self._home_activity.get_icon_color()) + menu_item.set_image(icon) + icon.show() + + menu_item.connect('activate', self.__open_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __open_activate_cb(self, menu_item): + self._home_activity.get_window().activate(gtk.get_current_event_time()) + + def __popup_cb(self, palette): + # TODO: we should be able to ask the datastore this info, as that's the + # component that knows about mount points. + stat = os.statvfs(env.get_profile_path()) + free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL] + total_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BLOCKS] + + fraction = (total_space - free_space) / float(total_space) + self._progress_bar.props.fraction = fraction + self._free_space_label.props.label = _('%(free_space)d MB Free') % \ + {'free_space': free_space / (1024 * 1024)} + diff --git a/src/jarabe/view/pulsingicon.py b/src/jarabe/view/pulsingicon.py new file mode 100644 index 0000000..499ae99 --- /dev/null +++ b/src/jarabe/view/pulsingicon.py @@ -0,0 +1,223 @@ +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import math + +import gobject + +from sugar.graphics.icon import Icon, CanvasIcon +from sugar.graphics.style import Color + +_INTERVAL = 100 +_STEP = math.pi / 10 # must be a fraction of pi, for clean caching + +class Pulser(object): + def __init__(self, icon): + self._pulse_hid = None + self._icon = icon + self._level = 0 + self._phase = 0 + + def start(self, restart=False): + if restart: + self._phase = 0 + if self._pulse_hid is None: + self._pulse_hid = gobject.timeout_add(_INTERVAL, self.__pulse_cb) + + def stop(self): + if self._pulse_hid is not None: + gobject.source_remove(self._pulse_hid) + self._pulse_hid = None + self._icon.xo_color = self._icon.base_color + + def update(self): + if self._icon.pulsing: + base_color = self._icon.base_color + pulse_color = self._icon.pulse_color + + base_stroke = self._get_as_rgba(base_color.get_stroke_color()) + pulse_stroke = self._get_as_rgba(pulse_color.get_stroke_color()) + base_fill = self._get_as_rgba(base_color.get_fill_color()) + pulse_fill = self._get_as_rgba(pulse_color.get_fill_color()) + + self._icon.stroke_color = \ + self._get_color(base_stroke, pulse_stroke).get_svg() + self._icon.fill_color = \ + self._get_color(base_fill, pulse_fill).get_svg() + else: + self._icon.xo_color = self._icon.base_color + + def _get_as_rgba(self, html_color): + if html_color == 'none': + return Color('#FFFFFF', alpha=1.0).get_rgba() + else: + return Color(html_color).get_rgba() + + def _get_color(self, orig_color, target_color): + next_point = (orig_color[0] + + self._level * (target_color[0] - orig_color[0]), + orig_color[1] + + self._level * (target_color[1] - orig_color[1]), + orig_color[2] + + self._level * (target_color[2] - orig_color[2])) + + return Color('#%02x%02x%02x' % (int(next_point[0] * 255), + int(next_point[1] * 255), + int(next_point[2] * 255))) + + def __pulse_cb(self): + self._phase += _STEP + self._level = (math.sin(self._phase) + 1) / 2 + self.update() + + return True + +class PulsingIcon(Icon): + __gtype_name__ = 'SugarPulsingIcon' + + def __init__(self, **kwargs): + self._pulser = Pulser(self) + self._base_color = None + self._pulse_color = None + self._paused = False + self._pulsing = False + + Icon.__init__(self, **kwargs) + + self._palette = None + self.connect('destroy', self.__destroy_cb) + + def set_pulse_color(self, pulse_color): + self._pulse_color = pulse_color + self._pulser.update() + + def get_pulse_color(self): + return self._pulse_color + + pulse_color = gobject.property( + type=object, getter=get_pulse_color, setter=set_pulse_color) + + def set_base_color(self, base_color): + self._base_color = base_color + self._pulser.update() + + def get_base_color(self): + return self._base_color + + base_color = gobject.property( + type=object, getter=get_base_color, setter=set_base_color) + + def set_paused(self, paused): + self._paused = paused + + if self._paused: + self._pulser.stop() + else: + self._pulser.start(restart=False) + + def get_paused(self): + return self._paused + + paused = gobject.property( + type=bool, default=False, getter=get_paused, setter=set_paused) + + def set_pulsing(self, pulsing): + self._pulsing = pulsing + + if self._pulsing: + self._pulser.start(restart=True) + else: + self._pulser.stop() + + def get_pulsing(self): + return self._pulsing + + pulsing = gobject.property( + type=bool, default=False, getter=get_pulsing, setter=set_pulsing) + + def _get_palette(self): + return self._palette + + def _set_palette(self, palette): + if self._palette is not None: + self._palette.props.invoker = None + self._palette = palette + + palette = property(_get_palette, _set_palette) + + def __destroy_cb(self, icon): + if self._palette is not None: + self._palette.destroy() + +class CanvasPulsingIcon(CanvasIcon): + __gtype_name__ = 'SugarCanvasPulsingIcon' + + def __init__(self, **kwargs): + self._pulser = Pulser(self) + self._base_color = None + self._pulse_color = None + self._paused = False + self._pulsing = False + + CanvasIcon.__init__(self, **kwargs) + + def set_pulse_color(self, pulse_color): + self._pulse_color = pulse_color + self._pulser.update() + + def get_pulse_color(self): + return self._pulse_color + + pulse_color = gobject.property( + type=object, getter=get_pulse_color, setter=set_pulse_color) + + def set_base_color(self, base_color): + self._base_color = base_color + self._pulser.update() + + def get_base_color(self): + return self._base_color + + base_color = gobject.property( + type=object, getter=get_base_color, setter=set_base_color) + + def set_paused(self, paused): + self._paused = paused + + if self._paused: + self._pulser.stop() + else: + self._pulser.start(restart=False) + + def get_paused(self): + return self._paused + + paused = gobject.property( + type=bool, default=False, getter=get_paused, setter=set_paused) + + def set_pulsing(self, pulsing): + self._pulsing = pulsing + + if self._pulsing: + self._pulser.start(restart=True) + else: + self._pulser.stop() + + def get_pulsing(self): + return self._pulsing + + pulsing = gobject.property( + type=bool, default=False, getter=get_pulsing, setter=set_pulsing) diff --git a/src/jarabe/view/tabbinghandler.py b/src/jarabe/view/tabbinghandler.py new file mode 100644 index 0000000..f1df2e3 --- /dev/null +++ b/src/jarabe/view/tabbinghandler.py @@ -0,0 +1,146 @@ +# Copyright (C) 2008, Benjamin Berg <benjamin@sipsolutions.net> +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import gtk +import gobject + +import view.Shell +from jarabe.view.frame import frame +from jarabe.model import shellmodel + +_RAISE_DELAY = 250 + +class TabbingHandler(object): + def __init__(self, modifier): + self._tabbing = False + self._modifier = modifier + self._timeout = None + self._frame = frame.get_instance() + + def _start_tabbing(self): + if not self._tabbing: + logging.debug('Grabing the input.') + + screen = gtk.gdk.screen_get_default() + window = screen.get_root_window() + keyboard_grab_result = gtk.gdk.keyboard_grab(window) + pointer_grab_result = gtk.gdk.pointer_grab(window) + + self._tabbing = (keyboard_grab_result == gtk.gdk.GRAB_SUCCESS and + pointer_grab_result == gtk.gdk.GRAB_SUCCESS) + + # Now test that the modifier is still active to prevent race + # conditions. We also test if one of the grabs failed. + mask = window.get_pointer()[2] + if not self._tabbing or not (mask & self._modifier): + logging.debug('Releasing grabs again.') + + # ungrab keyboard/pointer if the grab was successfull. + if keyboard_grab_result == gtk.gdk.GRAB_SUCCESS: + gtk.gdk.keyboard_ungrab() + if pointer_grab_result == gtk.gdk.GRAB_SUCCESS: + gtk.gdk.pointer_ungrab() + + self._tabbing = False + else: + self._frame.show(self._frame.MODE_NON_INTERACTIVE) + + def __timeout_cb(self): + self._activate_current() + self._timeout = None + return False + + def _start_timeout(self): + self._cancel_timeout() + self._timeout = gobject.timeout_add(_RAISE_DELAY, self.__timeout_cb) + + def _cancel_timeout(self): + if self._timeout: + gobject.source_remove(self._timeout) + self._timeout = None + + def _activate_current(self): + shell_model = shellmodel.get_instance() + home_model = shell_model.get_home() + activity = home_model.get_tabbing_activity() + if activity and activity.get_window(): + activity.get_window().activate(1) + + def next_activity(self): + if not self._tabbing: + first_switch = True + self._start_tabbing() + else: + first_switch = False + + if self._tabbing: + shell_model = shellmodel.get_instance() + home_model = shell_model.get_home() + zoom_level = shell_model.get_zoom_level() + zoom_activity = (zoom_level == shellmodel.ShellModel.ZOOM_ACTIVITY) + + if not zoom_activity and first_switch: + activity = home_model.get_active_activity() + else: + activity = home_model.get_tabbing_activity() + activity = home_model.get_next_activity(current=activity) + + home_model.set_tabbing_activity(activity) + self._start_timeout() + else: + view.Shell.get_instance().activate_next_activity() + + def previous_activity(self): + if not self._tabbing: + first_switch = True + self._start_tabbing() + else: + first_switch = False + + if self._tabbing: + shell_model = shellmodel.get_instance() + home_model = shell_model.get_home() + zoom_level = shell_model.get_zoom_level() + zoom_activity = (zoom_level == shellmodel.ShellModel.ZOOM_ACTIVITY) + + if not zoom_activity and first_switch: + activity = home_model.get_active_activity() + else: + activity = home_model.get_tabbing_activity() + activity = home_model.get_previous_activity(current=activity) + + home_model.set_tabbing_activity(activity) + self._start_timeout() + else: + view.Shell.get_instance().activate_next_activity() + + def stop(self): + gtk.gdk.keyboard_ungrab() + gtk.gdk.pointer_ungrab() + self._tabbing = False + + self._frame.hide() + + self._cancel_timeout() + self._activate_current() + + home_model = shellmodel.get_instance().get_home() + home_model.set_tabbing_activity(None) + + def is_tabbing(self): + return self._tabbing + |