diff options
Diffstat (limited to 'src')
95 files changed, 4880 insertions, 2585 deletions
diff --git a/src/jarabe/__init__.py b/src/jarabe/__init__.py index 41b4b1c..ed2f639 100644 --- a/src/jarabe/__init__.py +++ b/src/jarabe/__init__.py @@ -23,4 +23,3 @@ refer to a command-line "shell" interface. # 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 index 6c418e9..d22ee9a 100644 --- a/src/jarabe/config.py.in +++ b/src/jarabe/config.py.in @@ -14,7 +14,7 @@ # 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 +# pylint: disable=C0301 prefix = '@prefix@' data_path = '@prefix@/share/sugar/data' diff --git a/src/jarabe/controlpanel/__init__.py b/src/jarabe/controlpanel/__init__.py index a9dd95a..85f6a24 100644 --- a/src/jarabe/controlpanel/__init__.py +++ b/src/jarabe/controlpanel/__init__.py @@ -13,4 +13,3 @@ # 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/cmd.py b/src/jarabe/controlpanel/cmd.py index 7144b33..fe8f1a4 100644 --- a/src/jarabe/controlpanel/cmd.py +++ b/src/jarabe/controlpanel/cmd.py @@ -18,20 +18,21 @@ import sys import getopt import os from gettext import gettext as _ -import traceback import logging from jarabe 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") +_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''' + """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\ @@ -45,14 +46,16 @@ def cmd_help(): -c key clear the current value for the key \n\ ') + def note_restart(): - '''Instructions how to restart sugar''' + """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. - ''' + """Build a list of pointers to available modules and import them. + """ modules = [] path = os.path.join(config.ext_path, 'cpsection') @@ -65,16 +68,16 @@ def load_modules(): module = __import__('.'.join(('cpsection', item, 'model')), globals(), locals(), ['model']) except Exception: - logging.error('Exception while loading extension:\n' + \ - ''.join(traceback.format_exception(*sys.exc_info()))) + logging.exception('Exception while loading extension:') else: modules.append(module) return modules + def main(): try: - options, args = getopt.getopt(sys.argv[1:], "h:s:g:c:l", []) + options, args = getopt.getopt(sys.argv[1:], 'h:s:g:c:l', []) except getopt.GetoptError: cmd_help() sys.exit(2) @@ -87,7 +90,7 @@ def main(): for option, key in options: found = 0 - if option in ("-h"): + if option in ('-h'): for module in modules: method = getattr(module, 'set_' + key, None) if method: @@ -98,7 +101,7 @@ def main(): print _(_same_option_warning % (key, module)) if found == 0: print _(_no_option_error % key) - if option in ("-l"): + if option in ('-l'): for module in modules: methods = dir(module) print '%s:' % module.__name__.split('.')[1] @@ -106,9 +109,9 @@ def main(): if method.startswith('get_'): print ' %s' % method[4:] elif method.startswith('clear_'): - print " %s (use the -c argument with this option)" \ + print ' %s (use the -c argument with this option)' \ % method[6:] - if option in ("-g"): + if option in ('-g'): for module in modules: method = getattr(module, 'print_' + key, None) if method: @@ -122,7 +125,7 @@ def main(): print _(_same_option_warning % (key, module)) if found == 0: print _(_no_option_error % key) - if option in ("-s"): + if option in ('-s'): for module in modules: method = getattr(module, 'set_' + key, None) if method: @@ -139,7 +142,7 @@ def main(): print _(_same_option_warning % (key, module)) if found == 0: print _(_no_option_error % key) - if option in ("-c"): + if option in ('-c'): for module in modules: method = getattr(module, 'clear_' + key, None) if method: diff --git a/src/jarabe/controlpanel/gui.py b/src/jarabe/controlpanel/gui.py index 51d9820..2f55951 100644 --- a/src/jarabe/controlpanel/gui.py +++ b/src/jarabe/controlpanel/gui.py @@ -17,8 +17,6 @@ import os import logging from gettext import gettext as _ -import sys -import traceback import gobject import gtk @@ -32,8 +30,9 @@ from jarabe.controlpanel.toolbar import MainToolbar from jarabe.controlpanel.toolbar import SectionToolbar from jarabe import config + _logger = logging.getLogger('ControlPanel') -_MAX_COLUMNS = 5 + class ControlPanel(gtk.Window): __gtype_name__ = 'SugarControlPanel' @@ -41,6 +40,9 @@ class ControlPanel(gtk.Window): def __init__(self): gtk.Window.__init__(self) + self._max_columns = int(0.285 * (float(gtk.gdk.screen_width()) / + style.GRID_CELL_SIZE - 3)) + self.set_border_width(style.LINE_WIDTH) offset = style.GRID_CELL_SIZE width = gtk.gdk.screen_width() - offset * 2 @@ -74,7 +76,7 @@ class ControlPanel(gtk.Window): self.add(self._vbox) self._vbox.show() - self.connect("realize", self.__realize_cb) + self.connect('realize', self.__realize_cb) self._options = self._get_options() self._current_option = None @@ -110,6 +112,7 @@ class ControlPanel(gtk.Window): self._table = gtk.Table() self._table.set_col_spacings(style.GRID_CELL_SIZE) + self._table.set_row_spacings(style.GRID_CELL_SIZE) self._table.set_border_width(style.GRID_CELL_SIZE) self._scrolledwindow = gtk.ScrolledWindow() @@ -134,8 +137,17 @@ class ControlPanel(gtk.Window): except ImportError: del self._options['keyboard'] - row = 0 - column = 2 + # If the screen width only supports two columns, start + # placing from the second row. + if self._max_columns == 2: + row = 1 + column = 0 + else: + # About Me and About my computer are hardcoded below to use the + # first two slots so we need to leave them free. + row = 0 + column = 2 + options = self._options.keys() options.sort() @@ -157,7 +169,7 @@ class ControlPanel(gtk.Window): column, column + 1, row, row + 1) column += 1 - if column == _MAX_COLUMNS: + if column == self._max_columns: column = 0 row += 1 @@ -214,11 +226,16 @@ class ControlPanel(gtk.Window): globals(), locals(), ['model']) model = ModelWrapper(mod) - self._section_view = view_class(model, - self._options[option]['alerts']) + try: + self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + self._section_view = view_class(model, + self._options[option]['alerts']) + + self._set_canvas(self._section_view) + self._section_view.show() + finally: + self.get_window().set_cursor(None) - 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', @@ -227,13 +244,13 @@ class ControlPanel(gtk.Window): style.COLOR_WHITE.get_gdk_color()) def set_section_view_auto_close(self): - '''Automatically close the control panel if there is "nothing to do" - ''' + """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 - ''' + """Get the available option information from the extensions + """ options = {} path = os.path.join(config.ext_path, 'cpsection') @@ -259,11 +276,9 @@ class ControlPanel(gtk.Window): keywords.append(item) options[item]['keywords'] = keywords else: - _logger.error('There is no CLASS constant specifieds ' \ - 'in the view file \'%s\'.' % item) + _logger.error('no CLASS attribute in %r', item) except Exception: - logging.error('Exception while loading extension:\n' + \ - ''.join(traceback.format_exception(*sys.exc_info()))) + logging.exception('Exception while loading extension:') return options @@ -333,6 +348,7 @@ class ControlPanel(gtk.Window): 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 @@ -360,18 +376,15 @@ class ModelWrapper(object): except Exception, detail: _logger.debug('Error undo option: %s', detail) + class _SectionIcon(gtk.EventBox): - __gtype_name__ = "SugarSectionIcon" + __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) + '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): diff --git a/src/jarabe/controlpanel/inlinealert.py b/src/jarabe/controlpanel/inlinealert.py index b1880da..f970af4 100644 --- a/src/jarabe/controlpanel/inlinealert.py +++ b/src/jarabe/controlpanel/inlinealert.py @@ -21,6 +21,7 @@ import pango from sugar.graphics import style from sugar.graphics.icon import Icon + class InlineAlert(gtk.HBox): """UI interface for Inline alerts @@ -36,11 +37,9 @@ class InlineAlert(gtk.HBox): __gtype_name__ = 'SugarInlineAlert' __gproperties__ = { - 'msg' : (str, None, None, None, - gobject.PARAM_READWRITE), - 'icon' : (object, None, None, - gobject.PARAM_WRITABLE) - } + 'msg': (str, None, None, None, gobject.PARAM_READWRITE), + 'icon': (object, None, None, gobject.PARAM_WRITABLE), + } def __init__(self, **kwargs): @@ -80,4 +79,3 @@ class InlineAlert(gtk.HBox): def do_get_property(self, pspec): if pspec.name == 'msg': return self._msg - diff --git a/src/jarabe/controlpanel/sectionview.py b/src/jarabe/controlpanel/sectionview.py index 4de27a2..4b5751f 100644 --- a/src/jarabe/controlpanel/sectionview.py +++ b/src/jarabe/controlpanel/sectionview.py @@ -18,18 +18,17 @@ 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, ([])) - } + 'request-close': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } __gproperties__ = { - 'is_valid' : (bool, None, None, True, - gobject.PARAM_READWRITE) - } + 'is_valid': (bool, None, None, True, gobject.PARAM_READWRITE), + } _APPLY_TIMEOUT = 1000 @@ -51,5 +50,5 @@ class SectionView(gtk.VBox): return self._is_valid def undo(self): - '''Undo here the changes that have been made in this section.''' + """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 index 320a8eb..fca34a0 100644 --- a/src/jarabe/controlpanel/toolbar.py +++ b/src/jarabe/controlpanel/toolbar.py @@ -25,6 +25,7 @@ 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 """ @@ -36,8 +37,9 @@ class MainToolbar(gtk.Toolbar): ([])), 'search-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([str])) + ([str])), } + def __init__(self): gtk.Toolbar.__init__(self) @@ -83,6 +85,7 @@ class MainToolbar(gtk.Toolbar): def __stop_clicked_cb(self, button): self.emit('stop-clicked') + class SectionToolbar(gtk.Toolbar): """ Toolbar of the sections of the control panel """ @@ -94,8 +97,9 @@ class SectionToolbar(gtk.Toolbar): ([])), 'accept-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([])) + ([])), } + def __init__(self): gtk.Toolbar.__init__(self) @@ -154,4 +158,3 @@ class SectionToolbar(gtk.Toolbar): def __accept_button_clicked_cb(self, widget, data=None): self.emit('accept-clicked') - diff --git a/src/jarabe/desktop/Makefile.am b/src/jarabe/desktop/Makefile.am index 5b47455..25fb0b4 100644 --- a/src/jarabe/desktop/Makefile.am +++ b/src/jarabe/desktop/Makefile.am @@ -11,7 +11,7 @@ sugar_PYTHON = \ homewindow.py \ keydialog.py \ meshbox.py \ - myicon.py \ + networkviews.py \ schoolserver.py \ snowflakelayout.py \ spreadlayout.py \ diff --git a/src/jarabe/desktop/__init__.py b/src/jarabe/desktop/__init__.py index a9dd95a..85f6a24 100644 --- a/src/jarabe/desktop/__init__.py +++ b/src/jarabe/desktop/__init__.py @@ -13,4 +13,3 @@ # 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/desktop/activitieslist.py b/src/jarabe/desktop/activitieslist.py index 87f2af0..0370ef3 100644 --- a/src/jarabe/desktop/activitieslist.py +++ b/src/jarabe/desktop/activitieslist.py @@ -30,19 +30,18 @@ from sugar.graphics.icon import Icon, CellRendererIcon from sugar.graphics.xocolor import XoColor from sugar.graphics.menuitem import MenuItem from sugar.graphics.alert import Alert -from sugar.activity import activityfactory -from sugar.activity.activityhandle import ActivityHandle from jarabe.model import bundleregistry from jarabe.view.palettes import ActivityPalette -from jarabe.view import launcher +from jarabe.journal import misc + class ActivitiesTreeView(gtk.TreeView): __gtype_name__ = 'SugarActivitiesTreeView' __gsignals__ = { - 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([str])) + 'erase-activated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), } def __init__(self): @@ -143,13 +142,7 @@ class ActivitiesTreeView(gtk.TreeView): registry = bundleregistry.get_registry() bundle = registry.get_bundle(row[ListModel.COLUMN_BUNDLE_ID]) - activity_id = activityfactory.create_activity_id() - - client = gconf.client_get_default() - xo_color = XoColor(client.get_string('/desktop/sugar/user/color')) - - launcher.add_launcher(activity_id, bundle.get_icon(), xo_color) - activityfactory.create(bundle, ActivityHandle(activity_id)) + misc.launch(bundle) def set_filter(self, query): self._query = query.lower() @@ -159,6 +152,7 @@ class ActivitiesTreeView(gtk.TreeView): title = model[tree_iter][ListModel.COLUMN_TITLE] return title is not None and title.lower().find(self._query) > -1 + class ListModel(gtk.TreeModelSort): __gtype_name__ = 'SugarListModel' @@ -172,7 +166,7 @@ class ListModel(gtk.TreeModelSort): COLUMN_DATE_TEXT = 7 def __init__(self): - self._model = gtk.ListStore(str, bool, str, str, int, str, int, str) + self._model = gtk.ListStore(str, bool, str, str, str, str, int, str) self._model_filter = self._model.filter_new() gtk.TreeModelSort.__init__(self, self._model_filter) @@ -243,6 +237,7 @@ class ListModel(gtk.TreeModelSort): def refilter(self): self._model_filter.refilter() + class CellRendererFavorite(CellRendererIcon): __gtype_name__ = 'SugarCellRendererFavorite' @@ -257,12 +252,13 @@ class CellRendererFavorite(CellRendererIcon): self.props.prelit_stroke_color = style.COLOR_BUTTON_GREY.get_svg() self.props.prelit_fill_color = style.COLOR_BUTTON_GREY.get_svg() + class CellRendererActivityIcon(CellRendererIcon): __gtype_name__ = 'SugarCellRendererActivityIcon' __gsignals__ = { - 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([str])) + 'erase-activated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), } def __init__(self, tree_view): @@ -295,6 +291,7 @@ class CellRendererActivityIcon(CellRendererIcon): def __erase_activated_cb(self, palette, bundle_id): self.emit('erase-activated', bundle_id) + class ActivitiesList(gtk.VBox): __gtype_name__ = 'SugarActivitiesList' @@ -376,14 +373,15 @@ class ActivitiesList(gtk.VBox): if response_id == gtk.RESPONSE_OK: registry = bundleregistry.get_registry() bundle = registry.get_bundle(bundle_id) - registry.uninstall(bundle) + registry.uninstall(bundle, delete_profile=True) + class ActivityListPalette(ActivityPalette): __gtype_name__ = 'SugarActivityListPalette' __gsignals__ = { - 'erase-activated' : (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([str])) + 'erase-activated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), } def __init__(self, activity_info): @@ -406,13 +404,7 @@ class ActivityListPalette(ActivityPalette): self._favorite_item.show() if activity_info.is_user_activity(): - menu_item = MenuItem(_('Erase'), 'list-remove') - menu_item.connect('activate', self.__erase_activate_cb) - self.menu.append(menu_item) - menu_item.show() - - if not os.access(activity_info.get_path(), os.W_OK): - menu_item.props.sensitive = False + self._add_erase_option(registry, activity_info) registry = bundleregistry.get_registry() self._activity_changed_sid = registry.connect('bundle_changed', @@ -421,6 +413,16 @@ class ActivityListPalette(ActivityPalette): self.connect('destroy', self.__destroy_cb) + def _add_erase_option(self, registry, activity_info): + menu_item = MenuItem(_('Erase'), 'list-remove') + menu_item.connect('activate', self.__erase_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + if not os.access(activity_info.get_path(), os.W_OK) or \ + registry.is_activity_protected(self._bundle_id): + menu_item.props.sensitive = False + def __destroy_cb(self, palette): self.disconnect(self._activity_changed_sid) @@ -433,7 +435,7 @@ class ActivityListPalette(ActivityPalette): else: label.set_text(_('Make favorite')) client = gconf.client_get_default() - xo_color = XoColor(client.get_string("/desktop/sugar/user/color")) + xo_color = XoColor(client.get_string('/desktop/sugar/user/color')) self._favorite_icon.props.xo_color = xo_color @@ -453,4 +455,3 @@ class ActivityListPalette(ActivityPalette): def __erase_activate_cb(self, menu_item): self.emit('erase-activated', self._bundle_id) - diff --git a/src/jarabe/desktop/favoriteslayout.py b/src/jarabe/desktop/favoriteslayout.py index 85e1b59..360c147 100644 --- a/src/jarabe/desktop/favoriteslayout.py +++ b/src/jarabe/desktop/favoriteslayout.py @@ -1,4 +1,5 @@ # Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2010 Sugar Labs # # 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 @@ -28,10 +29,18 @@ from sugar.graphics import style from jarabe.model import bundleregistry from jarabe.desktop.grid import Grid + _logger = logging.getLogger('FavoritesLayout') _CELL_SIZE = 4 _BASE_SCALE = 1000 +_INTERMEDIATE_B = (style.STANDARD_ICON_SIZE + style.SMALL_ICON_SIZE) / 2 +_INTERMEDIATE_A = (style.STANDARD_ICON_SIZE + _INTERMEDIATE_B) / 2 +_INTERMEDIATE_C = (_INTERMEDIATE_B + style.SMALL_ICON_SIZE) / 2 +_ICON_SIZES = [style.MEDIUM_ICON_SIZE, style.STANDARD_ICON_SIZE, + _INTERMEDIATE_A, _INTERMEDIATE_B, _INTERMEDIATE_C, + style.SMALL_ICON_SIZE] + class FavoritesLayout(gobject.GObject, hippo.CanvasLayout): """Base class of the different layout types.""" @@ -81,7 +90,8 @@ class FavoritesLayout(gobject.GObject, hippo.CanvasLayout): if icon not in self.box.get_children(): raise ValueError('Child not in box.') - if not(hasattr(icon, 'get_bundle_id') and hasattr(icon, 'get_version')): + if not (hasattr(icon, 'get_bundle_id') and + hasattr(icon, 'get_version')): logging.debug('Not an activity icon %r', icon) return @@ -101,6 +111,7 @@ class FavoritesLayout(gobject.GObject, hippo.CanvasLayout): def allow_dnd(self): return False + class RandomLayout(FavoritesLayout): """Lay out icons randomly; try to nudge them around to resolve overlaps.""" @@ -181,13 +192,19 @@ class RandomLayout(FavoritesLayout): 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 +_ICON_SPACING_FACTORS = [1.5, 1.4, 1.3, 1.2, 1.1, 1.0] +_SPIRAL_SPACING_FACTORS = [1.5, 1.5, 1.5, 1.4, 1.3, 1.2] +_MIMIMUM_RADIUS_ENCROACHMENT = 0.75 +_INITIAL_ANGLE = math.pi + class RingLayout(FavoritesLayout): - """Lay out icons in a ring around the XO man.""" + """Lay out icons in a ring or spiral around the XO man.""" __gtype_name__ = 'RingLayout' icon_name = 'view-radial' @@ -201,6 +218,7 @@ class RingLayout(FavoritesLayout): def __init__(self): FavoritesLayout.__init__(self) self._locked_children = {} + self._spiral_mode = False def append(self, icon, locked=False): FavoritesLayout.append(self, icon, locked) @@ -221,33 +239,78 @@ class RingLayout(FavoritesLayout): 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 + """ Adjust the ring or spiral radius and icon size as needed. """ + self._spiral_mode = False + distance = style.MEDIUM_ICON_SIZE + style.DEFAULT_SPACING * \ + _ICON_SPACING_FACTORS[_ICON_SIZES.index(style.MEDIUM_ICON_SIZE)] + radius = max(children_count * distance / (2 * math.pi), + _MINIMUM_RADIUS) + if radius < _MAXIMUM_RADIUS: + return radius, style.MEDIUM_ICON_SIZE + + distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING * \ + _ICON_SPACING_FACTORS[_ICON_SIZES.index(style.STANDARD_ICON_SIZE)] + radius = max(children_count * distance / (2 * math.pi), + _MINIMUM_RADIUS) + if radius < _MAXIMUM_RADIUS: + return radius, style.STANDARD_ICON_SIZE + + self._spiral_mode = True 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) + angle_, radius = self._calculate_angle_and_radius(children_count, + icon_size) + while radius > _MAXIMUM_RADIUS: + i = _ICON_SIZES.index(icon_size) + if i < len(_ICON_SIZES) - 1: + icon_size = _ICON_SIZES[i + 1] + angle_, radius = self._calculate_angle_and_radius( + children_count, icon_size) + else: + break return radius, icon_size - def _calculate_position(self, radius, icon_size, index, children_count, - sin=math.sin, cos=math.cos): + def _calculate_position(self, radius, icon_size, icon_index, + children_count, sin=math.sin, cos=math.cos): + """ Calculate an icon position on a circle or a spiral. """ 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 + if self._spiral_mode: + min_width_, box_width = self.box.get_width_request() + min_height_, box_height = self.box.get_height_request(box_width) + angle, radius = self._calculate_angle_and_radius(icon_index, + icon_size) + x, y = self._convert_from_polar_to_cartesian(angle, radius, + icon_size, + width, height) + else: + angle = icon_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 _convert_from_polar_to_cartesian(self, angle, radius, icon_size, width, + height): + """ Convert angle, radius to x, y """ + x = int(math.sin(angle) * radius) + y = int(math.cos(angle) * radius) + x = - x + (width - icon_size) / 2 + y = y + (height - icon_size - (style.GRID_CELL_SIZE / 2)) / 2 return x, y + def _calculate_angle_and_radius(self, icon_count, icon_size): + """ Based on icon_count and icon_size, calculate radius and angle. """ + spiral_spacing = _SPIRAL_SPACING_FACTORS[_ICON_SIZES.index(icon_size)] + icon_spacing = icon_size + style.DEFAULT_SPACING * \ + _ICON_SPACING_FACTORS[_ICON_SIZES.index(icon_size)] + angle = _INITIAL_ANGLE + radius = _MINIMUM_RADIUS - (icon_size * _MIMIMUM_RADIUS_ENCROACHMENT) + for i_ in range(icon_count): + circumference = radius * 2 * math.pi + n = circumference / icon_spacing + angle += (2 * math.pi / n) + radius += (float(icon_spacing) * spiral_spacing / n) + return angle, radius + 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] @@ -294,6 +357,7 @@ class RingLayout(FavoritesLayout): else: return 0 + _SUNFLOWER_CONSTANT = style.STANDARD_ICON_SIZE * .75 """Chose a constant such that STANDARD_ICON_SIZE icons are nicely spaced.""" @@ -319,6 +383,7 @@ 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. @@ -346,7 +411,8 @@ class SunflowerLayout(RingLayout): return None, style.STANDARD_ICON_SIZE def adjust_index(self, i): - """Skip floret indices which end up outside the desired bounding box.""" + """Skip floret indices which end up outside the desired bounding box. + """ for idx in self.skipped_indices: if i < idx: break @@ -377,7 +443,7 @@ class SunflowerLayout(RingLayout): # 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 + (style.GRID_CELL_SIZE / 2)) / 2 # skip allocations outside the allocation box. # give up once we can't fit @@ -385,10 +451,12 @@ class SunflowerLayout(RingLayout): if y < 0 or y > (height - icon_size) or \ x < 0 or x > (width - icon_size): self.skipped_indices.append(index) - continue # try again + # try again + continue return x, y + class BoxLayout(RingLayout): """Lay out icons in a square around the XO man.""" @@ -421,14 +489,16 @@ class BoxLayout(RingLayout): return (90 - d) / 45. if d < 225: return -1 - return cos_d(360 - d) # mirror around 180 + # mirror around 180 + return cos_d(360 - d) 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) + 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.""" @@ -467,7 +537,8 @@ class TriangleLayout(RingLayout): return (d + 90) / 120. if d <= 90: return (90 - d) / 60. - return -cos_d(180 - d) # mirror around 90 + # mirror around 90 + return -cos_d(180 - d) sqrt_3 = math.sqrt(3) @@ -478,11 +549,12 @@ class TriangleLayout(RingLayout): return ((d + 90) / 120.) * sqrt_3 - 1 if d <= 90: return sqrt_3 - 1 - return sin_d(180 - d) # mirror around 90 + # mirror around 90 + return sin_d(180 - d) 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) + return RingLayout._calculate_position(self, radius, icon_size, index, + children_count, sin=sin, + cos=cos) diff --git a/src/jarabe/desktop/favoritesview.py b/src/jarabe/desktop/favoritesview.py index aca945a..b4a4e75 100644 --- a/src/jarabe/desktop/favoritesview.py +++ b/src/jarabe/desktop/favoritesview.py @@ -30,25 +30,23 @@ from sugar.graphics.menuitem import MenuItem from sugar.graphics.alert import Alert from sugar.graphics.xocolor import XoColor from sugar.activity import activityfactory -from sugar.activity.activityhandle import ActivityHandle -from sugar.presence import presenceservice from sugar import dispatch from sugar.datastore import datastore from jarabe.view.palettes import JournalPalette from jarabe.view.palettes import CurrentActivityPalette, ActivityPalette +from jarabe.view.buddyicon import BuddyIcon from jarabe.view.buddymenu import BuddyMenu -from jarabe.view import launcher -from jarabe.model.buddy import BuddyModel +from jarabe.model.buddy import get_owner_instance from jarabe.model import shell from jarabe.model import bundleregistry from jarabe.journal import misc from jarabe.desktop import schoolserver from jarabe.desktop.schoolserver import RegisterError -from jarabe.desktop.myicon import MyIcon from jarabe.desktop import favoriteslayout + _logger = logging.getLogger('FavoritesView') _ICON_DND_TARGET = ('activity-icon', gtk.TARGET_SAME_WIDGET, 0) @@ -62,6 +60,9 @@ LAYOUT_MAP = {favoriteslayout.RingLayout.key: favoriteslayout.RingLayout, `FavoritesLayout` which implement the layouts. Additional information about the layout can be accessed with fields of the class.""" +_favorites_settings = None + + class FavoritesView(hippo.Canvas): __gtype_name__ = 'SugarFavoritesView' @@ -82,7 +83,7 @@ class FavoritesView(hippo.Canvas): self._box.props.background_color = style.COLOR_WHITE.get_int() self.set_root(self._box) - self._my_icon = _MyIcon(style.XLARGE_ICON_SIZE) + self._my_icon = OwnerIcon(style.XLARGE_ICON_SIZE) self._my_icon.connect('register-activate', self.__register_activate_cb) self._box.append(self._my_icon) @@ -172,7 +173,8 @@ class FavoritesView(hippo.Canvas): 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) + 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) @@ -190,7 +192,8 @@ class FavoritesView(hippo.Canvas): # 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) + 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 @@ -203,9 +206,9 @@ class FavoritesView(hippo.Canvas): 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): + 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 @@ -274,7 +277,7 @@ class FavoritesView(hippo.Canvas): def _set_layout(self, layout): if layout not in LAYOUT_MAP: - logging.warn('Unknown favorites layout: %r' % layout) + logging.warn('Unknown favorites layout: %r', layout) layout = favoriteslayout.RingLayout.key assert layout in LAYOUT_MAP @@ -327,7 +330,7 @@ class FavoritesView(hippo.Canvas): alert.props.title = _('Registration Successful') alert.props.msg = _('You are now registered ' \ 'with your school server.') - self._my_icon.remove_register_menu() + self._my_icon.set_registered() ok_icon = Icon(icon_name='dialog-ok') alert.add_button(gtk.RESPONSE_OK, _('Ok'), ok_icon) @@ -387,7 +390,7 @@ class ActivityIcon(CanvasIcon): break def _get_last_activity_async(self, bundle_id, properties): - query = {'activity': bundle_id} + query = {'activity': bundle_id} datastore.find(query, sorting=['+timestamp'], limit=self._MAX_RESUME_ENTRIES, properties=properties, @@ -395,8 +398,8 @@ class ActivityIcon(CanvasIcon): error_handler=self.__get_last_activity_error_handler_cb) def __get_last_activity_reply_handler_cb(self, entries, total_count): - # If there's a problem with the DS index, we may get entries not related - # to this activity. + # If there's a problem with the DS index, we may get entries not + # related to this activity. checked_entries = [] for entry in entries: if entry['activity'] == self.bundle_id: @@ -484,16 +487,7 @@ class ActivityIcon(CanvasIcon): if self._resume_mode and self._journal_entries: self._resume(self._journal_entries[0]) else: - client = gconf.client_get_default() - xo_color = XoColor(client.get_string('/desktop/sugar/user/color')) - - activity_id = activityfactory.create_activity_id() - launcher.add_launcher(activity_id, - self._activity_info.get_icon(), - xo_color) - - handle = ActivityHandle(activity_id) - activityfactory.create(self._activity_info, handle) + misc.launch(self._activity_info) def get_bundle_id(self): return self._activity_info.get_bundle_id() @@ -516,6 +510,7 @@ class ActivityIcon(CanvasIcon): self._resume_mode = resume_mode self._update() + class FavoritePalette(ActivityPalette): __gtype_name__ = 'SugarFavoritePalette' @@ -564,6 +559,7 @@ class FavoritePalette(ActivityPalette): if entry is not None: self.emit('entry-activate', entry) + class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem): def __init__(self): CanvasIcon.__init__(self, cache=True) @@ -602,17 +598,18 @@ class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem): self._home_activity = home_activity self._update() -class _MyIcon(MyIcon): - __gtype_name__ = 'SugarFavoritesMyIcon' + +class OwnerIcon(BuddyIcon): + __gtype_name__ = 'SugarFavoritesOwnerIcon' __gsignals__ = { - 'register-activate' : (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([])) + 'register-activate': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([])), } - def __init__(self, scale): - MyIcon.__init__(self, scale) - self._power_manager = None + def __init__(self, size): + BuddyIcon.__init__(self, buddy=get_owner_instance(), size=size) + self._palette_enabled = False self._register_menu = None @@ -621,17 +618,20 @@ class _MyIcon(MyIcon): self._palette_enabled = True return - presence_service = presenceservice.get_instance() - owner = BuddyModel(buddy=presence_service.get_owner()) - palette = BuddyMenu(owner) + palette = BuddyMenu(get_owner_instance()) client = gconf.client_get_default() backup_url = client.get_string('/desktop/sugar/backup_url') + if not backup_url: self._register_menu = MenuItem(_('Register'), 'media-record') - self._register_menu.connect('activate', self.__register_activate_cb) - palette.menu.append(self._register_menu) - self._register_menu.show() + else: + self._register_menu = MenuItem(_('Register again'), + 'media-record') + + self._register_menu.connect('activate', self.__register_activate_cb) + palette.menu.append(self._register_menu) + self._register_menu.show() return palette @@ -641,12 +641,17 @@ class _MyIcon(MyIcon): def __register_activate_cb(self, menuitem): self.emit('register-activate') - def remove_register_menu(self): + def set_registered(self): self.palette.menu.remove(self._register_menu) + self._register_menu = MenuItem(_('Register again'), 'media-record') + self._register_menu.connect('activate', self.__register_activate_cb) + self.palette.menu.append(self._register_menu) + self._register_menu.show() + class FavoritesSetting(object): - _FAVORITES_KEY = "/desktop/sugar/desktop/favorites_layout" + _FAVORITES_KEY = '/desktop/sugar/desktop/favorites_layout' def __init__(self): client = gconf.client_get_default() @@ -672,7 +677,6 @@ class FavoritesSetting(object): layout = property(get_layout, set_layout) -_favorites_settings = None def get_settings(): global _favorites_settings diff --git a/src/jarabe/desktop/friendview.py b/src/jarabe/desktop/friendview.py index 4c5f1c8..8dab35f 100644 --- a/src/jarabe/desktop/friendview.py +++ b/src/jarabe/desktop/friendview.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> # # 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 @@ -18,17 +19,15 @@ import hippo from sugar.graphics.icon import CanvasIcon from sugar.graphics import style -from sugar.presence import presenceservice from jarabe.view.buddyicon import BuddyIcon from jarabe.model import bundleregistry + 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 @@ -37,14 +36,12 @@ class FriendView(hippo.CanvasBox): 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._update_activity() - 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) + self._buddy.connect('notify::current-activity', + self.__buddy_notify_current_activity_cb) + self._buddy.connect('notify::present', self.__buddy_notify_present_cb) + self._buddy.connect('notify::color', self.__buddy_notify_color_cb) def _get_new_icon_name(self, ps_activity): registry = bundleregistry.get_registry() @@ -58,30 +55,30 @@ class FriendView(hippo.CanvasBox): self.remove(self._activity_icon) self._activity_icon_visible = False - def _buddy_activity_changed_cb(self, buddy, ps_activity=None): - if not ps_activity: + def __buddy_notify_current_activity_cb(self, buddy, pspec): + self._update_activity() + + def _update_activity(self): + if not self._buddy.props.present or \ + not self._buddy.props.current_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) + name = self._get_new_icon_name(self._buddy.current_activity) if name: self._activity_icon.props.file_name = name - self._activity_icon.props.xo_color = buddy.get_color() + self._activity_icon.props.xo_color = self._buddy.props.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_notify_present_cb(self, buddy, pspec): + self._update_activity() - def _buddy_color_changed_cb(self, buddy, color): + def __buddy_notify_color_cb(self, buddy, pspec): # TODO: shouldn't this change self._buddy_icon instead? - self._activity_icon.props.xo_color = buddy.get_color() + self._activity_icon.props.xo_color = buddy.props.color diff --git a/src/jarabe/desktop/grid.py b/src/jarabe/desktop/grid.py index f3412c9..eab4033 100644 --- a/src/jarabe/desktop/grid.py +++ b/src/jarabe/desktop/grid.py @@ -22,17 +22,19 @@ 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])) + 'child-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), } + def __init__(self, width, height): gobject.GObject.__init__(self) @@ -185,7 +187,8 @@ class Grid(_sugarext.Grid): 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: + if (c not in self._locked_children and + c not in self._collisions): collision_found = True self._collisions.append(c) diff --git a/src/jarabe/desktop/groupbox.py b/src/jarabe/desktop/groupbox.py index 76c2981..ed8f8ae 100644 --- a/src/jarabe/desktop/groupbox.py +++ b/src/jarabe/desktop/groupbox.py @@ -23,18 +23,19 @@ import gconf from sugar.graphics import style from sugar.graphics.icon import CanvasIcon from sugar.graphics.xocolor import XoColor -from sugar.presence import presenceservice from jarabe.view.buddymenu import BuddyMenu -from jarabe.model.buddy import BuddyModel +from jarabe.model.buddy import get_owner_instance from jarabe.model import friends from jarabe.desktop.friendview import FriendView from jarabe.desktop.spreadlayout import SpreadLayout + class GroupBox(hippo.Canvas): __gtype_name__ = 'SugarGroupBox' + def __init__(self): - logging.debug("STARTUP: Loading the group view") + logging.debug('STARTUP: Loading the group view') gobject.GObject.__init__(self) @@ -48,15 +49,13 @@ class GroupBox(hippo.Canvas): self._box.set_layout(self._layout) client = gconf.client_get_default() - color = XoColor(client.get_string("/desktop/sugar/user/color")) + color = XoColor(client.get_string('/desktop/sugar/user/color')) self._owner_icon = CanvasIcon(icon_name='computer-xo', cache=True, xo_color=color) self._owner_icon.props.size = style.LARGE_ICON_SIZE - presence_service = presenceservice.get_instance() - owner = BuddyModel(buddy=presence_service.get_owner()) - self._owner_icon.set_palette(BuddyMenu(owner)) + self._owner_icon.set_palette(BuddyMenu(get_owner_instance())) self._layout.add(self._owner_icon) friends_model = friends.get_model() diff --git a/src/jarabe/desktop/homebox.py b/src/jarabe/desktop/homebox.py index 85279ff..661326e 100644 --- a/src/jarabe/desktop/homebox.py +++ b/src/jarabe/desktop/homebox.py @@ -30,16 +30,18 @@ from sugar.graphics.icon import Icon from jarabe.desktop import favoritesview from jarabe.desktop.activitieslist import ActivitiesList + _FAVORITES_VIEW = 0 _LIST_VIEW = 1 _AUTOSEARCH_TIMEOUT = 1000 + class HomeBox(gtk.VBox): __gtype_name__ = 'SugarHomeBox' def __init__(self): - logging.debug("STARTUP: Loading the home view") + logging.debug('STARTUP: Loading the home view') gobject.GObject.__init__(self) @@ -57,7 +59,7 @@ class HomeBox(gtk.VBox): def show_software_updates_alert(self): alert = Alert() updater_icon = Icon(icon_name='module-updater', - pixel_size = style.STANDARD_ICON_SIZE) + pixel_size=style.STANDARD_ICON_SIZE) alert.props.icon = updater_icon updater_icon.show() alert.props.title = _('Software Update') @@ -125,7 +127,7 @@ class HomeBox(gtk.VBox): else: raise ValueError('Invalid view: %r' % view) - _REDRAW_TIMEOUT = 5 * 60 * 1000 # 5 minutes + _REDRAW_TIMEOUT = 5 * 60 * 1000 # 5 minutes def resume(self): pass @@ -144,16 +146,15 @@ class HomeBox(gtk.VBox): def set_resume_mode(self, resume_mode): self._favorites_view.set_resume_mode(resume_mode) + class HomeToolbar(gtk.Toolbar): __gtype_name__ = 'SugarHomeToolbar' __gsignals__ = { - 'query-changed': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, + 'query-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), - 'view-changed': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([object])) + 'view-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), } def __init__(self): @@ -239,13 +240,14 @@ class HomeToolbar(gtk.Toolbar): if self._autosearch_timer: gobject.source_remove(self._autosearch_timer) self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT, - self.__autosearch_timer_cb) + 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' @@ -295,4 +297,3 @@ class FavoritesButton(RadioToolButton): def _update_icon(self): self.props.named_icon = favoritesview.LAYOUT_MAP[self._layout]\ .icon_name - diff --git a/src/jarabe/desktop/homewindow.py b/src/jarabe/desktop/homewindow.py index d830ed0..07deff7 100644 --- a/src/jarabe/desktop/homewindow.py +++ b/src/jarabe/desktop/homewindow.py @@ -16,6 +16,7 @@ import logging +import gobject import gtk from sugar.graphics import style @@ -28,11 +29,15 @@ from jarabe.desktop.transitionbox import TransitionBox from jarabe.model.shell import ShellModel from jarabe.model import shell -_HOME_PAGE = 0 -_GROUP_PAGE = 1 -_MESH_PAGE = 2 + +_HOME_PAGE = 0 +_GROUP_PAGE = 1 +_MESH_PAGE = 2 _TRANSITION_PAGE = 3 +_instance = None + + class HomeWindow(gtk.Window): def __init__(self): logging.debug('STARTUP: Loading the desktop window') @@ -45,8 +50,10 @@ class HomeWindow(gtk.Window): self._active = False self._fully_obscured = True - self.set_default_size(gtk.gdk.screen_width(), - gtk.gdk.screen_height()) + screen = self.get_screen() + screen.connect('size-changed', self.__screen_size_change_cb) + self.set_default_size(screen.get_width(), + screen.get_height()) self.realize() self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DESKTOP) @@ -73,13 +80,16 @@ class HomeWindow(gtk.Window): self.__zoom_level_changed_cb) def _deactivate_view(self, level): - group = palettegroup.get_group("default") + group = palettegroup.get_group('default') group.popdown() if level == ShellModel.ZOOM_HOME: self._home_box.suspend() elif level == ShellModel.ZOOM_MESH: self._mesh_box.suspend() + def __screen_size_change_cb(self, screen): + self.resize(screen.get_width(), screen.get_height()) + def _activate_view(self, level): if level == ShellModel.ZOOM_HOME: self._home_box.resume() @@ -178,11 +188,22 @@ class HomeWindow(gtk.Window): def get_home_box(self): return self._home_box -_instance = None + def busy_during_delayed_action(self, action): + """Use busy cursor during execution of action, scheduled via idle_add. + """ + def action_wrapper(old_cursor): + try: + action() + finally: + self.get_window().set_cursor(old_cursor) + + old_cursor = self.get_window().get_cursor() + self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + gobject.idle_add(action_wrapper, old_cursor) + def get_instance(): global _instance if not _instance: _instance = HomeWindow() return _instance - diff --git a/src/jarabe/desktop/keydialog.py b/src/jarabe/desktop/keydialog.py index 1e6d17a..6241b9b 100644 --- a/src/jarabe/desktop/keydialog.py +++ b/src/jarabe/desktop/keydialog.py @@ -24,8 +24,14 @@ import dbus from jarabe.model import network from jarabe.model.network import Secrets + IW_AUTH_ALG_OPEN_SYSTEM = 'open' -IW_AUTH_ALG_SHARED_KEY = 'shared' +IW_AUTH_ALG_SHARED_KEY = 'shared' + +WEP_PASSPHRASE = 1 +WEP_HEX = 2 +WEP_ASCII = 3 + def string_is_hex(key): is_hex = True @@ -34,6 +40,7 @@ def string_is_hex(key): is_hex = False return is_hex + def string_is_ascii(string): try: string.encode('ascii') @@ -41,12 +48,14 @@ def string_is_ascii(string): 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: @@ -57,16 +66,18 @@ def hash_passphrase(passphrase): passphrase = hashlib.md5(passphrase).digest() return string_to_hex(passphrase)[:26] + class CanceledKeyRequestError(dbus.DBusException): def __init__(self): dbus.DBusException.__init__(self) self._dbus_error_name = network.NM_SETTINGS_IFACE + '.CanceledError' + class KeyDialog(gtk.Dialog): def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, settings, response): gtk.Dialog.__init__(self, flags=gtk.DIALOG_MODAL) - self.set_title("Wireless Key Required") + self.set_title('Wireless Key Required') self._settings = settings self._response = response @@ -108,9 +119,6 @@ class KeyDialog(gtk.Dialog): def get_response_object(self): return self._response -WEP_PASSPHRASE = 1 -WEP_HEX = 2 -WEP_ASCII = 3 class WEPKeyDialog(KeyDialog): def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, settings, @@ -120,9 +128,9 @@ class WEPKeyDialog(KeyDialog): # 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_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() @@ -132,7 +140,7 @@ class WEPKeyDialog(KeyDialog): self.key_combo.connect('changed', self._key_combo_changed_cb) hbox = gtk.HBox() - hbox.pack_start(gtk.Label(_("Key Type:"))) + hbox.pack_start(gtk.Label(_('Key Type:'))) hbox.pack_start(self.key_combo) hbox.show_all() self.vbox.pack_start(hbox) @@ -142,8 +150,8 @@ class WEPKeyDialog(KeyDialog): # WEP authentication mode self.auth_store = gtk.ListStore(str, str) - self.auth_store.append(["Open System", IW_AUTH_ALG_OPEN_SYSTEM]) - self.auth_store.append(["Shared Key", IW_AUTH_ALG_SHARED_KEY]) + 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() @@ -152,7 +160,7 @@ class WEPKeyDialog(KeyDialog): self.auth_combo.set_active(0) hbox = gtk.HBox() - hbox.pack_start(gtk.Label(_("Authentication Type:"))) + hbox.pack_start(gtk.Label(_('Authentication Type:'))) hbox.pack_start(self.auth_combo) hbox.show_all() @@ -179,8 +187,8 @@ class WEPKeyDialog(KeyDialog): def print_security(self): (key, auth_alg) = self._get_security() - print "Key: %s" % key - print "Auth: %d" % auth_alg + print 'Key: %s' % key + print 'Auth: %d' % auth_alg def create_security(self): (key, auth_alg) = self._get_security() @@ -209,6 +217,7 @@ class WEPKeyDialog(KeyDialog): self.set_response_sensitive(gtk.RESPONSE_OK, valid) + class WPAKeyDialog(KeyDialog): def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, settings, response): @@ -217,7 +226,7 @@ class WPAKeyDialog(KeyDialog): self.add_key_entry() self.store = gtk.ListStore(str) - self.store.append([_("WPA & WPA2 Personal")]) + self.store.append([_('WPA & WPA2 Personal')]) self.combo = gtk.ComboBox(self.store) cell = gtk.CellRendererText() @@ -226,7 +235,7 @@ class WPAKeyDialog(KeyDialog): self.combo.set_active(0) self.hbox = gtk.HBox() - self.hbox.pack_start(gtk.Label(_("Wireless Security:"))) + self.hbox.pack_start(gtk.Label(_('Wireless Security:'))) self.hbox.pack_start(self.combo) self.hbox.show_all() @@ -246,21 +255,21 @@ class WPAKeyDialog(KeyDialog): from subprocess import Popen, PIPE p = Popen(['wpa_passphrase', ssid, key], stdout=PIPE) for line in p.stdout: - if line.strip().startswith("psk="): + if line.strip().startswith('psk='): real_key = line.strip()[4:] if p.wait() != 0: - raise RuntimeError("Error hashing passphrase") + raise RuntimeError('Error hashing passphrase') if real_key and len(real_key) != 64: real_key = None if not real_key: - raise RuntimeError("Invalid key") + raise RuntimeError('Invalid key') return real_key def print_security(self): key = self._get_security() - print "Key: %s" % key + print 'Key: %s' % key def create_security(self): secrets = Secrets(self._settings) @@ -281,6 +290,7 @@ class WPAKeyDialog(KeyDialog): self.set_response_sensitive(gtk.RESPONSE_OK, valid) return False + def create(ssid, flags, wpa_flags, rsn_flags, dev_caps, settings, response): if wpa_flags == network.NM_802_11_AP_SEC_NONE and \ rsn_flags == network.NM_802_11_AP_SEC_NONE: @@ -290,13 +300,15 @@ def create(ssid, flags, wpa_flags, rsn_flags, dev_caps, settings, response): key_dialog = WPAKeyDialog(ssid, flags, wpa_flags, rsn_flags, dev_caps, settings, response) - key_dialog.connect("response", _key_dialog_response_cb) - key_dialog.connect("destroy", _key_dialog_destroy_cb) + key_dialog.connect('response', _key_dialog_response_cb) + key_dialog.connect('destroy', _key_dialog_destroy_cb) key_dialog.show_all() + def _key_dialog_destroy_cb(key_dialog, data=None): _key_dialog_response_cb(key_dialog, gtk.RESPONSE_CANCEL) + def _key_dialog_response_cb(key_dialog, response_id): response = key_dialog.get_response_object() secrets = None @@ -308,10 +320,9 @@ def _key_dialog_response_cb(key_dialog, response_id): response.set_error(CanceledKeyRequestError()) elif response_id == gtk.RESPONSE_OK: if not secrets: - raise RuntimeError("Invalid security arguments.") + raise RuntimeError('Invalid security arguments.') response.set_secrets(secrets) else: - raise RuntimeError("Unhandled key dialog response %d" % response_id) + raise RuntimeError('Unhandled key dialog response %d' % response_id) key_dialog.destroy() - diff --git a/src/jarabe/desktop/meshbox.py b/src/jarabe/desktop/meshbox.py index a04922b..ad4b873 100644 --- a/src/jarabe/desktop/meshbox.py +++ b/src/jarabe/desktop/meshbox.py @@ -1,6 +1,7 @@ # Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer -# Copyright (C) 2009 One Laptop per Child +# Copyright (C) 2009-2010 One Laptop per Child +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> # # 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 @@ -18,41 +19,34 @@ from gettext import gettext as _ import logging -import hashlib import dbus import hippo +import glib import gobject import gtk +import gconf from sugar.graphics.icon import CanvasIcon, Icon -from sugar.graphics.xocolor import XoColor -from sugar.graphics 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.activity.activityhandle import ActivityHandle -from sugar.activity import activityfactory -from sugar.util import unique_id -from sugar import profile from jarabe.model import neighborhood +from jarabe.model.buddy import get_owner_instance from jarabe.view.buddyicon import BuddyIcon -from jarabe.view.pulsingicon import CanvasPulsingIcon -from jarabe.view import launcher from jarabe.desktop.snowflakelayout import SnowflakeLayout from jarabe.desktop.spreadlayout import SpreadLayout -from jarabe.desktop import keydialog -from jarabe.model import bundleregistry +from jarabe.desktop.networkviews import WirelessNetworkView +from jarabe.desktop.networkviews import OlpcMeshView +from jarabe.desktop.networkviews import SugarAdhocView from jarabe.model import network -from jarabe.model import shell -from jarabe.model.network import Settings -from jarabe.model.network import IP4Config -from jarabe.model.network import WirelessSecurity from jarabe.model.network import AccessPoint from jarabe.model.olpcmesh import OlpcMeshManager +from jarabe.model.adhoc import get_adhoc_manager_instance +from jarabe.journal import misc + _NM_SERVICE = 'org.freedesktop.NetworkManager' _NM_IFACE = 'org.freedesktop.NetworkManager' @@ -66,522 +60,7 @@ _NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' _AP_ICON_NAME = 'network-wireless' _OLPC_MESH_ICON_NAME = 'network-mesh' -class WirelessNetworkView(CanvasPulsingIcon): - def __init__(self, initial_ap): - CanvasPulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE, - cache=True) - self._bus = dbus.SystemBus() - self._access_points = {initial_ap.model.object_path: initial_ap} - self._active_ap = None - self._device = initial_ap.device - self._palette_icon = None - self._disconnect_item = None - self._connect_item = None - self._greyed_out = False - self._name = initial_ap.name - self._mode = initial_ap.mode - self._strength = initial_ap.strength - self._flags = initial_ap.flags - self._wpa_flags = initial_ap.wpa_flags - self._rsn_flags = initial_ap.rsn_flags - self._device_caps = 0 - self._device_state = None - self._connection = None - self._color = None - - if self._mode == network.NM_802_11_MODE_ADHOC \ - and self._name_encodes_colors(): - encoded_color = self._name.split("#", 1) - if len(encoded_color) == 2: - self._color = xocolor.XoColor('#' + encoded_color[1]) - else: - sha_hash = hashlib.sha1() - data = self._name + hex(self._flags) - sha_hash.update(data) - digest = hash(sha_hash.digest()) - index = digest % len(xocolor.colors) - - self._color = xocolor.XoColor('%s,%s' % - (xocolor.colors[index][0], - xocolor.colors[index][1])) - - self.connect('button-release-event', self.__button_release_event_cb) - - pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), - style.COLOR_TRANSPARENT.get_svg())) - self.props.pulse_color = pulse_color - - self._palette = self._create_palette() - self.set_palette(self._palette) - self._palette_icon.props.xo_color = self._color - - if network.find_connection_by_ssid(self._name) is not None: - self.props.badge_name = "emblem-favorite" - self._palette_icon.props.badge_name = "emblem-favorite" - elif initial_ap.flags == network.NM_802_11_AP_FLAGS_PRIVACY: - self.props.badge_name = "emblem-locked" - self._palette_icon.props.badge_name = "emblem-locked" - else: - self.props.badge_name = None - self._palette_icon.props.badge_name = None - - interface_props = dbus.Interface(self._device, - 'org.freedesktop.DBus.Properties') - interface_props.Get(_NM_DEVICE_IFACE, 'State', - reply_handler=self.__get_device_state_reply_cb, - error_handler=self.__get_device_state_error_cb) - interface_props.Get(_NM_WIRELESS_IFACE, 'WirelessCapabilities', - reply_handler=self.__get_device_caps_reply_cb, - error_handler=self.__get_device_caps_error_cb) - interface_props.Get(_NM_WIRELESS_IFACE, 'ActiveAccessPoint', - reply_handler=self.__get_active_ap_reply_cb, - error_handler=self.__get_active_ap_error_cb) - - self._bus.add_signal_receiver(self.__device_state_changed_cb, - signal_name='StateChanged', - path=self._device.object_path, - dbus_interface=_NM_DEVICE_IFACE) - self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, - signal_name='PropertiesChanged', - path=self._device.object_path, - dbus_interface=_NM_WIRELESS_IFACE) - - def _name_encodes_colors(self): - """Match #XXXXXX,#YYYYYY at the end of the network name""" - return self._name[-7] == '#' and self._name[-8] == ',' \ - and self._name[-15] == '#' - - def _create_palette(self): - icon_name = get_icon_state(_AP_ICON_NAME, self._strength) - self._palette_icon = Icon(icon_name=icon_name, - icon_size=style.STANDARD_ICON_SIZE, - badge_name=self.props.badge_name) - - p = palette.Palette(primary_text=self._name, - icon=self._palette_icon) - - self._connect_item = MenuItem(_('Connect'), 'dialog-ok') - self._connect_item.connect('activate', self.__connect_activate_cb) - p.menu.append(self._connect_item) - - 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 __device_state_changed_cb(self, new_state, old_state, reason): - self._device_state = new_state - self._update_state() - - def __update_active_ap(self, ap_path): - if ap_path in self._access_points: - # save reference to active AP, so that we always display the - # strength of that one - self._active_ap = self._access_points[ap_path] - self.update_strength() - self._update_state() - elif self._active_ap is not None: - # revert to showing state of strongest AP again - self._active_ap = None - self.update_strength() - self._update_state() - - def __wireless_properties_changed_cb(self, properties): - if 'ActiveAccessPoint' in properties: - self.__update_active_ap(properties['ActiveAccessPoint']) - - def __get_active_ap_reply_cb(self, ap_path): - self.__update_active_ap(ap_path) - - def __get_active_ap_error_cb(self, err): - logging.error('Error getting the active access point: %s', err) - - def __get_device_caps_reply_cb(self, caps): - self._device_caps = caps - - def __get_device_caps_error_cb(self, err): - logging.error('Error getting the wireless device properties: %s', err) - - def __get_device_state_reply_cb(self, state): - self._device_state = state - self._update() - - def __get_device_state_error_cb(self, err): - logging.error('Error getting the device state: %s', err) - - def _update(self): - self._update_state() - self._update_color() - - def _update_state(self): - if self._active_ap is not None: - state = self._device_state - else: - state = network.DEVICE_STATE_UNKNOWN - - if state == network.DEVICE_STATE_ACTIVATED: - connection = network.find_connection_by_ssid(self._name) - if connection: - if self._mode == network.NM_802_11_MODE_INFRA: - connection.set_connected() - - icon_name = '%s-connected' % _AP_ICON_NAME - else: - icon_name = _AP_ICON_NAME - - icon_name = get_icon_state(icon_name, self._strength) - if icon_name: - self.props.icon_name = icon_name - icon = self._palette.props.icon - icon.props.icon_name = icon_name - - if state == network.DEVICE_STATE_PREPARE or \ - state == network.DEVICE_STATE_CONFIG or \ - state == network.DEVICE_STATE_NEED_AUTH or \ - state == network.DEVICE_STATE_IP_CONFIG: - if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() - self._palette.props.secondary_text = _('Connecting...') - self.props.pulsing = True - elif state == network.DEVICE_STATE_ACTIVATED: - if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() - self._palette.props.secondary_text = _('Connected') - self.props.pulsing = False - else: - if self._disconnect_item: - self._disconnect_item.hide() - self._connect_item.show() - self._palette.props.secondary_text = None - self.props.pulsing = False - - def _update_color(self): - if self._greyed_out: - self.props.pulsing = False - self.props.base_color = XoColor('#D5D5D5,#D5D5D5') - else: - self.props.base_color = self._color - - def _disconnect_activate_cb(self, item): - pass - - def _add_ciphers_from_flags(self, flags, pairwise): - ciphers = [] - if pairwise: - if flags & network.NM_802_11_AP_SEC_PAIR_TKIP: - ciphers.append("tkip") - if flags & network.NM_802_11_AP_SEC_PAIR_CCMP: - ciphers.append("ccmp") - else: - if flags & network.NM_802_11_AP_SEC_GROUP_WEP40: - ciphers.append("wep40") - if flags & network.NM_802_11_AP_SEC_GROUP_WEP104: - ciphers.append("wep104") - if flags & network.NM_802_11_AP_SEC_GROUP_TKIP: - ciphers.append("tkip") - if flags & network.NM_802_11_AP_SEC_GROUP_CCMP: - ciphers.append("ccmp") - return ciphers - - def _get_security(self): - if not (self._flags & network.NM_802_11_AP_FLAGS_PRIVACY) and \ - (self._wpa_flags == network.NM_802_11_AP_SEC_NONE) and \ - (self._rsn_flags == network.NM_802_11_AP_SEC_NONE): - # No security - return None - - if (self._flags & network.NM_802_11_AP_FLAGS_PRIVACY) and \ - (self._wpa_flags == network.NM_802_11_AP_SEC_NONE) and \ - (self._rsn_flags == network.NM_802_11_AP_SEC_NONE): - # Static WEP, Dynamic WEP, or LEAP - wireless_security = WirelessSecurity() - wireless_security.key_mgmt = 'none' - return wireless_security - - if (self._mode != network.NM_802_11_MODE_INFRA): - # Stuff after this point requires infrastructure - logging.error('The infrastructure mode is not supoorted' - ' by your wireless device.') - return None - - if (self._rsn_flags & network.NM_802_11_AP_SEC_KEY_MGMT_PSK) and \ - (self._device_caps & network.NM_802_11_DEVICE_CAP_RSN): - # WPA2 PSK first - pairwise = self._add_ciphers_from_flags(self._rsn_flags, True) - group = self._add_ciphers_from_flags(self._rsn_flags, False) - wireless_security = WirelessSecurity() - wireless_security.key_mgmt = 'wpa-psk' - wireless_security.proto = 'rsn' - wireless_security.pairwise = pairwise - wireless_security.group = group - return wireless_security - - if (self._wpa_flags & network.NM_802_11_AP_SEC_KEY_MGMT_PSK) and \ - (self._device_caps & network.NM_802_11_DEVICE_CAP_WPA): - # WPA PSK - pairwise = self._add_ciphers_from_flags(self._wpa_flags, True) - group = self._add_ciphers_from_flags(self._wpa_flags, False) - wireless_security = WirelessSecurity() - wireless_security.key_mgmt = 'wpa-psk' - wireless_security.proto = 'wpa' - wireless_security.pairwise = pairwise - wireless_security.group = group - return wireless_security - - def __connect_activate_cb(self, icon): - self._connect() - - def __button_release_event_cb(self, icon, event): - self._connect() - - def _connect(self): - connection = network.find_connection_by_ssid(self._name) - if connection is None: - settings = Settings() - settings.connection.id = 'Auto ' + self._name - uuid = settings.connection.uuid = unique_id() - settings.connection.type = '802-11-wireless' - settings.wireless.ssid = self._name - - if self._mode == network.NM_802_11_MODE_INFRA: - settings.wireless.mode = 'infrastructure' - elif self._mode == network.NM_802_11_MODE_ADHOC: - settings.wireless.mode = 'adhoc' - settings.wireless.band = 'bg' - settings.ip4_config = IP4Config() - settings.ip4_config.method = 'link-local' - - wireless_security = self._get_security() - settings.wireless_security = wireless_security - - if wireless_security is not None: - settings.wireless.security = '802-11-wireless-security' - - connection = network.add_connection(uuid, settings) - - obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) - netmgr = dbus.Interface(obj, _NM_IFACE) - - netmgr.ActivateConnection(network.SETTINGS_SERVICE, connection.path, - self._device.object_path, - "/", - reply_handler=self.__activate_reply_cb, - error_handler=self.__activate_error_cb) - - def __activate_reply_cb(self, connection): - logging.debug('Connection activated: %s', connection) - - def __activate_error_cb(self, err): - logging.error('Failed to activate connection: %s', err) - - def set_filter(self, query): - self._greyed_out = self._name.lower().find(query) == -1 - self._update_state() - self._update_color() - - def create_keydialog(self, settings, response): - keydialog.create(self._name, self._flags, self._wpa_flags, - self._rsn_flags, self._device_caps, settings, response) - - def update_strength(self): - if self._active_ap is not None: - # display strength of AP that we are connected to - new_strength = self._active_ap.strength - else: - # display the strength of the strongest AP that makes up this - # network, also considering that there may be no APs - new_strength = max([0] + [ap.strength for ap in - self._access_points.values()]) - - if new_strength != self._strength: - self._strength = new_strength - self._update_state() - - def add_ap(self, ap): - self._access_points[ap.model.object_path] = ap - self.update_strength() - - def remove_ap(self, ap): - path = ap.model.object_path - if path not in self._access_points: - return - del self._access_points[path] - if self._active_ap == ap: - self._active_ap = None - self.update_strength() - - def num_aps(self): - return len(self._access_points) - - def find_ap(self, ap_path): - if ap_path not in self._access_points: - return None - return self._access_points[ap_path] - - def is_olpc_mesh(self): - return self._mode == network.NM_802_11_MODE_ADHOC \ - and self.name == "olpc-mesh" - - def remove_all_aps(self): - for ap in self._access_points.values(): - ap.disconnect() - self._access_points = {} - self._active_ap = None - self.update_strength() - - def disconnect(self): - self._bus.remove_signal_receiver(self.__device_state_changed_cb, - signal_name='StateChanged', - path=self._device.object_path, - dbus_interface=_NM_DEVICE_IFACE) - self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, - signal_name='PropertiesChanged', - path=self._device.object_path, - dbus_interface=_NM_WIRELESS_IFACE) - - -class OlpcMeshView(CanvasPulsingIcon): - def __init__(self, mesh_mgr, channel): - CanvasPulsingIcon.__init__(self, icon_name=_OLPC_MESH_ICON_NAME, - size=style.STANDARD_ICON_SIZE, cache=True) - self._bus = dbus.SystemBus() - self._channel = channel - self._mesh_mgr = mesh_mgr - self._disconnect_item = None - self._connect_item = None - self._greyed_out = False - self._name = '' - self._device_state = None - self._connection = None - self._active = False - device = mesh_mgr.mesh_device - - self.connect('button-release-event', self.__button_release_event_cb) - - interface_props = dbus.Interface(device, - 'org.freedesktop.DBus.Properties') - interface_props.Get(_NM_DEVICE_IFACE, 'State', - reply_handler=self.__get_device_state_reply_cb, - error_handler=self.__get_device_state_error_cb) - interface_props.Get(_NM_OLPC_MESH_IFACE, 'ActiveChannel', - reply_handler=self.__get_active_channel_reply_cb, - error_handler=self.__get_active_channel_error_cb) - - self._bus.add_signal_receiver(self.__device_state_changed_cb, - signal_name='StateChanged', - path=device.object_path, - dbus_interface=_NM_DEVICE_IFACE) - self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, - signal_name='PropertiesChanged', - path=device.object_path, - dbus_interface=_NM_OLPC_MESH_IFACE) - - pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), - style.COLOR_TRANSPARENT.get_svg())) - self.props.pulse_color = pulse_color - self.props.base_color = profile.get_color() - self._palette = self._create_palette() - self.set_palette(self._palette) - - def _create_palette(self): - _palette = palette.Palette(_("Mesh Network %d") % self._channel) - - self._connect_item = MenuItem(_('Connect'), 'dialog-ok') - self._connect_item.connect('activate', self.__connect_activate_cb) - _palette.menu.append(self._connect_item) - - return _palette - - def __get_device_state_reply_cb(self, state): - self._device_state = state - self._update() - - def __get_device_state_error_cb(self, err): - logging.error('Error getting the device state: %s', err) - - def __device_state_changed_cb(self, new_state, old_state, reason): - self._device_state = new_state - self._update() - - def __get_active_channel_reply_cb(self, channel): - self._active = (channel == self._channel) - self._update() - - def __get_active_channel_error_cb(self, err): - logging.error('Error getting the active channel: %s', err) - - def __wireless_properties_changed_cb(self, properties): - if 'ActiveChannel' in properties: - channel = properties['ActiveChannel'] - self._active = (channel == self._channel) - self._update() - - def _update(self): - if self._active: - state = self._device_state - else: - state = network.DEVICE_STATE_UNKNOWN - - if state in [network.DEVICE_STATE_PREPARE, - network.DEVICE_STATE_CONFIG, - network.DEVICE_STATE_NEED_AUTH, - network.DEVICE_STATE_IP_CONFIG]: - if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() - self._palette.props.secondary_text = _('Connecting...') - self.props.pulsing = True - elif state == network.DEVICE_STATE_ACTIVATED: - if self._disconnect_item: - self._disconnect_item.show() - self._connect_item.hide() - self._palette.props.secondary_text = _('Connected') - self.props.pulsing = False - else: - if self._disconnect_item: - self._disconnect_item.hide() - self._connect_item.show() - self._palette.props.secondary_text = None - self.props.pulsing = False - - def _update_color(self): - if self._greyed_out: - self.props.base_color = XoColor('#D5D5D5,#D5D5D5') - else: - self.props.base_color = profile.get_color() - - def __connect_activate_cb(self, icon): - self._connect() - - def __button_release_event_cb(self, icon, event): - self._connect() - - def _connect(self): - self._mesh_mgr.user_activate_channel(self._channel) - - def __activate_reply_cb(self, connection): - logging.debug('Connection activated: %s', connection) - - def __activate_error_cb(self, err): - logging.error('Failed to activate connection: %s', err) - - def set_filter(self, query): - self._greyed_out = (query != '') - self._update_color() - - def disconnect(self): - self._bus.remove_signal_receiver(self.__device_state_changed_cb, - signal_name='StateChanged', - path=self._device.object_path, - dbus_interface=_NM_DEVICE_IFACE) - self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, - signal_name='PropertiesChanged', - path=self._device.object_path, - dbus_interface=_NM_OLPC_MESH_IFACE) +_AUTOSEARCH_TIMEOUT = 1000 class ActivityView(hippo.CanvasBox): @@ -589,6 +68,9 @@ class ActivityView(hippo.CanvasBox): hippo.CanvasBox.__init__(self) self._model = model + self._model.connect('current-buddy-added', self.__buddy_added_cb) + self._model.connect('current-buddy-removed', self.__buddy_removed_cb) + self._icons = {} self._palette = None @@ -598,31 +80,30 @@ class ActivityView(hippo.CanvasBox): self._icon = self._create_icon() self._layout.add(self._icon, center=True) - self._update_palette() + self._palette = self._create_palette() + self._icon.set_palette(self._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 + for buddy in self._model.props.current_buddies: + self._add_buddy(buddy) def _create_icon(self): - icon = CanvasIcon(file_name=self._model.get_icon_name(), + icon = CanvasIcon(file_name=self._model.bundle.get_icon(), 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(), + p_text = glib.markup_escape_text(self._model.bundle.get_name()) + p_icon = Icon(file=self._model.bundle.get_icon(), 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, + p = palette.Palette(None, + primary_text=p_text, icon=p_icon) - private = self._model.activity.props.private - joined = self._model.activity.props.joined + private = self._model.props.private + joined = get_owner_instance() in self._model.props.buddies if joined: item = MenuItem(_('Resume'), 'activity-start') @@ -637,42 +118,30 @@ class ActivityView(hippo.CanvasBox): 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) + return key in self._icons + + def __buddy_added_cb(self, activity, buddy): + self._add_buddy(buddy) - def add_buddy_icon(self, key, icon): - self._icons[key] = icon + def _add_buddy(self, buddy): + icon = BuddyIcon(buddy, style.STANDARD_ICON_SIZE) + self._icons[buddy.props.key] = icon self._layout.add(icon) - def remove_buddy_icon(self, key): - icon = self._icons[key] - del self._icons[key] + def __buddy_removed_cb(self, activity, buddy): + icon = self._icons[buddy.props.key] + del self._icons[buddy.props.key] icon.destroy() def _clicked_cb(self, item): - shell_model = shell.get_model() - activity = shell_model.get_activity_by_id(self._model.get_id()) - if activity: - activity.get_window().activate(gtk.get_current_event_time()) - return - - bundle_id = self._model.get_bundle_id() - bundle = bundleregistry.get_registry().get_bundle(bundle_id) - - launcher.add_launcher(self._model.get_id(), - bundle.get_icon(), - self._model.get_color()) - - handle = ActivityHandle(self._model.get_id()) - activityfactory.create(bundle, handle) + bundle = self._model.get_bundle() + misc.launch(bundle, activity_id=self._model.activity_id, + color=self._model.get_color()) def set_filter(self, query): - text_to_check = self._model.activity.props.name.lower() + \ - self._model.activity.props.type.lower() + text_to_check = self._model.bundle.get_name().lower() + \ + self._model.bundle.get_bundle_id().lower() if text_to_check.find(query) == -1: self._icon.props.stroke_color = '#D5D5D5' self._icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() @@ -683,31 +152,13 @@ class ActivityView(hippo.CanvasBox): 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') - -_AUTOSEARCH_TIMEOUT = 1000 - class MeshToolbar(gtk.Toolbar): __gtype_name__ = 'MeshToolbar' __gsignals__ = { - 'query-changed': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([str])) + 'query-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), } def __init__(self): @@ -770,15 +221,23 @@ class MeshToolbar(gtk.Toolbar): return False -class DeviceObserver(object): - def __init__(self, box, device): - self._box = box +class DeviceObserver(gobject.GObject): + __gsignals__ = { + '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])), + } + + def __init__(self, device): + gobject.GObject.__init__(self) self._bus = dbus.SystemBus() - self._device = device + self.device = device - wireless = dbus.Interface(self._device, _NM_WIRELESS_IFACE) - wireless.GetAccessPoints(reply_handler=self._get_access_points_reply_cb, - error_handler=self._get_access_points_error_cb) + wireless = dbus.Interface(device, _NM_WIRELESS_IFACE) + wireless.GetAccessPoints( + reply_handler=self._get_access_points_reply_cb, + error_handler=self._get_access_points_error_cb) self._bus.add_signal_receiver(self.__access_point_added_cb, signal_name='AccessPointAdded', @@ -792,35 +251,42 @@ class DeviceObserver(object): def _get_access_points_reply_cb(self, access_points_o): for ap_o in access_points_o: ap = self._bus.get_object(_NM_SERVICE, ap_o) - self._box.add_access_point(self._device, ap) + self.emit('access-point-added', ap) def _get_access_points_error_cb(self, err): logging.error('Failed to get access points: %s', err) def __access_point_added_cb(self, access_point_o): ap = self._bus.get_object(_NM_SERVICE, access_point_o) - self._box.add_access_point(self._device, ap) + self.emit('access-point-added', ap) def __access_point_removed_cb(self, access_point_o): - self._box.remove_access_point(access_point_o) + self.emit('access-point-removed', access_point_o) def disconnect(self): self._bus.remove_signal_receiver(self.__access_point_added_cb, signal_name='AccessPointAdded', - path=self._device.object_path, + path=self.device.object_path, dbus_interface=_NM_WIRELESS_IFACE) self._bus.remove_signal_receiver(self.__access_point_removed_cb, signal_name='AccessPointRemoved', - path=self._device.object_path, + path=self.device.object_path, dbus_interface=_NM_WIRELESS_IFACE) class NetworkManagerObserver(object): + + _SHOW_ADHOC_GCONF_KEY = '/desktop/sugar/network/adhoc' + def __init__(self, box): self._box = box self._bus = None self._devices = {} self._netmgr = None + self._olpc_mesh_device_o = None + + client = gconf.client_get_default() + self._have_adhoc_networks = client.get_bool(self._SHOW_ADHOC_GCONF_KEY) def listen(self): try: @@ -840,6 +306,9 @@ class NetworkManagerObserver(object): self._bus.add_signal_receiver(self.__device_removed_cb, signal_name='DeviceRemoved', dbus_interface=_NM_IFACE) + self._bus.add_signal_receiver(self.__properties_changed_cb, + signal_name='PropertiesChanged', + dbus_interface=_NM_IFACE) settings = network.get_settings() if settings is not None: @@ -849,13 +318,12 @@ class NetworkManagerObserver(object): # FIXME It would be better to do all of this async, but I cannot think # of a good way to. NM could really use some love here. - netmgr_props = dbus.Interface( - self._netmgr, 'org.freedesktop.DBus.Properties') + netmgr_props = dbus.Interface(self._netmgr, dbus.PROPERTIES_IFACE) active_connections_o = netmgr_props.Get(_NM_IFACE, 'ActiveConnections') for conn_o in active_connections_o: obj = self._bus.get_object(_NM_IFACE, conn_o) - props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties') + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) state = props.Get(_NM_ACTIVE_CONN_IFACE, 'State') if state == network.NM_ACTIVE_CONNECTION_STATE_ACTIVATING: ap_o = props.Get(_NM_ACTIVE_CONN_IFACE, 'SpecificObject') @@ -867,8 +335,8 @@ class NetworkManagerObserver(object): settings = kwargs['connection'].get_settings() net.create_keydialog(settings, kwargs['response']) if not found: - logging.error('Could not determine AP for' - ' specific object %s' % conn_o) + logging.error('Could not determine AP for specific object' + ' %s', conn_o) def __get_devices_reply_cb(self, devices_o): for dev_o in devices_o: @@ -879,12 +347,19 @@ class NetworkManagerObserver(object): def _check_device(self, device_o): device = self._bus.get_object(_NM_SERVICE, device_o) - props = dbus.Interface(device, 'org.freedesktop.DBus.Properties') + props = dbus.Interface(device, dbus.PROPERTIES_IFACE) device_type = props.Get(_NM_DEVICE_IFACE, 'DeviceType') if device_type == network.DEVICE_TYPE_802_11_WIRELESS: - self._devices[device_o] = DeviceObserver(self._box, device) + self._devices[device_o] = DeviceObserver(device) + self._devices[device_o].connect('access-point-added', + self.__ap_added_cb) + self._devices[device_o].connect('access-point-removed', + self.__ap_removed_cb) + if self._have_adhoc_networks: + self._box.add_adhoc_networks(device) elif device_type == network.DEVICE_TYPE_802_11_OLPC_MESH: + self._olpc_mesh_device_o = device_o self._box.enable_olpc_mesh(device) def _get_device_path_error_cb(self, err): @@ -898,23 +373,41 @@ class NetworkManagerObserver(object): observer = self._devices[device_o] observer.disconnect() del self._devices[device_o] + if self._have_adhoc_networks: + self._box.remove_adhoc_networks() return - - device = self._bus.get_object(_NM_SERVICE, device_o) - props = dbus.Interface(device, 'org.freedesktop.DBus.Properties') - device_type = props.Get(_NM_DEVICE_IFACE, 'DeviceType') - if device_type == network.DEVICE_TYPE_802_11_OLPC_MESH: - self._box.disable_olpc_mesh(device) + + if self._olpc_mesh_device_o == device_o: + self._box.disable_olpc_mesh(device_o) + + def __ap_added_cb(self, device_observer, access_point): + self._box.add_access_point(device_observer.device, access_point) + + def __ap_removed_cb(self, device_observer, access_point_o): + self._box.remove_access_point(access_point_o) + + def __properties_changed_cb(self, properties): + if 'WirelessHardwareEnabled' in properties: + if properties['WirelessHardwareEnabled']: + if not self._have_adhoc_networks: + self._box.remove_adhoc_networks() + elif properties['WirelessHardwareEnabled']: + for device in self._devices: + if self._have_adhoc_networks: + self._box.add_adhoc_networks(device) + class MeshBox(gtk.VBox): __gtype_name__ = 'SugarMeshBox' def __init__(self): - logging.debug("STARTUP: Loading the mesh view") + logging.debug('STARTUP: Loading the mesh view') gobject.GObject.__init__(self) self.wireless_networks = {} + self._adhoc_manager = None + self._adhoc_networks = [] self._model = neighborhood.get_model() self._buddies = {} @@ -942,11 +435,10 @@ class MeshBox(gtk.VBox): self._layout_box.set_layout(self._layout) for buddy_model in self._model.get_buddies(): - self._add_alone_buddy(buddy_model) + self._add_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) @@ -970,24 +462,22 @@ class MeshBox(gtk.VBox): gtk.VBox.do_size_allocate(self, allocation) def _buddy_added_cb(self, model, buddy_model): - self._add_alone_buddy(buddy_model) + self._add_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 _add_alone_buddy(self, buddy_model): + def _add_buddy(self, buddy_model): + buddy_model.connect('notify::current-activity', + self.__buddy_notify_current_activity_cb) + if buddy_model.props.current_activity is not None: + return icon = BuddyIcon(buddy_model) if buddy_model.is_owner(): self._owner_icon = icon @@ -996,36 +486,23 @@ class MeshBox(gtk.VBox): if hasattr(icon, 'set_filter'): icon.set_filter(self._query) - self._buddies[buddy_model.get_buddy().object_path()] = icon + self._buddies[buddy_model.props.key] = icon - def _remove_alone_buddy(self, buddy_model): - icon = self._buddies[buddy_model.get_buddy().object_path()] + def _remove_buddy(self, buddy_model): + logging.debug('MeshBox._remove_buddy') + icon = self._buddies[buddy_model.props.key] self._layout.remove(icon) - del self._buddies[buddy_model.get_buddy().object_path()] + del self._buddies[buddy_model.props.key] 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 __buddy_notify_current_activity_cb(self, buddy_model, pspec): + logging.debug('MeshBox.__buddy_notify_current_activity_cb %s', + buddy_model.props.current_activity) + if buddy_model.props.current_activity is None: + if not buddy_model.props.key in self._buddies: + self._add_buddy(buddy_model) + elif buddy_model.props.key in self._buddies: + self._remove_buddy(buddy_model) def _add_activity(self, activity_model): icon = ActivityView(activity_model) @@ -1034,58 +511,70 @@ class MeshBox(gtk.VBox): if hasattr(icon, 'set_filter'): icon.set_filter(self._query) - self._activities[activity_model.get_id()] = icon + self._activities[activity_model.activity_id] = icon def _remove_activity(self, activity_model): - icon = self._activities[activity_model.get_id()] + icon = self._activities[activity_model.activity_id] self._layout.remove(icon) - del self._activities[activity_model.get_id()] + del self._activities[activity_model.activity_id] icon.destroy() # add AP to its corresponding network icon on the desktop, # creating one if it doesn't already exist def _add_ap_to_network(self, ap): - hash = ap.network_hash() - if hash in self.wireless_networks: - self.wireless_networks[hash].add_ap(ap) + hash_value = ap.network_hash() + if hash_value in self.wireless_networks: + self.wireless_networks[hash_value].add_ap(ap) else: # this is a new network icon = WirelessNetworkView(ap) - self.wireless_networks[hash] = icon + self.wireless_networks[hash_value] = icon self._layout.add(icon) if hasattr(icon, 'set_filter'): icon.set_filter(self._query) - def _remove_net_if_empty(self, net, hash): + def _remove_net_if_empty(self, net, hash_value): # remove a network if it has no APs left if net.num_aps() == 0: net.disconnect() self._layout.remove(net) - del self.wireless_networks[hash] + del self.wireless_networks[hash_value] - def _ap_props_changed_cb(self, ap, old_hash): + def _ap_props_changed_cb(self, ap, old_hash_value): # if we have mesh hardware, ignore OLPC mesh networks that appear as # normal wifi networks if len(self._mesh) > 0 and ap.mode == network.NM_802_11_MODE_ADHOC \ - and ap.name == "olpc-mesh": - logging.debug("ignoring OLPC mesh IBSS") + and ap.name == 'olpc-mesh': + logging.debug('ignoring OLPC mesh IBSS') ap.disconnect() return - if old_hash is None: # new AP finished initializing + if self._adhoc_manager is not None and \ + network.is_sugar_adhoc_network(ap.name) and \ + ap.mode == network.NM_802_11_MODE_ADHOC: + if old_hash_value is None: + # new Ad-hoc network finished initializing + self._adhoc_manager.add_access_point(ap) + # we are called as well in other cases but we do not need to + # act here as we don't display signal strength for Ad-hoc networks + return + + if old_hash_value is None: + # new AP finished initializing self._add_ap_to_network(ap) return - hash = ap.network_hash() - if old_hash == hash: + hash_value = ap.network_hash() + if old_hash_value == hash_value: # no change in network identity, so just update signal strengths - self.wireless_networks[hash].update_strength() + self.wireless_networks[hash_value].update_strength() return # properties change includes a change of the identity of the network # that it is on. so create this as a new network. - self.wireless_networks[old_hash].remove_ap(ap) - self._remove_net_if_empty(self.wireless_networks[old_hash], old_hash) + self.wireless_networks[old_hash_value].remove_ap(ap) + self._remove_net_if_empty(self.wireless_networks[old_hash_value], + old_hash_value) self._add_ap_to_network(ap) def add_access_point(self, device, ap_o): @@ -1094,6 +583,11 @@ class MeshBox(gtk.VBox): ap.initialize() def remove_access_point(self, ap_o): + if self._adhoc_manager is not None: + if self._adhoc_manager.is_sugar_adhoc_access_point(ap_o): + self._adhoc_manager.remove_access_point(ap_o) + return + # we don't keep an index of ap object path to network, but since # we'll only ever have a handful of networks, just try them all... for net in self.wireless_networks.values(): @@ -1108,7 +602,26 @@ class MeshBox(gtk.VBox): # it's not an error if the AP isn't found, since we might have ignored # it (e.g. olpc-mesh adhoc network) - logging.debug('Can not remove access point %s' % ap_o) + logging.debug('Can not remove access point %s', ap_o) + + def add_adhoc_networks(self, device): + if self._adhoc_manager is None: + self._adhoc_manager = get_adhoc_manager_instance() + self._adhoc_manager.start_listening(device) + self._add_adhoc_network_icon(1) + self._add_adhoc_network_icon(6) + self._add_adhoc_network_icon(11) + self._adhoc_manager.autoconnect() + + def remove_adhoc_networks(self): + for icon in self._adhoc_networks: + self._layout.remove(icon) + self._adhoc_networks = [] + + def _add_adhoc_network_icon(self, channel): + icon = SugarAdhocView(channel) + self._layout.add(icon) + self._adhoc_networks.append(icon) def _add_olpc_mesh_icon(self, mesh_mgr, channel): icon = OlpcMeshView(mesh_mgr, channel) @@ -1123,15 +636,15 @@ class MeshBox(gtk.VBox): # the OLPC mesh can be recognised as a "normal" wifi network. remove # any such normal networks if they have been created - for hash, net in self.wireless_networks.iteritems(): + for hash_value, net in self.wireless_networks.iteritems(): if not net.is_olpc_mesh(): continue - logging.debug("removing OLPC mesh IBSS") + logging.debug('removing OLPC mesh IBSS') net.remove_all_aps() net.disconnect() self._layout.remove(net) - del self.wireless_networks[hash] + del self.wireless_networks[hash_value] def disable_olpc_mesh(self, mesh_device): for icon in self._mesh: diff --git a/src/jarabe/desktop/networkviews.py b/src/jarabe/desktop/networkviews.py new file mode 100644 index 0000000..99d46b6 --- /dev/null +++ b/src/jarabe/desktop/networkviews.py @@ -0,0 +1,721 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer +# Copyright (C) 2009-2010 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 hashlib + +import dbus +import glib + +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.graphics import xocolor +from sugar.graphics import style +from sugar.graphics.icon import get_icon_state +from sugar.graphics import palette +from sugar.graphics.menuitem import MenuItem +from sugar.util import unique_id +from sugar import profile + +from jarabe.view.pulsingicon import CanvasPulsingIcon +from jarabe.desktop import keydialog +from jarabe.model import network +from jarabe.model.network import Settings +from jarabe.model.network import IP4Config +from jarabe.model.network import WirelessSecurity +from jarabe.model.adhoc import get_adhoc_manager_instance + + +_NM_SERVICE = 'org.freedesktop.NetworkManager' +_NM_IFACE = 'org.freedesktop.NetworkManager' +_NM_PATH = '/org/freedesktop/NetworkManager' +_NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' +_NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +_NM_OLPC_MESH_IFACE = 'org.freedesktop.NetworkManager.Device.OlpcMesh' +_NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +_NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' + +_AP_ICON_NAME = 'network-wireless' +_OLPC_MESH_ICON_NAME = 'network-mesh' + + +class WirelessNetworkView(CanvasPulsingIcon): + def __init__(self, initial_ap): + CanvasPulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE, + cache=True) + self._bus = dbus.SystemBus() + self._access_points = {initial_ap.model.object_path: initial_ap} + self._active_ap = None + self._device = initial_ap.device + self._palette_icon = None + self._disconnect_item = None + self._connect_item = None + self._greyed_out = False + self._name = initial_ap.name + self._mode = initial_ap.mode + self._strength = initial_ap.strength + self._flags = initial_ap.flags + self._wpa_flags = initial_ap.wpa_flags + self._rsn_flags = initial_ap.rsn_flags + self._device_caps = 0 + self._device_state = None + self._color = None + + if self._mode == network.NM_802_11_MODE_ADHOC and \ + network.is_sugar_adhoc_network(self._name): + self._color = profile.get_color() + else: + sha_hash = hashlib.sha1() + data = self._name + hex(self._flags) + sha_hash.update(data) + digest = hash(sha_hash.digest()) + index = digest % len(xocolor.colors) + + self._color = xocolor.XoColor('%s,%s' % + (xocolor.colors[index][0], + xocolor.colors[index][1])) + + self.connect('button-release-event', self.__button_release_event_cb) + + pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.pulse_color = pulse_color + + self._palette = self._create_palette() + self.set_palette(self._palette) + self._palette_icon.props.xo_color = self._color + self._update_badge() + + interface_props = dbus.Interface(self._device, dbus.PROPERTIES_IFACE) + interface_props.Get(_NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_device_state_reply_cb, + error_handler=self.__get_device_state_error_cb) + interface_props.Get(_NM_WIRELESS_IFACE, 'WirelessCapabilities', + reply_handler=self.__get_device_caps_reply_cb, + error_handler=self.__get_device_caps_error_cb) + interface_props.Get(_NM_WIRELESS_IFACE, 'ActiveAccessPoint', + reply_handler=self.__get_active_ap_reply_cb, + error_handler=self.__get_active_ap_error_cb) + + self._bus.add_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=_NM_WIRELESS_IFACE) + + def _create_palette(self): + icon_name = get_icon_state(_AP_ICON_NAME, self._strength) + self._palette_icon = Icon(icon_name=icon_name, + icon_size=style.STANDARD_ICON_SIZE, + badge_name=self.props.badge_name) + + p = palette.Palette(primary_text=glib.markup_escape_text(self._name), + icon=self._palette_icon) + + self._connect_item = MenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('activate', self.__connect_activate_cb) + p.menu.append(self._connect_item) + + 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 __device_state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update_state() + self._update_icon() + self._update_badge() + + def __update_active_ap(self, ap_path): + if ap_path in self._access_points: + # save reference to active AP, so that we always display the + # strength of that one + self._active_ap = self._access_points[ap_path] + self.update_strength() + elif self._active_ap is not None: + # revert to showing state of strongest AP again + self._active_ap = None + self.update_strength() + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveAccessPoint' in properties: + self.__update_active_ap(properties['ActiveAccessPoint']) + + def __get_active_ap_reply_cb(self, ap_path): + self.__update_active_ap(ap_path) + + def __get_active_ap_error_cb(self, err): + logging.error('Error getting the active access point: %s', err) + + def __get_device_caps_reply_cb(self, caps): + self._device_caps = caps + + def __get_device_caps_error_cb(self, err): + logging.error('Error getting the wireless device properties: %s', err) + + def __get_device_state_reply_cb(self, state): + self._device_state = state + self._update_state() + self._update_color() + self._update_badge() + + def __get_device_state_error_cb(self, err): + logging.error('Error getting the device state: %s', err) + + def _update_icon(self): + if self._mode == network.NM_802_11_MODE_ADHOC and \ + network.is_sugar_adhoc_network(self._name): + channel = max([1] + [ap.channel for ap in + self._access_points.values()]) + if self._device_state == network.DEVICE_STATE_ACTIVATED and \ + self._active_ap is not None: + icon_name = 'network-adhoc-%s-connected' % channel + else: + icon_name = 'network-adhoc-%s' % channel + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + else: + if self._device_state == network.DEVICE_STATE_ACTIVATED and \ + self._active_ap is not None: + icon_name = '%s-connected' % _AP_ICON_NAME + else: + icon_name = _AP_ICON_NAME + + icon_name = get_icon_state(icon_name, self._strength) + if icon_name: + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + + def _update_badge(self): + if self._mode != network.NM_802_11_MODE_ADHOC: + if network.find_connection_by_ssid(self._name) is not None: + self.props.badge_name = 'emblem-favorite' + self._palette_icon.props.badge_name = 'emblem-favorite' + elif self._flags == network.NM_802_11_AP_FLAGS_PRIVACY: + self.props.badge_name = 'emblem-locked' + self._palette_icon.props.badge_name = 'emblem-locked' + else: + self.props.badge_name = None + self._palette_icon.props.badge_name = None + else: + self.props.badge_name = None + self._palette_icon.props.badge_name = None + + def _update_state(self): + if self._active_ap is not None: + state = self._device_state + else: + state = network.DEVICE_STATE_UNKNOWN + + if state == network.DEVICE_STATE_PREPARE or \ + state == network.DEVICE_STATE_CONFIG or \ + state == network.DEVICE_STATE_NEED_AUTH or \ + state == network.DEVICE_STATE_IP_CONFIG: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connecting...') + self.props.pulsing = True + elif state == network.DEVICE_STATE_ACTIVATED: + connection = network.find_connection_by_ssid(self._name) + if connection is not None: + if self._mode == network.NM_802_11_MODE_INFRA: + connection.set_connected() + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connected') + self.props.pulsing = False + else: + if self._disconnect_item: + self._disconnect_item.hide() + self._connect_item.show() + self._palette.props.secondary_text = None + self.props.pulsing = False + + def _update_color(self): + if self._greyed_out: + self.props.pulsing = False + self.props.base_color = XoColor('#D5D5D5,#D5D5D5') + else: + self.props.base_color = self._color + + def _disconnect_activate_cb(self, item): + if self._mode == network.NM_802_11_MODE_INFRA: + connection = network.find_connection_by_ssid(self._name) + if connection: + connection.disable_autoconnect() + + ap_paths = self._access_points.keys() + network.disconnect_access_points(ap_paths) + + def _add_ciphers_from_flags(self, flags, pairwise): + ciphers = [] + if pairwise: + if flags & network.NM_802_11_AP_SEC_PAIR_TKIP: + ciphers.append('tkip') + if flags & network.NM_802_11_AP_SEC_PAIR_CCMP: + ciphers.append('ccmp') + else: + if flags & network.NM_802_11_AP_SEC_GROUP_WEP40: + ciphers.append('wep40') + if flags & network.NM_802_11_AP_SEC_GROUP_WEP104: + ciphers.append('wep104') + if flags & network.NM_802_11_AP_SEC_GROUP_TKIP: + ciphers.append('tkip') + if flags & network.NM_802_11_AP_SEC_GROUP_CCMP: + ciphers.append('ccmp') + return ciphers + + def _get_security(self): + if not (self._flags & network.NM_802_11_AP_FLAGS_PRIVACY) and \ + (self._wpa_flags == network.NM_802_11_AP_SEC_NONE) and \ + (self._rsn_flags == network.NM_802_11_AP_SEC_NONE): + # No security + return None + + if (self._flags & network.NM_802_11_AP_FLAGS_PRIVACY) and \ + (self._wpa_flags == network.NM_802_11_AP_SEC_NONE) and \ + (self._rsn_flags == network.NM_802_11_AP_SEC_NONE): + # Static WEP, Dynamic WEP, or LEAP + wireless_security = WirelessSecurity() + wireless_security.key_mgmt = 'none' + return wireless_security + + if (self._mode != network.NM_802_11_MODE_INFRA): + # Stuff after this point requires infrastructure + logging.error('The infrastructure mode is not supoorted' + ' by your wireless device.') + return None + + if (self._rsn_flags & network.NM_802_11_AP_SEC_KEY_MGMT_PSK) and \ + (self._device_caps & network.NM_802_11_DEVICE_CAP_RSN): + # WPA2 PSK first + pairwise = self._add_ciphers_from_flags(self._rsn_flags, True) + group = self._add_ciphers_from_flags(self._rsn_flags, False) + wireless_security = WirelessSecurity() + wireless_security.key_mgmt = 'wpa-psk' + wireless_security.proto = 'rsn' + wireless_security.pairwise = pairwise + wireless_security.group = group + return wireless_security + + if (self._wpa_flags & network.NM_802_11_AP_SEC_KEY_MGMT_PSK) and \ + (self._device_caps & network.NM_802_11_DEVICE_CAP_WPA): + # WPA PSK + pairwise = self._add_ciphers_from_flags(self._wpa_flags, True) + group = self._add_ciphers_from_flags(self._wpa_flags, False) + wireless_security = WirelessSecurity() + wireless_security.key_mgmt = 'wpa-psk' + wireless_security.proto = 'wpa' + wireless_security.pairwise = pairwise + wireless_security.group = group + return wireless_security + + def __connect_activate_cb(self, icon): + self._connect() + + def __button_release_event_cb(self, icon, event): + self._connect() + + def _connect(self): + connection = network.find_connection_by_ssid(self._name) + if connection is None: + settings = Settings() + settings.connection.id = 'Auto ' + self._name + uuid = settings.connection.uuid = unique_id() + settings.connection.type = '802-11-wireless' + settings.wireless.ssid = self._name + + if self._mode == network.NM_802_11_MODE_INFRA: + settings.wireless.mode = 'infrastructure' + elif self._mode == network.NM_802_11_MODE_ADHOC: + settings.wireless.mode = 'adhoc' + settings.wireless.band = 'bg' + settings.ip4_config = IP4Config() + settings.ip4_config.method = 'link-local' + + wireless_security = self._get_security() + settings.wireless_security = wireless_security + + if wireless_security is not None: + settings.wireless.security = '802-11-wireless-security' + + connection = network.add_connection(uuid, settings) + + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + + netmgr.ActivateConnection(network.SETTINGS_SERVICE, connection.path, + self._device.object_path, + '/', + reply_handler=self.__activate_reply_cb, + error_handler=self.__activate_error_cb) + + def __activate_reply_cb(self, connection): + logging.debug('Connection activated: %s', connection) + + def __activate_error_cb(self, err): + logging.error('Failed to activate connection: %s', err) + + def set_filter(self, query): + self._greyed_out = self._name.lower().find(query) == -1 + self._update_icon() + self._update_color() + + def create_keydialog(self, settings, response): + keydialog.create(self._name, self._flags, self._wpa_flags, + self._rsn_flags, self._device_caps, settings, + response) + + def update_strength(self): + if self._active_ap is not None: + # display strength of AP that we are connected to + new_strength = self._active_ap.strength + else: + # display the strength of the strongest AP that makes up this + # network, also considering that there may be no APs + new_strength = max([0] + [ap.strength for ap in + self._access_points.values()]) + + if new_strength != self._strength: + self._strength = new_strength + self._update_icon() + + def add_ap(self, ap): + self._access_points[ap.model.object_path] = ap + self.update_strength() + + def remove_ap(self, ap): + path = ap.model.object_path + if path not in self._access_points: + return + del self._access_points[path] + if self._active_ap == ap: + self._active_ap = None + self.update_strength() + + def num_aps(self): + return len(self._access_points) + + def find_ap(self, ap_path): + if ap_path not in self._access_points: + return None + return self._access_points[ap_path] + + def is_olpc_mesh(self): + return self._mode == network.NM_802_11_MODE_ADHOC \ + and self.name == 'olpc-mesh' + + def remove_all_aps(self): + for ap in self._access_points.values(): + ap.disconnect() + self._access_points = {} + self._active_ap = None + self.update_strength() + + def disconnect(self): + self._bus.remove_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=_NM_WIRELESS_IFACE) + + +class SugarAdhocView(CanvasPulsingIcon): + """To mimic the mesh behavior on devices where mesh hardware is + not available we support the creation of an Ad-hoc network on + three channels 1, 6, 11. This is the class for an icon + representing a channel in the neighborhood view. + + """ + + _ICON_NAME = 'network-adhoc-' + _NAME = 'Ad-hoc Network ' + + def __init__(self, channel): + CanvasPulsingIcon.__init__(self, + icon_name=self._ICON_NAME + str(channel), + size=style.STANDARD_ICON_SIZE, cache=True) + self._bus = dbus.SystemBus() + self._channel = channel + self._disconnect_item = None + self._connect_item = None + self._palette_icon = None + self._greyed_out = False + + get_adhoc_manager_instance().connect('members-changed', + self.__members_changed_cb) + get_adhoc_manager_instance().connect('state-changed', + self.__state_changed_cb) + + self.connect('button-release-event', self.__button_release_event_cb) + + pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.pulse_color = pulse_color + self._state_color = XoColor('%s,%s' % \ + (profile.get_color().get_stroke_color(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.base_color = self._state_color + self._palette = self._create_palette() + self.set_palette(self._palette) + self._palette_icon.props.xo_color = self._state_color + + def _create_palette(self): + self._palette_icon = Icon( \ + icon_name=self._ICON_NAME + str(self._channel), + icon_size=style.STANDARD_ICON_SIZE) + + palette_ = palette.Palette(_('Ad-hoc Network %d') % self._channel, + icon=self._palette_icon) + + self._connect_item = MenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('activate', self.__connect_activate_cb) + palette_.menu.append(self._connect_item) + + self._disconnect_item = MenuItem(_('Disconnect'), 'media-eject') + self._disconnect_item.connect('activate', + self.__disconnect_activate_cb) + palette_.menu.append(self._disconnect_item) + + return palette_ + + def __button_release_event_cb(self, icon, event): + get_adhoc_manager_instance().activate_channel(self._channel) + + def __connect_activate_cb(self, icon): + get_adhoc_manager_instance().activate_channel(self._channel) + + def __disconnect_activate_cb(self, icon): + get_adhoc_manager_instance().deactivate_active_channel() + + def __state_changed_cb(self, adhoc_manager, channel, device_state): + if self._channel == channel: + state = device_state + else: + state = network.DEVICE_STATE_UNKNOWN + + if state == network.DEVICE_STATE_ACTIVATED: + icon_name = '%s-connected' % (self._ICON_NAME + str(self._channel)) + else: + icon_name = self._ICON_NAME + str(self._channel) + + if icon_name is not None: + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + + if state in [network.DEVICE_STATE_PREPARE, + network.DEVICE_STATE_CONFIG, + network.DEVICE_STATE_NEED_AUTH, + network.DEVICE_STATE_IP_CONFIG]: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connecting...') + self.props.pulsing = True + elif state == network.DEVICE_STATE_ACTIVATED: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connected') + self.props.pulsing = False + else: + if self._disconnect_item: + self._disconnect_item.hide() + self._connect_item.show() + self._palette.props.secondary_text = None + self.props.pulsing = False + + def _update_color(self): + if self._greyed_out: + self.props.pulsing = False + self.props.base_color = XoColor('#D5D5D5,#D5D5D5') + else: + self.props.base_color = self._state_color + + def __members_changed_cb(self, adhoc_manager, channel, has_members): + if channel == self._channel: + if has_members == True: + self._state_color = profile.get_color() + else: + color = '%s,%s' % (profile.get_color().get_stroke_color(), + style.COLOR_TRANSPARENT.get_svg()) + self._state_color = XoColor(color) + + if not self._greyed_out: + self.props.base_color = self._state_color + self._palette_icon.props.xo_color = self._state_color + + def set_filter(self, query): + name = self._NAME + str(self._channel) + self._greyed_out = name.lower().find(query) == -1 + self._update_color() + + +class OlpcMeshView(CanvasPulsingIcon): + def __init__(self, mesh_mgr, channel): + CanvasPulsingIcon.__init__(self, icon_name=_OLPC_MESH_ICON_NAME, + size=style.STANDARD_ICON_SIZE, cache=True) + self._bus = dbus.SystemBus() + self._channel = channel + self._mesh_mgr = mesh_mgr + self._disconnect_item = None + self._connect_item = None + self._greyed_out = False + self._name = '' + self._device_state = None + self._active = False + device = mesh_mgr.mesh_device + + self.connect('button-release-event', self.__button_release_event_cb) + + interface_props = dbus.Interface(device, dbus.PROPERTIES_IFACE) + interface_props.Get(_NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_device_state_reply_cb, + error_handler=self.__get_device_state_error_cb) + interface_props.Get(_NM_OLPC_MESH_IFACE, 'ActiveChannel', + reply_handler=self.__get_active_channel_reply_cb, + error_handler=self.__get_active_channel_error_cb) + + self._bus.add_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=device.object_path, + dbus_interface=_NM_OLPC_MESH_IFACE) + + pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.pulse_color = pulse_color + self.props.base_color = profile.get_color() + self._palette = self._create_palette() + self.set_palette(self._palette) + + def _create_palette(self): + _palette = palette.Palette(_('Mesh Network %d') % self._channel) + + self._connect_item = MenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('activate', self.__connect_activate_cb) + _palette.menu.append(self._connect_item) + + return _palette + + def __get_device_state_reply_cb(self, state): + self._device_state = state + self._update() + + def __get_device_state_error_cb(self, err): + logging.error('Error getting the device state: %s', err) + + def __device_state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update() + + def __get_active_channel_reply_cb(self, channel): + self._active = (channel == self._channel) + self._update() + + def __get_active_channel_error_cb(self, err): + logging.error('Error getting the active channel: %s', err) + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveChannel' in properties: + channel = properties['ActiveChannel'] + self._active = (channel == self._channel) + self._update() + + def _update(self): + if self._active: + state = self._device_state + else: + state = network.DEVICE_STATE_UNKNOWN + + if state in [network.DEVICE_STATE_PREPARE, + network.DEVICE_STATE_CONFIG, + network.DEVICE_STATE_NEED_AUTH, + network.DEVICE_STATE_IP_CONFIG]: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connecting...') + self.props.pulsing = True + elif state == network.DEVICE_STATE_ACTIVATED: + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connected') + self.props.pulsing = False + else: + if self._disconnect_item: + self._disconnect_item.hide() + self._connect_item.show() + self._palette.props.secondary_text = None + self.props.pulsing = False + + def _update_color(self): + if self._greyed_out: + self.props.base_color = XoColor('#D5D5D5,#D5D5D5') + else: + self.props.base_color = profile.get_color() + + def __connect_activate_cb(self, icon): + self._connect() + + def __button_release_event_cb(self, icon, event): + self._connect() + + def _connect(self): + self._mesh_mgr.user_activate_channel(self._channel) + + def __activate_reply_cb(self, connection): + logging.debug('Connection activated: %s', connection) + + def __activate_error_cb(self, err): + logging.error('Failed to activate connection: %s', err) + + def set_filter(self, query): + self._greyed_out = (query != '') + self._update_color() + + def disconnect(self): + device_object_path = self._mesh_mgr.mesh_device.object_path + + self._bus.remove_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=device_object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=device_object_path, + dbus_interface=_NM_OLPC_MESH_IFACE) diff --git a/src/jarabe/desktop/schoolserver.py b/src/jarabe/desktop/schoolserver.py index fc9ddeb..aea2357 100644 --- a/src/jarabe/desktop/schoolserver.py +++ b/src/jarabe/desktop/schoolserver.py @@ -16,8 +16,9 @@ import logging from gettext import gettext as _ -from xmlrpclib import ServerProxy, Error +import xmlrpclib import socket +import httplib import os from string import ascii_uppercase import random @@ -29,15 +30,17 @@ import gconf from sugar import env from sugar.profile import get_profile -REGISTER_URL = 'http://schoolserver:8080/' +_REGISTER_URL = 'http://schoolserver:8080/' +_REGISTER_TIMEOUT = 8 -def generate_serial_number(): + +def _generate_serial_number(): """ Generates a serial number based on 3 random uppercase letters and the last 8 digits of the current unix seconds. """ serial_part1 = [] - for y_ in range(3) : + for y_ in range(3): serial_part1.append(random.choice(ascii_uppercase)) serial_part1 = ''.join(serial_part1) @@ -46,7 +49,8 @@ def generate_serial_number(): return serial -def store_identifiers(serial_number, uuid, backup_url): + +def _store_identifiers(serial_number, uuid_, backup_url): """ Stores the serial number, uuid and backup_url in the identifier folder inside the profile directory so that these identifiers can be used for backup. """ @@ -64,7 +68,7 @@ def store_identifiers(serial_number, uuid, backup_url): if os.path.exists(os.path.join(identifier_path, 'uuid')): os.remove(os.path.join(identifier_path, 'uuid')) uuid_file = open(os.path.join(identifier_path, 'uuid'), 'w') - uuid_file.write(uuid) + uuid_file.write(uuid_) uuid_file.close() if os.path.exists(os.path.join(identifier_path, 'backup_url')): @@ -73,33 +77,56 @@ def store_identifiers(serial_number, uuid, backup_url): backup_url_file.write(backup_url) backup_url_file.close() + class RegisterError(Exception): pass -def register_laptop(url=REGISTER_URL): + +class _TimeoutHTTP(httplib.HTTP): + + def __init__(self, host='', port=None, strict=None, timeout=None): + if port == 0: + port = None + # FIXME: Depending on undocumented internals that can break between + # Python releases. Please have a look at SL #2350 + self._setup(self._connection_class(host, + port, strict, timeout=_REGISTER_TIMEOUT)) + + +class _TimeoutTransport(xmlrpclib.Transport): + + def make_connection(self, host): + host, extra_headers, x509_ = self.get_host_info(host) + return _TimeoutHTTP(host, timeout=_REGISTER_TIMEOUT) + + +def register_laptop(url=_REGISTER_URL): profile = get_profile() client = gconf.client_get_default() - if have_ofw_tree(): - sn = read_ofw('mfg-data/SN') - uuid_ = read_ofw('mfg-data/U#') + if _have_ofw_tree(): + sn = _read_ofw('mfg-data/SN') + uuid_ = _read_ofw('mfg-data/U#') sn = sn or 'SHF00000000' uuid_ = uuid_ or '00000000-0000-0000-0000-000000000000' else: - sn = generate_serial_number() + sn = _generate_serial_number() uuid_ = str(uuid.uuid1()) - setting_name = '/desktop/sugar/collaboration/jabber_server' - jabber_server = client.get_string(setting_name) - store_identifiers(sn, uuid_, jabber_server) + + setting_name = '/desktop/sugar/collaboration/jabber_server' + jabber_server = client.get_string(setting_name) + _store_identifiers(sn, uuid_, jabber_server) + + if jabber_server: url = 'http://' + jabber_server + ':8080/' nick = client.get_string('/desktop/sugar/user/nick') - server = ServerProxy(url) + server = xmlrpclib.ServerProxy(url, _TimeoutTransport()) try: data = server.register(sn, nick, uuid_, profile.pubkey) - except (Error, socket.error): + except (xmlrpclib.Error, TypeError, socket.error): logging.exception('Registration: cannot connect to server') raise RegisterError(_('Cannot connect to the server.')) @@ -114,10 +141,12 @@ def register_laptop(url=REGISTER_URL): return True -def have_ofw_tree(): + +def _have_ofw_tree(): return os.path.exists('/ofw') -def read_ofw(path): + +def _read_ofw(path): path = os.path.join('/ofw', path) if not os.path.exists(path): return None diff --git a/src/jarabe/desktop/snowflakelayout.py b/src/jarabe/desktop/snowflakelayout.py index 5782cff..e4963ba 100644 --- a/src/jarabe/desktop/snowflakelayout.py +++ b/src/jarabe/desktop/snowflakelayout.py @@ -21,11 +21,14 @@ 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 diff --git a/src/jarabe/desktop/spreadlayout.py b/src/jarabe/desktop/spreadlayout.py index ffc5bc7..9200361 100644 --- a/src/jarabe/desktop/spreadlayout.py +++ b/src/jarabe/desktop/spreadlayout.py @@ -22,10 +22,13 @@ from sugar.graphics import style from jarabe.desktop.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 @@ -80,4 +83,3 @@ class SpreadLayout(gobject.GObject, hippo.CanvasLayout): def _grid_child_changed_cb(self, grid, child): child.emit_request_changed() - diff --git a/src/jarabe/desktop/transitionbox.py b/src/jarabe/desktop/transitionbox.py index af17cfb..4042044 100644 --- a/src/jarabe/desktop/transitionbox.py +++ b/src/jarabe/desktop/transitionbox.py @@ -20,7 +20,9 @@ import gobject from sugar.graphics import style from sugar.graphics import animator -from jarabe.desktop.myicon import MyIcon +from jarabe.model.buddy import get_owner_instance +from jarabe.view.buddyicon import BuddyIcon + class _Animation(animator.Animation): def __init__(self, icon, start_size, end_size): @@ -34,8 +36,10 @@ class _Animation(animator.Animation): 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 @@ -59,12 +63,12 @@ class _Layout(gobject.GObject, hippo.CanvasLayout): 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, ([])) + 'completed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } def __init__(self): @@ -77,7 +81,8 @@ class TransitionBox(hippo.Canvas): self._layout = _Layout() self._box.set_layout(self._layout) - self._my_icon = MyIcon(style.XLARGE_ICON_SIZE) + self._my_icon = BuddyIcon(buddy=get_owner_instance(), + size=style.XLARGE_ICON_SIZE) self._box.append(self._my_icon) self._animator = animator.Animator(0.3) diff --git a/src/jarabe/frame/__init__.py b/src/jarabe/frame/__init__.py index d7aec3d..b3e4b80 100644 --- a/src/jarabe/frame/__init__.py +++ b/src/jarabe/frame/__init__.py @@ -16,8 +16,10 @@ from jarabe.frame.frame import Frame + _view = None + def get_view(): global _view if not _view: diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py index b5762ee..6e08fc0 100644 --- a/src/jarabe/frame/activitiestray.py +++ b/src/jarabe/frame/activitiestray.py @@ -1,5 +1,6 @@ # Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> # # 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 @@ -33,20 +34,16 @@ from sugar.graphics.toolbutton import ToolButton from sugar.graphics.icon import Icon, get_icon_file_name from sugar.graphics.palette import Palette, WidgetInvoker from sugar.graphics.menuitem import MenuItem -from sugar.activity.activityhandle import ActivityHandle -from sugar.activity import activityfactory from sugar.datastore import datastore from sugar import mime from sugar import env from jarabe.model import shell -from jarabe.model import neighborhood -from jarabe.model import owner +from jarabe.model import invites from jarabe.model import bundleregistry from jarabe.model import filetransfer from jarabe.view.palettes import JournalPalette, CurrentActivityPalette from jarabe.view.pulsingicon import PulsingIcon -from jarabe.view import launcher from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.frame.notification import NotificationIcon import jarabe.frame @@ -59,6 +56,7 @@ class ActivityButton(RadioToolButton): self.set_palette_invoker(FrameWidgetInvoker(self)) self._home_activity = home_activity + self._notify_launch_hid = None self._icon = PulsingIcon() self._icon.props.base_color = home_activity.get_icon_color() @@ -72,13 +70,12 @@ class ActivityButton(RadioToolButton): self.set_icon_widget(self._icon) self._icon.show() - if home_activity.props.launching: + if home_activity.props.launch_status == shell.Activity.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 + self._notify_launch_hid = home_activity.connect( \ + 'notify::launch-status', self.__notify_launch_status_cb) + elif home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() def create_palette(self): if self._home_activity.is_journal(): @@ -88,26 +85,64 @@ class ActivityButton(RadioToolButton): palette.set_group_id('frame') self.set_palette(palette) - def __notify_launching_cb(self, home_activity, pspec): - if not home_activity.props.launching: + def _on_failed_launch(self): + # TODO http://bugs.sugarlabs.org/ticket/2007 + pass + + def __notify_launch_status_cb(self, home_activity, pspec): + home_activity.disconnect(self._notify_launch_hid) + self._notify_launch_hid = None + if home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() + else: self._icon.props.pulsing = False - home_activity.disconnect(self._notify_launching_hid) -class BaseInviteButton(ToolButton): + +class InviteButton(ToolButton): + """Invite to shared activity""" def __init__(self, invite): ToolButton.__init__(self) + self._invite = invite + self.connect('clicked', self.__clicked_cb) + self.connect('destroy', self.__destroy_cb) + + bundle_registry = bundleregistry.get_registry() + bundle = bundle_registry.get_bundle(invite.get_bundle_id()) + self._icon = Icon() + self._icon.props.xo_color = invite.get_color() + if bundle is not None: + self._icon.props.file = bundle.get_icon() + else: + self._icon.props.icon_name = 'image-missing' self.set_icon_widget(self._icon) self._icon.show() - self.connect('clicked', self.__clicked_cb) - self.connect('destroy', self.__destroy_cb) + palette = InvitePalette(invite) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + self.set_palette(palette) + self._notif_icon = NotificationIcon() self._notif_icon.connect('button-release-event', self.__button_release_event_cb) + self._notif_icon.props.xo_color = invite.get_color() + if bundle is not None: + self._notif_icon.props.icon_filename = bundle.get_icon() + else: + self._notif_icon.props.icon_name = 'image-missing' + + palette = InvitePalette(invite) + palette.props.invoker = WidgetInvoker(self._notif_icon) + palette.set_group_id('frame') + self._notif_icon.palette = palette + + frame = jarabe.frame.get_view() + frame.add_notification(self._notif_icon, gtk.CORNER_TOP_LEFT) + def __button_release_event_cb(self, icon, event): self.emit('clicked') @@ -118,118 +153,23 @@ class BaseInviteButton(ToolButton): self._notif_icon = None self._launch() - def _launch(self): - """Launch the target of the invite""" - raise NotImplementedError - def __destroy_cb(self, button): frame = jarabe.frame.get_view() frame.remove_notification(self._notif_icon) -class ActivityInviteButton(BaseInviteButton): - """Invite to shared activity""" - def __init__(self, invite): - BaseInviteButton.__init__(self, invite) - mesh = neighborhood.get_model() - 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' - - 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 = jarabe.frame.get_view() - frame.add_notification(self._notif_icon, - gtk.CORNER_TOP_LEFT) - def _launch(self): """Join the activity in the invite.""" + self._invite.join() - shell_model = shell.get_model() - activity = shell_model.get_activity_by_id(self._activity_model.get_id()) - if activity: - activity.get_window().activate(gtk.get_current_event_time()) - return - - registry = bundleregistry.get_registry() - bundle = registry.get_bundle(self._bundle_id) - - launcher.add_launcher(self._activity_model.get_id(), - bundle.get_icon(), - self._activity_model.get_color()) - handle = ActivityHandle(self._activity_model.get_id()) - activityfactory.create(bundle, handle) +class InvitePalette(Palette): + """Palette for frame or notification icon for invites.""" -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() - - client = gconf.client_get_default() - color = XoColor(client.get_string('/desktop/sugar/user/color')) - - self._icon.props.xo_color = color - registry = bundleregistry.get_registry() - self._bundle = registry.get_bundle(self._bundle_id) - - if self._bundle: - self._icon.props.file = self._bundle.get_icon() - else: - self._icon.props.icon_name = 'image-missing' - - palette = PrivateInvitePalette(invite) - palette.props.invoker = FrameWidgetInvoker(self) - palette.set_group_id('frame') - self.set_palette(palette) - - self._notif_icon.props.xo_color = color - - if self._bundle: - self._notif_icon.props.icon_filename = self._bundle.get_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 = jarabe.frame.get_view() - frame.add_notification(self._notif_icon, - gtk.CORNER_TOP_LEFT) - - def _launch(self): - """Start the activity with private channel.""" - activityfactory.create_with_uri(self._bundle, self._private_channel) - -class BaseInvitePalette(Palette): - """Palette for frame or notification icon for invites.""" - def __init__(self): Palette.__init__(self, '') + self._invite = invite + menu_item = MenuItem(_('Join'), icon_name='dialog-ok') menu_item.connect('activate', self.__join_activate_cb) self.menu.append(menu_item) @@ -240,72 +180,22 @@ class BaseInvitePalette(Palette): 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 = neighborhood.get_model() - activity_model = mesh.get_activity(invite.get_activity_id()) - self._activity_model = activity_model - self._bundle_id = activity_model.get_bundle_id() + bundle_id = invite.get_bundle_id() registry = bundleregistry.get_registry() - self._bundle = registry.get_bundle(self._bundle_id) + self._bundle = registry.get_bundle(bundle_id) if self._bundle: self.set_primary_text(self._bundle.get_name()) else: - self.set_primary_text(self._bundle_id) + self.set_primary_text(bundle_id) - def _join(self): - handle = ActivityHandle(self._activity_model.get_id()) - activityfactory.create(self._bundle, handle) + def __join_activate_cb(self, menu_item): + self._invite.join() - def _decline(self): - invites = owner.get_model().get_invites() + def __decline_activate_cb(self, menu_item): + invites_model = invites.get_instance() 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 = bundleregistry.get_registry() - self._bundle = registry.get_bundle(self._bundle_id) - if self._bundle: - self.set_primary_text(self._bundle.get_name()) - else: - self.set_primary_text(self._bundle_id) - - def _join(self): - activityfactory.create_with_uri(self._bundle, self._private_channel) - - invites = owner.get_model().get_invites() - invites.remove_private_channel(self._private_channel) - - def _decline(self): - invites = owner.get_model().get_invites() - invites.remove_private_channel(self._private_channel) + invites_model.remove_activity(activity_id) class ActivitiesTray(HTray): @@ -318,13 +208,14 @@ class ActivitiesTray(HTray): self._home_model = shell.get_model() self._home_model.connect('activity-added', self.__activity_added_cb) - self._home_model.connect('activity-removed', self.__activity_removed_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 = owner.get_model().get_invites() + self._invites = invites.get_instance() for invite in self._invites: self._add_invite(invite) self._invites.connect('invite-added', self.__invite_added_cb) @@ -388,32 +279,22 @@ class ActivitiesTray(HTray): 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) + self._invites.remove_invite(invite) - def __invite_added_cb(self, invites, invite): + def __invite_added_cb(self, invites_model, invite): self._add_invite(invite) - def __invite_removed_cb(self, invites, invite): + def __invite_removed_cb(self, invites_model, 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 = neighborhood.get_model() - 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 + """Add an invite""" + item = InviteButton(invite) + 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]) @@ -432,6 +313,7 @@ class ActivitiesTray(HTray): self.add_item(button) button.show() + class BaseTransferButton(ToolButton): """Button with a notification attached """ @@ -468,6 +350,7 @@ class BaseTransferButton(ToolButton): filetransfer.FT_REASON_LOCAL_STOPPED: self.remove() + class IncomingTransferButton(BaseTransferButton): """UI element representing an ongoing incoming file transfer """ @@ -490,7 +373,7 @@ class IncomingTransferButton(BaseTransferButton): self.notif_icon.props.icon_name = icon_name break - icon_color = XoColor(file_transfer.buddy.props.color) + icon_color = file_transfer.buddy.props.color self.props.icon_widget.props.xo_color = icon_color self.notif_icon.props.xo_color = icon_color @@ -515,18 +398,18 @@ class IncomingTransferButton(BaseTransferButton): self._ds_object.metadata['buddies'] = '' self._ds_object.metadata['preview'] = '' self._ds_object.metadata['icon-color'] = \ - file_transfer.buddy.props.color + file_transfer.buddy.props.color.to_string() self._ds_object.metadata['mime_type'] = file_transfer.mime_type elif file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: logging.debug('__notify_state_cb COMPLETED') self._ds_object.metadata['progress'] = '100' self._ds_object.file_path = file_transfer.destination_path - datastore.write(self._ds_jobject, transfer_ownership=True, + datastore.write(self._ds_object, transfer_ownership=True, reply_handler=self.__reply_handler_cb, error_handler=self.__error_handler_cb) elif file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: logging.debug('__notify_state_cb CANCELLED') - object_id = self._jobject.object_id + object_id = self._ds_object.object_id if object_id is not None: self._ds_object.destroy() datastore.delete(object_id) @@ -536,17 +419,19 @@ class IncomingTransferButton(BaseTransferButton): progress = file_transfer.props.transferred_bytes / \ file_transfer.file_size self._ds_object.metadata['progress'] = str(progress * 100) - datastore.write(self._ds_object.object_id, update_mtime=False) + datastore.write(self._ds_object, update_mtime=False) def __reply_handler_cb(self): - logging.debug('__reply_handler_cb %r', self._object_id) + logging.debug('__reply_handler_cb %r', self._ds_object.object_id) def __error_handler_cb(self, error): - logging.debug('__error_handler_cb %r %s', self._object_id, error) + logging.debug('__error_handler_cb %r %s', self._ds_object.object_id, + error) def __dismiss_clicked_cb(self, palette): self.remove() + class OutgoingTransferButton(BaseTransferButton): """UI element representing an ongoing outgoing file transfer """ @@ -564,7 +449,7 @@ class OutgoingTransferButton(BaseTransferButton): break client = gconf.client_get_default() - icon_color = XoColor(client.get_string("/desktop/sugar/user/color")) + icon_color = XoColor(client.get_string('/desktop/sugar/user/color')) self.props.icon_widget.props.xo_color = icon_color self.notif_icon.props.xo_color = icon_color @@ -582,14 +467,14 @@ class OutgoingTransferButton(BaseTransferButton): def __dismiss_clicked_cb(self, palette): self.remove() + class BaseTransferPalette(Palette): """Base palette class for frame or notification icon for file transfers """ - __gtype_name__ = "SugarBaseTransferPalette" + __gtype_name__ = 'SugarBaseTransferPalette' __gsignals__ = { - 'dismiss-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([])), + 'dismiss-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } def __init__(self, file_transfer): @@ -644,10 +529,12 @@ class BaseTransferPalette(Palette): total = self._format_size(self.file_transfer.file_size) self.progress_label.props.label = _('%s of %s') % (transferred, total) + class IncomingTransferPalette(BaseTransferPalette): """Palette for frame or notification icon for incoming file transfers """ - __gtype_name__ = "SugarIncomingTransferPalette" + __gtype_name__ = 'SugarIncomingTransferPalette' + def __init__(self, file_transfer): BaseTransferPalette.__init__(self, file_transfer) @@ -770,10 +657,11 @@ class IncomingTransferPalette(BaseTransferPalette): def __dismiss_activate_cb(self, menu_item): self.emit('dismiss-clicked') + class OutgoingTransferPalette(BaseTransferPalette): """Palette for frame or notification icon for outgoing file transfers """ - __gtype_name__ = "SugarOutgoingTransferPalette" + __gtype_name__ = 'SugarOutgoingTransferPalette' def __init__(self, file_transfer): BaseTransferPalette.__init__(self, file_transfer) diff --git a/src/jarabe/frame/clipboard.py b/src/jarabe/frame/clipboard.py index 3b9f745..be2b902 100644 --- a/src/jarabe/frame/clipboard.py +++ b/src/jarabe/frame/clipboard.py @@ -26,6 +26,10 @@ from sugar import mime from jarabe.frame.clipboardobject import ClipboardObject, Format + +_instance = None + + class Clipboard(gobject.GObject): __gsignals__ = { @@ -34,7 +38,7 @@ class Clipboard(gobject.GObject): 'object-deleted': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([int])), 'object-state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([object])) + ([object])), } def __init__(self): @@ -69,7 +73,7 @@ class Clipboard(gobject.GObject): + ' 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 + '.') + logging.debug('Added in-memory format of type %s.', format_type) self.emit('object-state-changed', cb_object) @@ -82,9 +86,9 @@ class Clipboard(gobject.GObject): def set_object_percent(self, object_id, percent): cb_object = self._objects[object_id] if percent < 0 or percent > 100: - raise ValueError("invalid percentage") + raise ValueError('invalid percentage') if cb_object.get_percent() > percent: - raise ValueError("invalid percentage; less than current percent") + raise ValueError('invalid percentage; less than current percent') if cb_object.get_percent() == percent: # ignore setting same percentage return @@ -126,21 +130,21 @@ class Clipboard(gobject.GObject): def _copy_file(self, original_uri): uri = urlparse.urlparse(original_uri) - path_, file_name = os.path.split(uri.path) + path = uri.path # pylint: disable=E1101 + directory_, file_name = os.path.split(path) root, ext = os.path.splitext(file_name) if not ext or ext == '.': - mime_type = mime.get_for_file(uri.path) + mime_type = mime.get_for_file(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) + shutil.copyfile(path, new_file_path) os.chmod(new_file_path, 0644) return 'file://' + new_file_path -_instance = None def get_instance(): global _instance diff --git a/src/jarabe/frame/clipboardicon.py b/src/jarabe/frame/clipboardicon.py index 279db08..aa72d8a 100644 --- a/src/jarabe/frame/clipboardicon.py +++ b/src/jarabe/frame/clipboardicon.py @@ -31,6 +31,7 @@ from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.frame.notification import NotificationIcon import jarabe.frame + class ClipboardIcon(RadioToolButton): __gtype_name__ = 'SugarClipboardIcon' @@ -71,7 +72,8 @@ class ClipboardIcon(RadioToolButton): def _drag_data_get_cb(self, widget, context, selection, target_type, event_time): - logging.debug('_drag_data_get_cb: requested target ' + selection.target) + logging.debug('_drag_data_get_cb: requested target %s', + selection.target) data = self._cb_object.get_formats()[selection.target].get_data() selection.set(selection.target, 8, data) @@ -79,8 +81,8 @@ class ClipboardIcon(RadioToolButton): 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.') + raise ValueError('Object is not complete, cannot be put into the' + ' clipboard.') targets = self._get_targets() if targets: diff --git a/src/jarabe/frame/clipboardmenu.py b/src/jarabe/frame/clipboardmenu.py index b998110..d11538d 100644 --- a/src/jarabe/frame/clipboardmenu.py +++ b/src/jarabe/frame/clipboardmenu.py @@ -35,6 +35,7 @@ from jarabe.frame import clipboard from jarabe.journal import misc from jarabe.model import bundleregistry + class ClipboardMenu(Palette): def __init__(self, cb_object): @@ -212,7 +213,8 @@ class ClipboardMenu(Palette): 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 + parsed_url = urlparse.urlparse(uris[0]) + file_path = parsed_url.path # pylint: disable=E1101 transfer_ownership = False mime_type = mime.get_for_file(file_path) else: @@ -221,7 +223,8 @@ class ClipboardMenu(Palette): mime_type = 'text/uri-list' else: if format_.is_on_disk(): - file_path = urlparse.urlparse(format_.get_data()).path + parsed_url = urlparse.urlparse(format_.get_data()) + file_path = parsed_url.path # pylint: disable=E1101 transfer_ownership = False mime_type = mime.get_for_file(file_path) else: diff --git a/src/jarabe/frame/clipboardobject.py b/src/jarabe/frame/clipboardobject.py index e9403f9..407af2f 100644 --- a/src/jarabe/frame/clipboardobject.py +++ b/src/jarabe/frame/clipboardobject.py @@ -24,6 +24,7 @@ from gettext import gettext as _ from sugar import mime from sugar.bundle.activitybundle import ActivityBundle + class ClipboardObject(object): def __init__(self, object_path, name): @@ -105,15 +106,18 @@ class ClipboardObject(object): 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) + scheme = uri.scheme # pylint: disable=E1101 + if scheme == 'file': + path = uri.path # pylint: disable=E1101 + if os.path.exists(path): + format_ = mime.get_for_file(path) else: - format_ = mime.get_from_file_name(uri.path) + format_ = mime.get_from_file_name(path) logging.debug('Chose %r!', format_) return format_ + class Format(object): def __init__(self, mime_type, data, on_disk): @@ -126,8 +130,9 @@ class Format(object): def destroy(self): if self._on_disk: uri = urlparse.urlparse(self._data) - if os.path.isfile(uri.path): - os.remove(uri.path) + path = uri.path # pylint: disable=E1101 + if os.path.isfile(path): + os.remove(path) def get_type(self): return self._type diff --git a/src/jarabe/frame/clipboardpanelwindow.py b/src/jarabe/frame/clipboardpanelwindow.py index ac324f4..f5d537c 100644 --- a/src/jarabe/frame/clipboardpanelwindow.py +++ b/src/jarabe/frame/clipboardpanelwindow.py @@ -25,6 +25,7 @@ from jarabe.frame.clipboardtray import ClipboardTray from jarabe.frame import clipboard + class ClipboardPanelWindow(FrameWindow): def __init__(self, frame, orientation): FrameWindow.__init__(self, orientation) @@ -35,7 +36,7 @@ class ClipboardPanelWindow(FrameWindow): # 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.connect('owner-change', self._owner_change_cb) self._clipboard_tray = ClipboardTray() canvas_widget = hippo.CanvasWidget(widget=self._clipboard_tray) @@ -43,14 +44,14 @@ class ClipboardPanelWindow(FrameWindow): # Receiving dnd drops self.drag_dest_set(0, [], 0) - self.connect("drag_motion", self._clipboard_tray.drag_motion_cb) - self.connect("drag_leave", self._clipboard_tray.drag_leave_cb) - self.connect("drag_drop", self._clipboard_tray.drag_drop_cb) - self.connect("drag_data_received", + self.connect('drag_motion', self._clipboard_tray.drag_motion_cb) + self.connect('drag_leave', self._clipboard_tray.drag_leave_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") + logging.debug('owner_change_cb') if self._clipboard_tray.owns_clipboard(): return @@ -100,4 +101,3 @@ class ClipboardPanelWindow(FrameWindow): selection.type, selection.data, on_disk=False) - diff --git a/src/jarabe/frame/clipboardtray.py b/src/jarabe/frame/clipboardtray.py index 8beb6a8..f49b799 100644 --- a/src/jarabe/frame/clipboardtray.py +++ b/src/jarabe/frame/clipboardtray.py @@ -27,6 +27,7 @@ from sugar.graphics import style from jarabe.frame import clipboard from jarabe.frame.clipboardicon import ClipboardIcon + class _ContextMap(object): """Maps a drag context to the clipboard object involved in the dragging.""" def __init__(self): @@ -40,8 +41,8 @@ class _ContextMap(object): 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. + 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] @@ -56,6 +57,7 @@ class _ContextMap(object): 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 @@ -154,7 +156,7 @@ class ClipboardTray(tray.VTray): if 'XdndDirectSave0' in context.targets: window = context.source_window prop_type, format_, filename = \ - window.property_get('XdndDirectSave0','text/plain') + window.property_get('XdndDirectSave0', 'text/plain') # FIXME query the clipboard service for a filename? base_dir = tempfile.gettempdir() @@ -192,12 +194,13 @@ class ClipboardTray(tray.VTray): if selection.data == 'S': window = context.source_window - prop_type, format_, dest = \ - window.property_get('XdndDirectSave0', 'text/plain') + 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) + clipboardservice.add_object_format(object_id, + 'XdndDirectSave0', + dest, on_disk=True) else: self._add_selection(object_id, selection) @@ -213,4 +216,3 @@ class ClipboardTray(tray.VTray): return True else: return False - diff --git a/src/jarabe/frame/devicestray.py b/src/jarabe/frame/devicestray.py index 72affe3..c5db639 100644 --- a/src/jarabe/frame/devicestray.py +++ b/src/jarabe/frame/devicestray.py @@ -15,14 +15,13 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os -import sys -import traceback import logging from sugar.graphics import tray from jarabe import config + class DevicesTray(tray.HTray): def __init__(self): tray.HTray.__init__(self, align=tray.ALIGN_TO_END) @@ -35,14 +34,14 @@ class DevicesTray(tray.HTray): locals(), [module_name]) mod.setup(self) except Exception: - logging.error('Exception while loading extension:\n' + \ - ''.join(traceback.format_exception(*sys.exc_info()))) + logging.exception('Exception while loading extension:') def add_device(self, view): index = 0 - relative_index = getattr(view, "FRAME_POSITION_RELATIVE", -1) + relative_index = getattr(view, 'FRAME_POSITION_RELATIVE', -1) for item in self.get_children(): - current_relative_index = getattr(item, "FRAME_POSITION_RELATIVE", 0) + current_relative_index = getattr(item, 'FRAME_POSITION_RELATIVE', + 0) if current_relative_index >= relative_index: index += 1 else: diff --git a/src/jarabe/frame/eventarea.py b/src/jarabe/frame/eventarea.py index 166aaf5..1b5bf86 100644 --- a/src/jarabe/frame/eventarea.py +++ b/src/jarabe/frame/eventarea.py @@ -19,14 +19,14 @@ import gobject import wnck import gconf + _MAX_DELAY = 1000 + class EventArea(gobject.GObject): __gsignals__ = { - 'enter': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([])), - 'leave': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([])) + 'enter': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'leave': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } def __init__(self): @@ -37,10 +37,11 @@ class EventArea(gobject.GObject): self._sids = {} client = gconf.client_get_default() self._edge_delay = client.get_int('/desktop/sugar/frame/edge_delay') - self._corner_delay = client.get_int('/desktop/sugar/frame/corner_delay') + self._corner_delay = client.get_int('/desktop/sugar/frame' + '/corner_delay') right = gtk.gdk.screen_width() - 1 - bottom = gtk.gdk.screen_height() -1 + bottom = gtk.gdk.screen_height() - 1 width = gtk.gdk.screen_width() - 2 height = gtk.gdk.screen_height() - 2 @@ -94,6 +95,7 @@ class EventArea(gobject.GObject): invisible.connect('drag_leave', self._drag_leave_cb) invisible.realize() + # pylint: disable=E1101 invisible.window.set_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK) diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py index 55f866f..079eeeb 100644 --- a/src/jarabe/frame/frame.py +++ b/src/jarabe/frame/frame.py @@ -35,6 +35,7 @@ from jarabe.frame.clipboardpanelwindow import ClipboardPanelWindow from jarabe.frame.notification import NotificationIcon, NotificationWindow from jarabe.model import notifications + TOP_RIGHT = 0 TOP_LEFT = 1 BOTTOM_RIGHT = 2 @@ -43,6 +44,7 @@ BOTTOM_LEFT = 3 _FRAME_HIDING_DELAY = 500 _NOTIFICATION_DURATION = 5000 + class _Animation(animator.Animation): def __init__(self, frame, end): start = frame.current_position @@ -52,6 +54,7 @@ class _Animation(animator.Animation): def next_frame(self, current): self._frame.move(current) + class _MouseListener(object): def __init__(self, frame): self._frame = frame @@ -79,6 +82,7 @@ class _MouseListener(object): self._hide_sid = gobject.timeout_add( _FRAME_HIDING_DELAY, self._hide_frame_timeout_cb) + class _KeyListener(object): def __init__(self, frame): self._frame = frame @@ -90,13 +94,14 @@ class _KeyListener(object): else: self._frame.show(Frame.MODE_KEYBOARD) + class Frame(object): - MODE_MOUSE = 0 + MODE_MOUSE = 0 MODE_KEYBOARD = 1 MODE_NON_INTERACTIVE = 2 def __init__(self): - logging.debug("STARTUP: Loading the frame") + logging.debug('STARTUP: Loading the frame') self.mode = None self._palette_group = palettegroup.get_group('frame') @@ -173,12 +178,12 @@ class Frame(object): 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? + # 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)) + box_width=4 * style.GRID_CELL_SIZE)) zoom_toolbar.show() activities_tray = ActivitiesTray() @@ -193,7 +198,8 @@ class Frame(object): # TODO: same issue as in _create_top_panel() devices_tray = DevicesTray() - panel.append(hippo.CanvasWidget(widget=devices_tray), hippo.PACK_EXPAND) + panel.append(hippo.CanvasWidget(widget=devices_tray), + hippo.PACK_EXPAND) devices_tray.show() return panel @@ -322,7 +328,7 @@ class Frame(object): del self._notif_by_icon[icon] def __notification_received_cb(self, **kwargs): - logging.debug('__notification_received_cb %r', kwargs) + logging.debug('__notification_received_cb') icon = NotificationIcon() hints = kwargs['hints'] @@ -348,4 +354,3 @@ class Frame(object): # Do nothing for now. Our notification UI is so simple, there's no # point yet. pass - diff --git a/src/jarabe/frame/frameinvoker.py b/src/jarabe/frame/frameinvoker.py index e4a13e1..a4abfa8 100644 --- a/src/jarabe/frame/frameinvoker.py +++ b/src/jarabe/frame/frameinvoker.py @@ -19,6 +19,7 @@ import gtk from sugar.graphics import style from sugar.graphics.palette import WidgetInvoker + def _get_screen_area(): frame_thickness = style.GRID_CELL_SIZE @@ -28,6 +29,7 @@ def _get_screen_area(): return gtk.gdk.Rectangle(x, y, width, height) + class FrameWidgetInvoker(WidgetInvoker): def __init__(self, widget): WidgetInvoker.__init__(self, widget, widget.child) diff --git a/src/jarabe/frame/framewindow.py b/src/jarabe/frame/framewindow.py index a7d8fe7..c77e76c 100644 --- a/src/jarabe/frame/framewindow.py +++ b/src/jarabe/frame/framewindow.py @@ -19,6 +19,7 @@ import hippo from sugar.graphics import style + class FrameWindow(gtk.Window): __gtype_name__ = 'SugarFrameWindow' diff --git a/src/jarabe/frame/friendstray.py b/src/jarabe/frame/friendstray.py index b5437e5..31a9809 100644 --- a/src/jarabe/frame/friendstray.py +++ b/src/jarabe/frame/friendstray.py @@ -14,13 +14,16 @@ # 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 +import logging + from sugar.graphics.tray import VTray, TrayIcon from jarabe.view.buddymenu import BuddyMenu from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.model import shell -from jarabe.model.buddy import BuddyModel +from jarabe.model.buddy import get_owner_instance +from jarabe.model import neighborhood + class FriendIcon(TrayIcon): def __init__(self, buddy): @@ -32,46 +35,32 @@ class FriendIcon(TrayIcon): self.palette.props.icon_visible = False self.palette.set_group_id('frame') + class FriendsTray(VTray): def __init__(self): VTray.__init__(self) - self._activity_ps = None - self._joined_hid = -1 - self._left_hid = -1 + self._shared_activity = None 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) - shell.get_model().connect('active-activity-changed', - self._active_activity_changed_cb) + self.__active_activity_changed_cb) - def _get_activities_cb(self, activities_list): - for act in activities_list: - self.__activity_appeared_cb(self._pservice, act) + neighborhood.get_model().connect('activity-added', + self.__neighborhood_activity_added_cb) def add_buddy(self, buddy): - if self._buddies.has_key(buddy.props.key): + if buddy.props.key in self._buddies: return - model = BuddyModel(buddy=buddy) - - icon = FriendIcon(model) + icon = FriendIcon(buddy) 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): + if buddy.props.key not in self._buddies: return self.remove_item(self._buddies[buddy.props.key]) @@ -83,39 +72,23 @@ class FriendsTray(VTray): item.destroy() self._buddies = {} - def __activity_appeared_cb(self, pservice, activity_ps): - activity = shell.get_model().get_active_activity() - if activity and activity_ps.props.id == activity.get_activity_id(): - self._set_activity_ps(activity_ps, True) - - def _set_activity_ps(self, activity_ps, shared_activity): - if self._activity_ps == activity_ps: - return + def __neighborhood_activity_added_cb(self, neighborhood_model, + shared_activity): + logging.debug('FriendsTray.__neighborhood_activity_added_cb') + self.clear() - 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 + # always display ourselves + self.add_buddy(get_owner_instance()) - self._activity_ps = activity_ps + self._set_current_activity(shared_activity.activity_id) + def __active_activity_changed_cb(self, home_model, home_activity): + logging.debug('FriendsTray.__active_activity_changed_cb') 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.add_buddy(get_owner_instance()) - 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 @@ -123,19 +96,25 @@ class FriendsTray(VTray): 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._set_current_activity(activity_id) + + def _set_current_activity(self, activity_id): + logging.debug('FriendsTray._set_current_activity') + neighborhood_model = neighborhood.get_model() + self._shared_activity = neighborhood_model.get_activity(activity_id) + if self._shared_activity is None: + return + + for buddy in self._shared_activity.get_buddies(): + self.add_buddy(buddy) + + self._shared_activity.connect('buddy-added', self.__buddy_added_cb) + self._shared_activity.connect('buddy-removed', self.__buddy_removed_cb) + + def __buddy_added_cb(self, activity, buddy): + logging.debug('FriendsTray.__buddy_added_cb') self.add_buddy(buddy) - def __buddy_left_cb(self, activity, buddy): + def __buddy_removed_cb(self, activity, buddy): + logging.debug('FriendsTray.__buddy_removed_cb') self.remove_buddy(buddy) diff --git a/src/jarabe/frame/notification.py b/src/jarabe/frame/notification.py index 83dc27e..3471e2c 100644 --- a/src/jarabe/frame/notification.py +++ b/src/jarabe/frame/notification.py @@ -22,13 +22,14 @@ 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) + '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 = 3 @@ -45,7 +46,8 @@ class NotificationIcon(gtk.EventBox): self.add(self._icon) self._icon.show() - gobject.timeout_add_seconds(self._PULSE_TIMEOUT, self.__stop_pulsing_cb) + gobject.timeout_add_seconds(self._PULSE_TIMEOUT, + self.__stop_pulsing_cb) self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) @@ -80,6 +82,7 @@ class NotificationIcon(gtk.EventBox): palette = property(_get_palette, _set_palette) + class NotificationWindow(gtk.Window): __gtype_name__ = 'SugarNotificationWindow' @@ -97,4 +100,3 @@ class NotificationWindow(gtk.Window): color = gtk.gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html()) self.modify_bg(gtk.STATE_NORMAL, color) - diff --git a/src/jarabe/frame/zoomtoolbar.py b/src/jarabe/frame/zoomtoolbar.py index 2ed3c54..6c10c61 100644 --- a/src/jarabe/frame/zoomtoolbar.py +++ b/src/jarabe/frame/zoomtoolbar.py @@ -26,6 +26,7 @@ from sugar.graphics.radiotoolbutton import RadioToolButton from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.model import shell + class ZoomToolbar(gtk.Toolbar): def __init__(self): gtk.Toolbar.__init__(self) @@ -86,4 +87,3 @@ class ZoomToolbar(gtk.Toolbar): self._activity_button.props.active = True else: raise ValueError('Invalid zoom level: %r' % (new_level)) - diff --git a/src/jarabe/intro/Makefile.am b/src/jarabe/intro/Makefile.am index a9fb96b..2ea7cea 100644 --- a/src/jarabe/intro/Makefile.am +++ b/src/jarabe/intro/Makefile.am @@ -1,7 +1,3 @@ -imagedir = $(pythondir)/jarabe/intro -image_DATA = default-picture.png - -EXTRA_DIST = $(conf_DATA) $(image_DATA) sugardir = $(pythondir)/jarabe/intro sugar_PYTHON = \ __init__.py \ diff --git a/src/jarabe/intro/__init__.py b/src/jarabe/intro/__init__.py index ca4f64d..d2932f1 100644 --- a/src/jarabe/intro/__init__.py +++ b/src/jarabe/intro/__init__.py @@ -8,6 +8,7 @@ from sugar.profile import get_profile from jarabe.intro.window import IntroWindow from jarabe.intro.window import create_profile + def check_profile(): profile = get_profile() diff --git a/src/jarabe/intro/colorpicker.py b/src/jarabe/intro/colorpicker.py index a939857..997199b 100644 --- a/src/jarabe/intro/colorpicker.py +++ b/src/jarabe/intro/colorpicker.py @@ -20,6 +20,7 @@ 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) diff --git a/src/jarabe/intro/default-picture.png b/src/jarabe/intro/default-picture.png Binary files differdeleted file mode 100644 index e26b9b0..0000000 --- a/src/jarabe/intro/default-picture.png +++ /dev/null diff --git a/src/jarabe/intro/window.py b/src/jarabe/intro/window.py index 35c0cda..df19fbf 100644 --- a/src/jarabe/intro/window.py +++ b/src/jarabe/intro/window.py @@ -32,38 +32,34 @@ from sugar.graphics.xocolor import XoColor 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) +def create_profile(name, color=None): if not color: color = XoColor() - icon_path = os.path.join(env.get_profile_path(), "buddy-icon.jpg") - pixbuf.save(icon_path, "jpeg", {"quality":"85"}) - client = gconf.client_get_default() - client.set_string("/desktop/sugar/user/nick", name) - client.set_string("/desktop/sugar/user/color", color.to_string()) + client.set_string('/desktop/sugar/user/nick', name) + client.set_string('/desktop/sugar/user/color', color.to_string()) + client.suggest_sync() # Generate keypair import commands - keypath = os.path.join(env.get_profile_path(), "owner.key") + 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) + logging.error('Could not generate key pair: %d %s', s, o) else: - logging.error("Keypair exists, skip generation.") + logging.error('Keypair exists, skip generation.') + class _Page(hippo.CanvasBox): __gproperties__ = { - 'valid' : (bool, None, None, False, - gobject.PARAM_READABLE) + 'valid': (bool, None, None, False, gobject.PARAM_READABLE), } def __init__(self, **kwargs): @@ -81,6 +77,7 @@ class _Page(hippo.CanvasBox): def activate(self): pass + class _NamePage(_Page): def __init__(self, intro): _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER, @@ -90,7 +87,7 @@ class _NamePage(_Page): self._intro = intro - label = hippo.CanvasText(text=_("Name:")) + label = hippo.CanvasText(text=_('Name:')) self.append(label) self._entry = CanvasEntry(box_width=style.zoom(300)) @@ -118,6 +115,7 @@ class _NamePage(_Page): def activate(self): self._entry.props.widget.grab_focus() + class _ColorPage(_Page): def __init__(self, **kwargs): _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER, @@ -125,7 +123,7 @@ class _ColorPage(_Page): spacing=style.DEFAULT_SPACING, yalign=hippo.ALIGNMENT_CENTER, **kwargs) - self._label = hippo.CanvasText(text=_("Click to change color:"), + self._label = hippo.CanvasText(text=_('Click to change color:'), xalign=hippo.ALIGNMENT_CENTER) self.append(self._label) @@ -138,10 +136,11 @@ class _ColorPage(_Page): 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])) + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), } PAGE_NAME = 0 @@ -166,13 +165,9 @@ class _IntroBox(hippo.CanvasBox): self._page = self.PAGE_COLOR if default_nick == 'system': pwd_entry = pwd.getpwuid(os.getuid()) - if pwd_entry.pw_gecos: - nick = pwd_entry.pw_gecos.split(',')[0] - self._name_page.set_name(nick) - else: - self._name_page.set_name(pwd_entry.pw_name) - else: - self._name_page.set_name(default_nick) + default_nick = (pwd_entry.pw_gecos.split(',')[0] or + pwd_entry.pw_name) + self._name_page.set_name(default_nick) self._setup_page() @@ -255,6 +250,7 @@ class _IntroBox(hippo.CanvasBox): self.emit('done', name, color) + class IntroWindow(gtk.Window): def __init__(self): gtk.Window.__init__(self) @@ -282,16 +278,16 @@ class IntroWindow(gtk.Window): return False def __key_press_cb(self, widget, event): - if gtk.gdk.keyval_name(event.keyval) == "Return": + if gtk.gdk.keyval_name(event.keyval) == 'Return': self._intro_box.next() return True - elif gtk.gdk.keyval_name(event.keyval) == "Escape": + elif gtk.gdk.keyval_name(event.keyval) == 'Escape': self._intro_box.back() return True return False -if __name__ == "__main__": +if __name__ == '__main__': w = IntroWindow() w.show() w.connect('destroy', gtk.main_quit) diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am index f4bf273..ba29062 100644 --- a/src/jarabe/journal/Makefile.am +++ b/src/jarabe/journal/Makefile.am @@ -6,6 +6,7 @@ sugar_PYTHON = \ journalactivity.py \ journalentrybundle.py \ journaltoolbox.py \ + journalwindow.py \ keepicon.py \ listmodel.py \ listview.py \ diff --git a/src/jarabe/journal/detailview.py b/src/jarabe/journal/detailview.py index b4a2339..aa8c039 100644 --- a/src/jarabe/journal/detailview.py +++ b/src/jarabe/journal/detailview.py @@ -27,11 +27,12 @@ from sugar.graphics.icon import CanvasIcon from jarabe.journal.expandedentry import ExpandedEntry from jarabe.journal import model + class DetailView(gtk.VBox): __gtype_name__ = 'DetailView' __gsignals__ = { - 'go-back-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])) + 'go-back-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } def __init__(self, **kwargs): @@ -84,6 +85,7 @@ class DetailView(gtk.VBox): metadata = gobject.property( type=object, getter=get_metadata, setter=set_metadata) + class BackBar(hippo.CanvasBox): def __init__(self): hippo.CanvasBox.__init__(self, diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py index c8e40c1..fe2f320 100644 --- a/src/jarabe/journal/expandedentry.py +++ b/src/jarabe/journal/expandedentry.py @@ -36,6 +36,7 @@ from jarabe.journal.palettes import ObjectPalette, BuddyPalette from jarabe.journal import misc from jarabe.journal import model + class Separator(hippo.CanvasBox, hippo.CanvasItem): def __init__(self, orientation): hippo.CanvasBox.__init__(self, @@ -46,6 +47,7 @@ class Separator(hippo.CanvasBox, hippo.CanvasItem): else: self.props.box_height = style.LINE_WIDTH + class BuddyList(hippo.CanvasBox): def __init__(self, buddies): hippo.CanvasBox.__init__(self, xalign=hippo.ALIGNMENT_START, @@ -61,6 +63,7 @@ class BuddyList(hippo.CanvasBox): hbox.append(icon) self.append(hbox) + class ExpandedEntry(hippo.CanvasBox): def __init__(self): hippo.CanvasBox.__init__(self) @@ -209,9 +212,7 @@ class ExpandedEntry(hippo.CanvasBox): height = style.zoom(240) box = hippo.CanvasBox() - if self._metadata.has_key('preview') and \ - len(self._metadata['preview']) > 4: - + if len(self._metadata.get('preview', '')) > 4: if self._metadata['preview'][1:4] == 'PNG': preview_data = self._metadata['preview'] else: @@ -260,8 +261,9 @@ class ExpandedEntry(hippo.CanvasBox): lines = [ _('Kind: %s') % (self._metadata.get('mime_type') or _('Unknown'),), _('Date: %s') % (self._format_date(),), - _('Size: %s') % (format_size(model.get_file_size( - self._metadata['uid'])),)] + _('Size: %s') % (format_size(int(self._metadata.get('filesize', + model.get_file_size(self._metadata['uid']))))), + ] for line in lines: text = hippo.CanvasText(text=line, @@ -279,10 +281,15 @@ class ExpandedEntry(hippo.CanvasBox): def _format_date(self): if 'timestamp' in self._metadata: - timestamp = float(self._metadata['timestamp']) - return time.strftime('%x', time.localtime(timestamp)) - else: - return _('No date') + try: + timestamp = float(self._metadata['timestamp']) + except (ValueError, TypeError): + logging.warning('Invalid timestamp for %r: %r', + self._metadata['uid'], + self._metadata['timestamp']) + else: + return time.strftime('%x', time.localtime(timestamp)) + return _('No date') def _create_buddy_list(self): @@ -300,8 +307,7 @@ class ExpandedEntry(hippo.CanvasBox): vbox.append(text) - if self._metadata.has_key('buddies') and \ - self._metadata['buddies']: + if self._metadata.get('buddies'): buddies = simplejson.loads(self._metadata['buddies']).values() vbox.append(BuddyList(buddies)) return vbox diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index 0559560..a33038a 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -17,8 +17,6 @@ import logging from gettext import gettext as _ -import sys -import traceback import uuid import gtk @@ -27,6 +25,8 @@ import statvfs import os from sugar.graphics.window import Window +from sugar.graphics.alert import ErrorAlert + from sugar.bundle.bundle import ZipExtractException, RegistrationException from sugar import env from sugar.activity import activityfactory @@ -42,6 +42,8 @@ from jarabe.journal.journalentrybundle import JournalEntryBundle from jarabe.journal.objectchooser import ObjectChooser from jarabe.journal.modalalert import ModalAlert from jarabe.journal import model +from jarabe.journal.journalwindow import JournalWindow + J_DBUS_SERVICE = 'org.laptop.Journal' J_DBUS_INTERFACE = 'org.laptop.Journal' @@ -50,6 +52,9 @@ J_DBUS_PATH = '/org/laptop/Journal' _SPACE_TRESHOLD = 52428800 _BUNDLE_ID = 'org.laptop.JournalActivity' +_journal = None + + class JournalActivityDBusService(dbus.service.Object): def __init__(self, parent): self._parent = parent @@ -79,7 +84,8 @@ class JournalActivityDBusService(dbus.service.Object): chooser.destroy() del chooser - @dbus.service.method(J_DBUS_INTERFACE, in_signature='is', out_signature='s') + @dbus.service.method(J_DBUS_INTERFACE, in_signature='is', + out_signature='s') def ChooseObject(self, parent_xid, what_filter=''): chooser_id = uuid.uuid4().hex if parent_xid > 0: @@ -92,18 +98,19 @@ class JournalActivityDBusService(dbus.service.Object): return chooser_id - @dbus.service.signal(J_DBUS_INTERFACE, signature="ss") + @dbus.service.signal(J_DBUS_INTERFACE, signature='ss') def ObjectChooserResponse(self, chooser_id, object_id): pass - @dbus.service.signal(J_DBUS_INTERFACE, signature="s") + @dbus.service.signal(J_DBUS_INTERFACE, signature='s') def ObjectChooserCancelled(self, chooser_id): pass -class JournalActivity(Window): + +class JournalActivity(JournalWindow): def __init__(self): - logging.debug("STARTUP: Loading the journal") - Window.__init__(self) + logging.debug('STARTUP: Loading the journal') + JournalWindow.__init__(self) self.set_title(_('Journal')) @@ -138,6 +145,15 @@ class JournalActivity(Window): self._critical_space_alert = None self._check_available_space() + def __volume_error_cb(self, gobject, message, severity): + alert = ErrorAlert(title=severity, msg=message) + alert.connect('response', self.__alert_response_cb) + self.add_alert(alert) + alert.show() + + def __alert_response_cb(self, alert, response_id): + self.remove_alert(alert) + def __realize_cb(self, window): wm.set_bundle_id(window.window, _BUNDLE_ID) activity_id = activityfactory.create_activity_id() @@ -161,6 +177,7 @@ class JournalActivity(Window): self._volumes_toolbar = VolumesToolbar() self._volumes_toolbar.connect('volume-changed', self.__volume_changed_cb) + self._volumes_toolbar.connect('volume-error', self.__volume_error_cb) self._main_view.pack_start(self._volumes_toolbar, expand=False) search_toolbar = self._main_toolbox.search_toolbar @@ -171,7 +188,8 @@ class JournalActivity(Window): self._secondary_view = gtk.VBox() self._detail_toolbox = DetailToolbox() - entry_toolbar = self._detail_toolbox.entry_toolbar + self._detail_toolbox.entry_toolbar.connect('volume-error', + self.__volume_error_cb) self._detail_view = DetailView() self._detail_view.connect('go-back-clicked', self.__go_back_clicked_cb) @@ -210,8 +228,7 @@ class JournalActivity(Window): try: self._detail_toolbox.entry_toolbar.set_metadata(metadata) except Exception: - logging.error('Exception while displaying entry:\n' + \ - ''.join(traceback.format_exception(*sys.exc_info()))) + logging.exception('Exception while displaying entry:') self.set_toolbar_box(self._detail_toolbox) self._detail_toolbox.show() @@ -219,8 +236,7 @@ class JournalActivity(Window): try: self._detail_view.props.metadata = metadata except Exception: - logging.error('Exception while displaying entry:\n' + \ - ''.join(traceback.format_exception(*sys.exc_info()))) + logging.exception('Exception while displaying entry:') self.set_canvas(self._secondary_view) self._secondary_view.show() @@ -314,11 +330,11 @@ class JournalActivity(Window): self._list_view.set_is_visible(visible) def _check_available_space(self): - ''' Check available space on device + """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 @@ -345,7 +361,6 @@ class JournalActivity(Window): self.show_main_view() self.search_grab_focus() -_journal = None def get_journal(): global _journal @@ -354,6 +369,6 @@ def get_journal(): _journal.show() return _journal + def start(): get_journal() - diff --git a/src/jarabe/journal/journalentrybundle.py b/src/jarabe/journal/journalentrybundle.py index 41777c7..c220c09 100644 --- a/src/jarabe/journal/journalentrybundle.py +++ b/src/jarabe/journal/journalentrybundle.py @@ -25,6 +25,7 @@ from sugar.bundle.bundle import Bundle, MalformedBundleException from jarabe.journal import model + class JournalEntryBundle(Bundle): """A Journal entry bundle @@ -41,7 +42,7 @@ class JournalEntryBundle(Bundle): Bundle.__init__(self, path) def install(self, uid=''): - if os.environ.has_key('SUGAR_ACTIVITY_ROOT'): + if 'SUGAR_ACTIVITY_ROOT' in os.environ: install_dir = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'], 'data') else: @@ -91,4 +92,3 @@ class JournalEntryBundle(Bundle): 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 index 61671bc..cdf998d 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -43,6 +43,7 @@ from jarabe.model import bundleregistry from jarabe.journal import misc from jarabe.journal import model + _AUTOSEARCH_TIMEOUT = 1000 _ACTION_ANYTIME = 0 @@ -68,13 +69,13 @@ class MainToolbox(Toolbox): self.add_toolbar(_('Search'), self.search_toolbar) self.search_toolbar.show() + class SearchToolbar(gtk.Toolbar): __gtype_name__ = 'SearchToolbar' __gsignals__ = { - 'query-changed': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([object])) + 'query-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), } def __init__(self): @@ -109,6 +110,14 @@ class SearchToolbar(gtk.Toolbar): self.insert(tool_item, -1) tool_item.show() + self._sorting_button = SortingButton() + self._sorting_button.connect('clicked', + self.__sorting_button_clicked_cb) + self.insert(self._sorting_button, -1) + self._sorting_button.connect('sort-property-changed', + self.__sort_changed_cb) + self._sorting_button.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) @@ -191,6 +200,14 @@ class SearchToolbar(gtk.Toolbar): if text: query['query'] = text + property_, order = self._sorting_button.get_current_sort() + + if order == gtk.SORT_ASCENDING: + sign = '+' + else: + sign = '-' + query['order_by'] = [sign + property_] + return query def _get_date_range(self): @@ -213,6 +230,12 @@ class SearchToolbar(gtk.Toolbar): def _combo_changed_cb(self, combo): self._update_if_needed() + def __sort_changed_cb(self, button): + self._update_if_needed() + + def __sorting_button_clicked_cb(self, button): + self._sorting_button.palette.popup(immediate=True, state=1) + def _update_if_needed(self): new_query = self._build_query() if self._query != new_query: @@ -320,6 +343,7 @@ class SearchToolbar(gtk.Toolbar): self._when_search_combo.set_active(0) self._favorite_button.props.active = False + class DetailToolbox(Toolbox): def __init__(self): Toolbox.__init__(self) @@ -328,7 +352,13 @@ class DetailToolbox(Toolbox): self.add_toolbar('', self.entry_toolbar) self.entry_toolbar.show() + class EntryToolbar(gtk.Toolbar): + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + def __init__(self): gtk.Toolbar.__init__(self) @@ -398,7 +428,22 @@ class EntryToolbar(gtk.Toolbar): misc.resume(self._metadata, service_name) def _copy_menu_item_activate_cb(self, menu_item, mount_point): - model.copy(self._metadata, mount_point) + file_path = model.get_file(self._metadata['uid']) + + if not file_path or not os.path.exists(file_path): + logging.warn('Entries without a file cannot be copied.') + self.emit('volume-error', + _('Entries without a file cannot be copied.'), + _('Warning')) + return + + try: + model.copy(self._metadata, mount_point) + except IOError, e: + logging.exception('Error while copying the entry. %s', e.strerror) + self.emit('volume-error', + _('Error while copying the entry. %s') % e.strerror, + _('Error')) def _refresh_copy_palette(self): palette = self._copy.get_palette() @@ -456,3 +501,48 @@ class EntryToolbar(gtk.Toolbar): activity_info.get_bundle_id()) palette.menu.append(menu_item) menu_item.show() + + +class SortingButton(ToolButton): + __gtype_name__ = 'JournalSortingButton' + + __gsignals__ = { + 'sort-property-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + + _SORT_OPTIONS = [ + ('timestamp', 'view-lastedit', _('Sort by date modified')), + ('creation_time', 'view-created', _('Sort by date created')), + ('filesize', 'view-size', _('Sort by size')), + ] + + def __init__(self): + ToolButton.__init__(self) + + self._property = 'timestamp' + self._order = gtk.SORT_ASCENDING + + self.props.tooltip = _('Sort view') + self.props.icon_name = 'view-lastedit' + + for property_, icon, label in self._SORT_OPTIONS: + button = MenuItem(icon_name=icon, text_label=label) + button.connect('activate', + self.__sort_type_changed_cb, + property_, + icon) + button.show() + self.props.palette.menu.insert(button, -1) + + def __sort_type_changed_cb(self, widget, property_, icon_name): + self._property = property_ + #FIXME: Implement sorting order + self._order = gtk.SORT_ASCENDING + self.emit('sort-property-changed') + + self.props.icon_name = icon_name + + def get_current_sort(self): + return (self._property, self._order) diff --git a/src/jarabe/desktop/myicon.py b/src/jarabe/journal/journalwindow.py index 4a4ad95..31bc790 100644 --- a/src/jarabe/desktop/myicon.py +++ b/src/jarabe/journal/journalwindow.py @@ -1,4 +1,5 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +#Copyright (C) 2010 Software for Education, Entertainment and Training +#Activities # # 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 @@ -14,15 +15,19 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -import gconf +from sugar.graphics.window import Window -from sugar.graphics.icon import CanvasIcon -from sugar.graphics.xocolor import XoColor +_journal_window = None -class MyIcon(CanvasIcon): - def __init__(self, size): - client = gconf.client_get_default() - color = XoColor(client.get_string("/desktop/sugar/user/color")) - CanvasIcon.__init__(self, size=size, - icon_name='computer-xo', - xo_color=color) + +class JournalWindow(Window): + + def __init__(self): + + global _journal_window + Window.__init__(self) + _journal_window = self + + +def get_journal_window(): + return _journal_window diff --git a/src/jarabe/journal/keepicon.py b/src/jarabe/journal/keepicon.py index 2c692c6..1253afc 100644 --- a/src/jarabe/journal/keepicon.py +++ b/src/jarabe/journal/keepicon.py @@ -22,6 +22,7 @@ from sugar.graphics.icon import CanvasIcon from sugar.graphics import style from sugar.graphics.xocolor import XoColor + class KeepIcon(CanvasIcon): def __init__(self, keep): CanvasIcon.__init__(self, icon_name='emblem-favorite', diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py index 07f8544..3902eba 100644 --- a/src/jarabe/journal/listmodel.py +++ b/src/jarabe/journal/listmodel.py @@ -19,6 +19,7 @@ import logging import simplejson import gobject import gtk +from gettext import gettext as _ from sugar.graphics.xocolor import XoColor from sugar.graphics import style @@ -27,20 +28,18 @@ from sugar import util from jarabe.journal import model from jarabe.journal import misc + DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' DS_DBUS_PATH = '/org/laptop/sugar/DataStore' + class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): __gtype_name__ = 'JournalListModel' __gsignals__ = { - 'ready': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])), - 'progress': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])), + 'ready': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } COLUMN_UID = 0 @@ -48,22 +47,28 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): COLUMN_ICON = 2 COLUMN_ICON_COLOR = 3 COLUMN_TITLE = 4 - COLUMN_DATE = 5 - COLUMN_PROGRESS = 6 - COLUMN_BUDDY_1 = 7 - COLUMN_BUDDY_2 = 8 - COLUMN_BUDDY_3 = 9 - - _COLUMN_TYPES = {COLUMN_UID: str, - COLUMN_FAVORITE: bool, - COLUMN_ICON: str, - COLUMN_ICON_COLOR: object, - COLUMN_TITLE: str, - COLUMN_DATE: str, - COLUMN_PROGRESS: int, - COLUMN_BUDDY_1: object, - COLUMN_BUDDY_3: object, - COLUMN_BUDDY_2: object} + COLUMN_TIMESTAMP = 5 + COLUMN_CREATION_TIME = 6 + COLUMN_FILESIZE = 7 + COLUMN_PROGRESS = 8 + COLUMN_BUDDY_1 = 9 + COLUMN_BUDDY_2 = 10 + COLUMN_BUDDY_3 = 11 + + _COLUMN_TYPES = { + COLUMN_UID: str, + COLUMN_FAVORITE: bool, + COLUMN_ICON: str, + COLUMN_ICON_COLOR: object, + COLUMN_TITLE: str, + COLUMN_TIMESTAMP: str, + COLUMN_CREATION_TIME: str, + COLUMN_FILESIZE: str, + COLUMN_PROGRESS: int, + COLUMN_BUDDY_1: object, + COLUMN_BUDDY_3: object, + COLUMN_BUDDY_2: object, + } _PAGE_SIZE = 10 @@ -135,25 +140,64 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): xo_color = misc.get_icon_color(metadata) self._cached_row.append(xo_color) - title = gobject.markup_escape_text(metadata.get('title', None)) - self._cached_row.append('<b>%s</b>' % title) + title = gobject.markup_escape_text(metadata.get('title', + _('Untitled'))) + self._cached_row.append('<b>%s</b>' % (title, )) - timestamp = int(metadata.get('timestamp', 0)) - self._cached_row.append(util.timestamp_to_elapsed_string(timestamp)) + try: + timestamp = float(metadata.get('timestamp', 0)) + except (TypeError, ValueError): + timestamp_content = _('Unknown') + else: + timestamp_content = util.timestamp_to_elapsed_string(timestamp) + self._cached_row.append(timestamp_content) - self._cached_row.append(int(metadata.get('progress', 100))) + try: + creation_time = float(metadata.get('creation_time')) + except (TypeError, ValueError): + self._cached_row.append(_('Unknown')) + else: + self._cached_row.append( + util.timestamp_to_elapsed_string(float(creation_time))) - if metadata.get('buddies', ''): - buddies = simplejson.loads(metadata['buddies']).values() + try: + size = int(metadata.get('filesize')) + except (TypeError, ValueError): + self._cached_row.append(_('Unknown')) else: + self._cached_row.append(util.format_size(size)) + + try: + progress = int(float(metadata.get('progress', 100))) + except (TypeError, ValueError): + progress = 100 + self._cached_row.append(progress) + + buddies = [] + if metadata.get('buddies'): + try: + buddies = simplejson.loads(metadata['buddies']).values() + except simplejson.decoder.JSONDecodeError, exception: + logging.warning('Cannot decode buddies for %r: %s', + metadata['uid'], exception) + + if not isinstance(buddies, list): + logging.warning('Content of buddies for %r is not a list: %r', + metadata['uid'], buddies) buddies = [] for n_ in xrange(0, 3): if buddies: - nick, color = buddies.pop(0) - self._cached_row.append((nick, XoColor(color))) - else: - self._cached_row.append(None) + try: + nick, color = buddies.pop(0) + except (AttributeError, ValueError), exception: + logging.warning('Malformed buddies for %r: %s', + metadata['uid'], exception) + else: + self._cached_row.append((nick, XoColor(color))) + continue + + self._cached_row.append(None) return self._cached_row[column] @@ -198,4 +242,3 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): return True return False - diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index 9e19f70..0aee1b7 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -34,11 +34,13 @@ from jarabe.journal.palettes import ObjectPalette, BuddyPalette from jarabe.journal import model from jarabe.journal import misc + UPDATE_INTERVAL = 300 MESSAGE_EMPTY_JOURNAL = 0 MESSAGE_NO_MATCH = 1 + class TreeView(gtk.TreeView): __gtype_name__ = 'JournalTreeView' @@ -47,8 +49,8 @@ class TreeView(gtk.TreeView): self.set_headers_visible(False) def do_size_request(self, requisition): - # HACK: We tell the model that the view is just resizing so it can avoid - # hitting both D-Bus and disk. + # HACK: We tell the model that the view is just resizing so it can + # avoid hitting both D-Bus and disk. tree_model = self.get_model() if tree_model is not None: tree_model.view_is_resizing = True @@ -58,13 +60,12 @@ class TreeView(gtk.TreeView): if tree_model is not None: tree_model.view_is_resizing = False + class BaseListView(gtk.Bin): __gtype_name__ = 'JournalBaseListView' __gsignals__ = { - 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])) + 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } def __init__(self): @@ -81,7 +82,8 @@ class BaseListView(gtk.Bin): self.connect('destroy', self.__destroy_cb) self._scrolled_window = gtk.ScrolledWindow() - self._scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + self._scrolled_window.set_policy(gtk.POLICY_NEVER, + gtk.POLICY_AUTOMATIC) self.add(self._scrolled_window) self._scrolled_window.show() @@ -97,7 +99,7 @@ class BaseListView(gtk.Bin): self.cell_title = None self.cell_icon = None self._title_column = None - self.date_column = None + self.sort_column = None self._add_columns() self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, @@ -141,7 +143,8 @@ class BaseListView(gtk.Bin): column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED column.props.fixed_width = self.cell_icon.props.width column.pack_start(self.cell_icon) - column.add_attribute(self.cell_icon, 'file-name', ListModel.COLUMN_ICON) + column.add_attribute(self.cell_icon, 'file-name', + ListModel.COLUMN_ICON) column.add_attribute(self.cell_icon, 'xo-color', ListModel.COLUMN_ICON_COLOR) self.tree_view.append_column(column) @@ -163,7 +166,8 @@ class BaseListView(gtk.Bin): buddies_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED self.tree_view.append_column(buddies_column) - for column_index in [ListModel.COLUMN_BUDDY_1, ListModel.COLUMN_BUDDY_2, + for column_index in [ListModel.COLUMN_BUDDY_1, + ListModel.COLUMN_BUDDY_2, ListModel.COLUMN_BUDDY_3]: cell_icon = CellRendererBuddy(self.tree_view, column_index=column_index) @@ -190,15 +194,16 @@ class BaseListView(gtk.Bin): date = util.timestamp_to_elapsed_string(timestamp) date_width = self._get_width_for_string(date) - self.date_column = gtk.TreeViewColumn() - self.date_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED - self.date_column.props.fixed_width = date_width - self.date_column.set_alignment(1) - self.date_column.props.resizable = True - self.date_column.props.clickable = True - self.date_column.pack_start(cell_text) - self.date_column.add_attribute(cell_text, 'text', ListModel.COLUMN_DATE) - self.tree_view.append_column(self.date_column) + self.sort_column = gtk.TreeViewColumn() + self.sort_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + self.sort_column.props.fixed_width = date_width + self.sort_column.set_alignment(1) + self.sort_column.props.resizable = True + self.sort_column.props.clickable = True + self.sort_column.pack_start(cell_text) + self.sort_column.add_attribute(cell_text, 'text', + ListModel.COLUMN_TIMESTAMP) + self.tree_view.append_column(self.sort_column) def _get_width_for_string(self, text): # Add some extra margin @@ -252,11 +257,16 @@ class BaseListView(gtk.Bin): def update_with_query(self, query_dict): logging.debug('ListView.update_with_query') + if 'order_by' not in query_dict: + query_dict['order_by'] = ['+timestamp'] + if query_dict['order_by'] != self._query.get('order_by'): + property_ = query_dict['order_by'][0][1:] + cell_text = self.sort_column.get_cell_renderers()[0] + self.sort_column.set_attributes(cell_text, + text=getattr(ListModel, 'COLUMN_' + property_.upper(), + ListModel.COLUMN_TIMESTAMP)) self._query = query_dict - if 'order_by' not in self._query: - self._query['order_by'] = ['+timestamp'] - self.refresh() def refresh(self): @@ -316,12 +326,9 @@ class BaseListView(gtk.Bin): def _is_query_empty(self): # FIXME: This is a hack, we shouldn't have to update this every time # a new search term is added. - if self._query.get('query', '') or self._query.get('mime_type', '') or \ - self._query.get('keep', '') or self._query.get('mtime', '') or \ - self._query.get('activity', ''): - return False - else: - return True + return not (self._query.get('query') or self._query.get('mime_type') or + self._query.get('keep') or self._query.get('mtime') or + self._query.get('activity')) def __model_progress_cb(self, tree_model): if self._progress_bar is None: @@ -365,8 +372,8 @@ class BaseListView(gtk.Bin): 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()) + stroke_color=style.COLOR_BUTTON_GREY.get_svg(), + fill_color=style.COLOR_TRANSPARENT.get_svg()) box.append(icon) if message == MESSAGE_EMPTY_JOURNAL: @@ -379,7 +386,7 @@ class BaseListView(gtk.Bin): text = hippo.CanvasText(text=text, xalign=hippo.ALIGNMENT_CENTER, font_desc=style.FONT_BOLD.get_pango_desc(), - color = style.COLOR_BUTTON_GREY.get_int()) + color=style.COLOR_BUTTON_GREY.get_int()) box.append(text) if message == MESSAGE_NO_MATCH: @@ -415,7 +422,7 @@ class BaseListView(gtk.Bin): while True: x, y, width, height = self.tree_view.get_cell_area(path, - self.date_column) + self.sort_column) x, y = self.tree_view.convert_tree_to_widget_coords(x, y) self.tree_view.queue_draw_area(x, y, width, height) if path == end_path: @@ -455,13 +462,13 @@ class BaseListView(gtk.Bin): self.update_dates() return True + class ListView(BaseListView): __gtype_name__ = 'JournalListView' __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([object])) + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), } def __init__(self): @@ -533,6 +540,7 @@ class ListView(BaseListView): def __editing_canceled_cb(self, cell): self.cell_title.props.editable = False + class CellRendererFavorite(CellRendererIcon): __gtype_name__ = 'JournalCellRendererFavorite' @@ -547,6 +555,7 @@ class CellRendererFavorite(CellRendererIcon): self.props.prelit_stroke_color = style.COLOR_BUTTON_GREY.get_svg() self.props.prelit_fill_color = style.COLOR_BUTTON_GREY.get_svg() + class CellRendererDetail(CellRendererIcon): __gtype_name__ = 'JournalCellRendererDetail' @@ -563,12 +572,12 @@ class CellRendererDetail(CellRendererIcon): self.props.prelit_stroke_color = style.COLOR_TRANSPARENT.get_svg() self.props.prelit_fill_color = style.COLOR_BLACK.get_svg() + class CellRendererActivityIcon(CellRendererIcon): __gtype_name__ = 'JournalCellRendererActivityIcon' __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), } @@ -605,6 +614,7 @@ class CellRendererActivityIcon(CellRendererIcon): show_palette = gobject.property(type=bool, default=True, setter=set_show_palette) + class CellRendererBuddy(CellRendererIcon): __gtype_name__ = 'JournalCellRendererBuddy' @@ -638,4 +648,3 @@ class CellRendererBuddy(CellRendererIcon): self.props.xo_color = xo_color buddy = gobject.property(type=object, setter=set_buddy) - diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py index 24ad216..1431d5f 100644 --- a/src/jarabe/journal/misc.py +++ b/src/jarabe/journal/misc.py @@ -27,8 +27,10 @@ from sugar.activity import activityfactory from sugar.activity.activityhandle import ActivityHandle from sugar.graphics.icon import get_icon_file_name from sugar.graphics.xocolor import XoColor +from sugar.graphics.alert import ConfirmationAlert from sugar import mime from sugar.bundle.activitybundle import ActivityBundle +from sugar.bundle.bundle import AlreadyInstalledException from sugar.bundle.contentbundle import ContentBundle from sugar import util @@ -36,6 +38,8 @@ from jarabe.view import launcher from jarabe.model import bundleregistry, shell from jarabe.journal.journalentrybundle import JournalEntryBundle from jarabe.journal import model +from jarabe.journal import journalwindow + def _get_icon_for_mime(mime_type): generic_types = mime.get_all_generic_types() @@ -52,6 +56,7 @@ def _get_icon_for_mime(mime_type): if file_name is not None: return file_name + def get_icon_name(metadata): file_name = None @@ -81,35 +86,46 @@ def get_icon_name(metadata): return file_name + def get_date(metadata): """ Convert from a string in iso format to a more human-like format. """ - if metadata.has_key('timestamp'): - timestamp = float(metadata['timestamp']) - return util.timestamp_to_elapsed_string(timestamp) - elif metadata.has_key('mtime'): - ti = time.strptime(metadata['mtime'], "%Y-%m-%dT%H:%M:%S") - return util.timestamp_to_elapsed_string(time.mktime(ti)) - else: - return _('No date') + if 'timestamp' in metadata: + try: + timestamp = float(metadata['timestamp']) + except (TypeError, ValueError): + logging.warning('Invalid timestamp: %r', metadata['timestamp']) + else: + return util.timestamp_to_elapsed_string(timestamp) + + if 'mtime' in metadata: + try: + ti = time.strptime(metadata['mtime'], '%Y-%m-%dT%H:%M:%S') + except (TypeError, ValueError): + logging.warning('Invalid mtime: %r', metadata['mtime']) + else: + return util.timestamp_to_elapsed_string(time.mktime(ti)) + + return _('No date') + def get_bundle(metadata): try: if is_activity_bundle(metadata): - file_path = util.TempFilePath(model.get_file(metadata['uid'])) + file_path = model.get_file(metadata['uid']) if not os.path.exists(file_path): logging.warning('Invalid path: %r', file_path) return None return ActivityBundle(file_path) elif is_content_bundle(metadata): - file_path = util.TempFilePath(model.get_file(metadata['uid'])) + file_path = model.get_file(metadata['uid']) if not os.path.exists(file_path): logging.warning('Invalid path: %r', file_path) return None return ContentBundle(file_path) elif is_journal_bundle(metadata): - file_path = util.TempFilePath(model.get_file(metadata['uid'])) + file_path = model.get_file(metadata['uid']) if not os.path.exists(file_path): logging.warning('Invalid path: %r', file_path) return None @@ -120,6 +136,7 @@ def get_bundle(metadata): logging.exception('Incorrect bundle') return None + def _get_activities_for_mime(mime_type): registry = bundleregistry.get_registry() result = registry.get_activities_for_type(mime_type) @@ -130,6 +147,7 @@ def _get_activities_for_mime(mime_type): result.append(activity) return result + def get_activities(metadata): activities = [] @@ -148,6 +166,7 @@ def get_activities(metadata): return activities + def resume(metadata, bundle_id=None): registry = bundleregistry.get_registry() @@ -159,19 +178,16 @@ def resume(metadata, bundle_id=None): bundle = ActivityBundle(file_path) if not registry.is_installed(bundle): logging.debug('Installing activity bundle') - registry.install(bundle) + try: + registry.install(bundle) + except AlreadyInstalledException: + _downgrade_option_alert(bundle) + return else: logging.debug('Upgrading activity bundle') registry.upgrade(bundle) - logging.debug('activityfactory.creating bundle with id %r', - bundle.get_bundle_id()) - installed_bundle = registry.get_bundle(bundle.get_bundle_id()) - if installed_bundle: - activityfactory.create(installed_bundle) - else: - logging.error('Bundle %r is not installed.', - bundle.get_bundle_id()) + _launch_bundle(bundle) elif is_content_bundle(metadata) and bundle_id is None: @@ -192,17 +208,10 @@ def resume(metadata, bundle_id=None): logging.debug('activityfactory.creating with uri %s', uri) activity_bundle = registry.get_bundle(activities[0].get_bundle_id()) - activityfactory.create_with_uri(activity_bundle, bundle.get_start_uri()) + launch(activity_bundle, uri=uri) else: activity_id = metadata.get('activity_id', '') - if activity_id: - shell_model = shell.get_model() - activity = shell_model.get_activity_by_id(activity_id) - if activity: - activity.get_window().activate(gtk.get_current_event_time()) - return - if bundle_id is None: activities = get_activities(metadata) if not activities: @@ -213,36 +222,91 @@ def resume(metadata, bundle_id=None): bundle = registry.get_bundle(bundle_id) - if metadata.get('mountpoint', '/') == '/': object_id = metadata['uid'] else: object_id = model.copy(metadata, '/') - if activity_id: - launcher.add_launcher(activity_id, bundle.get_icon(), - get_icon_color(metadata)) - handle = ActivityHandle(object_id=object_id, - activity_id=activity_id) - activityfactory.create(bundle, handle) - else: - activityfactory.create_with_object_id(bundle, object_id) + launch(bundle, activity_id=activity_id, object_id=object_id, + color=get_icon_color(metadata)) + + +def _launch_bundle(bundle): + registry = bundleregistry.get_registry() + logging.debug('activityfactory.creating bundle with id %r', + bundle.get_bundle_id()) + installed_bundle = registry.get_bundle(bundle.get_bundle_id()) + if installed_bundle: + launch(installed_bundle) + else: + logging.error('Bundle %r is not installed.', + bundle.get_bundle_id()) + + +def launch(bundle, activity_id=None, object_id=None, uri=None, color=None, + invited=False): + if activity_id is None or not activity_id: + activity_id = activityfactory.create_activity_id() + + logging.debug('launch bundle_id=%s activity_id=%s object_id=%s uri=%s', + bundle.get_bundle_id(), activity_id, object_id, uri) + + shell_model = shell.get_model() + activity = shell_model.get_activity_by_id(activity_id) + if activity is not None: + logging.debug('re-launch %r', activity.get_window()) + activity.get_window().activate(gtk.get_current_event_time()) + return + + if color is None: + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + + launcher.add_launcher(activity_id, bundle.get_icon(), color) + activity_handle = ActivityHandle(activity_id=activity_id, + object_id=object_id, uri=uri, invited=invited) + activityfactory.create(bundle, activity_handle) + + +def _downgrade_option_alert(bundle): + alert = ConfirmationAlert() + alert.props.title = _('Older Version Of %s Activity') % (bundle.get_name()) + alert.props.msg = _('Do you want to downgrade to version %s') % \ + bundle.get_activity_version() + alert.connect('response', _downgrade_alert_response_cb, bundle) + journalwindow.get_journal_window().add_alert(alert) + alert.show() + + +def _downgrade_alert_response_cb(alert, response_id, bundle): + if response_id is gtk.RESPONSE_OK: + journalwindow.get_journal_window().remove_alert(alert) + registry = bundleregistry.get_registry() + registry.install(bundle, force_downgrade=True) + _launch_bundle(bundle) + elif response_id is gtk.RESPONSE_CANCEL: + journalwindow.get_journal_window().remove_alert(alert) + def is_activity_bundle(metadata): mime_type = metadata.get('mime_type', '') return mime_type == ActivityBundle.MIME_TYPE or \ mime_type == ActivityBundle.DEPRECATED_MIME_TYPE + def is_content_bundle(metadata): return metadata.get('mime_type', '') == ContentBundle.MIME_TYPE + def is_journal_bundle(metadata): return metadata.get('mime_type', '') == JournalEntryBundle.MIME_TYPE + def is_bundle(metadata): return is_activity_bundle(metadata) or is_content_bundle(metadata) or \ is_journal_bundle(metadata) + def get_icon_color(metadata): if metadata is None or not 'icon-color' in metadata: client = gconf.client_get_default() diff --git a/src/jarabe/journal/modalalert.py b/src/jarabe/journal/modalalert.py index c7c6a0a..6880941 100644 --- a/src/jarabe/journal/modalalert.py +++ b/src/jarabe/journal/modalalert.py @@ -22,6 +22,7 @@ from sugar.graphics.icon import Icon from sugar.graphics import style from sugar.graphics.xocolor import XoColor + class ModalAlert(gtk.Window): __gtype_name__ = 'SugarModalAlert' @@ -84,14 +85,12 @@ class ModalAlert(gtk.Window): self.add(self._main_view) self._main_view.show() - self.connect("realize", self.__realize_cb) + 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 - ''' + """The opener will listen on the destroy signal""" self.destroy() - diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py index ffc62e0..320e577 100644 --- a/src/jarabe/journal/model.py +++ b/src/jarabe/journal/model.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007-2008, One Laptop Per Child +# Copyright (C) 2007-2010, 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 @@ -16,11 +16,13 @@ import logging import os +import errno from datetime import datetime import time import shutil -from stat import S_IFMT, S_IFDIR, S_IFREG +from stat import S_IFLNK, S_IFMT, S_IFDIR, S_IFREG import re +from operator import itemgetter import gobject import dbus @@ -31,18 +33,25 @@ from sugar import dispatch from sugar import mime from sugar import util + DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' DS_DBUS_PATH = '/org/laptop/sugar/DataStore' # Properties the journal cares about. -PROPERTIES = ['uid', 'title', 'mtime', 'timestamp', 'keep', 'buddies', - 'icon-color', 'mime_type', 'progress', 'activity', 'mountpoint', - 'activity_id', 'bundle_id'] +PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id', + 'creation_time', 'filesize', 'icon-color', 'keep', 'mime_type', + 'mountpoint', 'mtime', 'progress', 'timestamp', 'title', 'uid'] MIN_PAGES_TO_CACHE = 3 MAX_PAGES_TO_CACHE = 5 +_datastore = None +created = dispatch.Signal() +updated = dispatch.Signal() +deleted = dispatch.Signal() + + class _Cache(object): __gtype_name__ = 'model_Cache' @@ -73,7 +82,7 @@ class BaseResultSet(object): """ def __init__(self, query, page_size): - self._total_count = -1 + self._total_count = -1 self._position = -1 self._query = query self._page_size = page_size @@ -108,8 +117,6 @@ class BaseResultSet(object): self._position = position def read(self): - logging.debug('ResultSet.read position: %r', self._position) - if self._position == -1: self.seek(0) @@ -142,7 +149,8 @@ class BaseResultSet(object): self._cache.append_all(entries) self._offset = offset - elif remaining_forward_entries <= 0 and remaining_backwards_entries > 0: + elif (remaining_forward_entries <= 0 and + remaining_backwards_entries > 0): # Add one page to the end of cache logging.debug('appending one more page, offset: %r', @@ -184,11 +192,10 @@ class BaseResultSet(object): objects_excess = len(self._cache) - cache_limit if objects_excess > 0: del self._cache[-objects_excess:] - else: - logging.debug('cache hit and no need to grow the cache') return self._cache[self._position - self._offset] + class DatastoreResultSet(BaseResultSet): """Encapsulates the result of a query on the datastore """ @@ -216,6 +223,7 @@ class DatastoreResultSet(BaseResultSet): return entries, total_count + class InplaceResultSet(BaseResultSet): """Encapsulates the result of a query on a mount point """ @@ -223,7 +231,9 @@ class InplaceResultSet(BaseResultSet): BaseResultSet.__init__(self, query, page_size) self._mount_point = mount_point self._file_list = None - self._pending_directories = 0 + self._pending_directories = [] + self._visited_directories = [] + self._pending_files = [] self._stopped = False query_text = query.get('query', '') @@ -246,15 +256,27 @@ class InplaceResultSet(BaseResultSet): self._mime_types = query.get('mime_type', []) + self._sort = query.get('order_by', ['+timestamp'])[0] + def setup(self): self._file_list = [] - self._recurse_dir(self._mount_point) + self._pending_directories = [self._mount_point] + self._visited_directories = [] + self._pending_files = [] + gobject.idle_add(self._scan) def stop(self): self._stopped = True def setup_ready(self): - self._file_list.sort(lambda a, b: b[2] - a[2]) + if self._sort[1:] == 'filesize': + keygetter = itemgetter(3) + else: + # timestamp + keygetter = itemgetter(2) + self._file_list.sort(lambda a, b: cmp(b, a), + key=keygetter, + reverse=(self._sort[0] == '-')) self.ready.send(self) def find(self, query): @@ -267,13 +289,13 @@ class InplaceResultSet(BaseResultSet): t = time.time() offset = int(query.get('offset', 0)) - limit = int(query.get('limit', len(self._file_list))) + limit = int(query.get('limit', len(self._file_list))) total_count = len(self._file_list) files = self._file_list[offset:offset + limit] entries = [] - for file_path, stat, mtime_ in files: + for file_path, stat, mtime_, size_ in files: metadata = _get_file_metadata(file_path, stat) metadata['mountpoint'] = self._mount_point entries.append(metadata) @@ -282,75 +304,115 @@ class InplaceResultSet(BaseResultSet): return entries, total_count - def _recurse_dir(self, dir_path): - self._pending_directories += 1 - gobject.idle_add(self._idle_recurse_dir, dir_path) + def _scan(self): + if self._stopped: + return False - def _idle_recurse_dir(self, dir_path): - try: - self._real_recurse_dir(dir_path) - finally: - self._pending_directories -= 1 - if self._pending_directories == 0: - self.setup_ready() + self.progress.send(self) - def _real_recurse_dir(self, dir_path): - if self._stopped: - return + if self._pending_files: + self._scan_a_file() + return True + + if self._pending_directories: + self._scan_a_directory() + return True + + self.setup_ready() + self._visited_directories = [] + return False + + def _scan_a_file(self): + full_path = self._pending_files.pop(0) try: - dirs = os.listdir(dir_path) - except Exception: - logging.exception('Error reading directory %r', dir_path) - dirs = [] + stat = os.lstat(full_path) + except OSError, e: + if e.errno != errno.ENOENT: + logging.exception( + 'Error reading metadata of file %r', full_path) + return + + if S_IFMT(stat.st_mode) == S_IFLNK: + try: + link = os.readlink(full_path) + except OSError, e: + logging.exception( + 'Error reading target of link %r', full_path) + return + + if not os.path.abspath(link).startswith(self._mount_point): + return - for entry in dirs: - if entry.startswith('.'): - continue - full_path = dir_path + '/' + entry try: stat = os.stat(full_path) - if S_IFMT(stat.st_mode) == S_IFDIR: - self._recurse_dir(full_path) - elif S_IFMT(stat.st_mode) == S_IFREG: - add_to_list = True + except OSError, e: + if e.errno != errno.ENOENT: + logging.exception( + 'Error reading metadata of linked file %r', full_path) + return + + if S_IFMT(stat.st_mode) == S_IFDIR: + id_tuple = stat.st_ino, stat.st_dev + if not id_tuple in self._visited_directories: + self._visited_directories.append(id_tuple) + self._pending_directories.append(full_path) + return + + if S_IFMT(stat.st_mode) != S_IFREG: + return - if self._regex is not None and \ - not self._regex.match(full_path): - add_to_list = False + if self._regex is not None and \ + not self._regex.match(full_path): + return - if None not in [self._date_start, self._date_end] and \ - (stat.st_mtime < self._date_start or - stat.st_mtime > self._date_end): - add_to_list = False + if self._date_start is not None and stat.st_mtime < self._date_start: + return - if self._mime_types: - mime_type = gio.content_type_guess(filename=full_path) - if mime_type not in self._mime_types: - add_to_list = False + if self._date_end is not None and stat.st_mtime > self._date_end: + return - if add_to_list: - file_info = (full_path, stat, int(stat.st_mtime)) - self._file_list.append(file_info) + if self._mime_types: + mime_type = gio.content_type_guess(filename=full_path) + if mime_type not in self._mime_types: + return - self.progress.send(self) + file_info = (full_path, stat, int(stat.st_mtime), stat.st_size) + self._file_list.append(file_info) + + return + + def _scan_a_directory(self): + dir_path = self._pending_directories.pop(0) + + try: + entries = os.listdir(dir_path) + except OSError, e: + if e.errno != errno.EACCES: + logging.exception('Error reading directory %r', dir_path) + return + + for entry in entries: + if entry.startswith('.'): + continue + self._pending_files.append(dir_path + '/' + entry) + return - except Exception: - logging.exception('Error reading file %r', full_path) def _get_file_metadata(path, stat): client = gconf.client_get_default() return {'uid': path, 'title': os.path.basename(path), 'timestamp': stat.st_mtime, + 'filesize': stat.st_size, 'mime_type': gio.content_type_guess(filename=path), 'activity': '', 'activity_id': '', 'icon-color': client.get_string('/desktop/sugar/user/color'), 'description': path} -_datastore = None + def _get_datastore(): global _datastore if _datastore is None: @@ -364,15 +426,19 @@ def _get_datastore(): return _datastore + def _datastore_created_cb(object_id): created.send(None, object_id=object_id) + def _datastore_updated_cb(object_id): updated.send(None, object_id=object_id) + def _datastore_deleted_cb(object_id): deleted.send(None, object_id=object_id) + def find(query_, page_size): """Returns a ResultSet """ @@ -387,6 +453,7 @@ def find(query_, page_size): else: return InplaceResultSet(query, page_size, mount_points[0]) + def _get_mount_point(path): dir_path = os.path.dirname(path) while True: @@ -395,6 +462,7 @@ def _get_mount_point(path): else: dir_path = dir_path.rsplit(os.sep, 1)[0] + def get(object_id): """Returns the metadata for an object """ @@ -407,6 +475,7 @@ def get(object_id): metadata['mountpoint'] = '/' return metadata + def get_file(object_id): """Returns the file for an object """ @@ -421,6 +490,7 @@ def get_file(object_id): else: return None + def get_file_size(object_id): """Return the file size for an object """ @@ -436,12 +506,14 @@ def get_file_size(object_id): return 0 + def get_unique_values(key): """Returns a list with the different values a property has taken """ empty_dict = dbus.Dictionary({}, signature='ss') return _get_datastore().get_uniquevaluesfor(key, empty_dict) + def delete(object_id): """Removes an object from persistent storage """ @@ -451,6 +523,7 @@ def delete(object_id): else: _get_datastore().delete(object_id) + def copy(metadata, mount_point): """Copies an object to another mount point """ @@ -462,6 +535,7 @@ def copy(metadata, mount_point): return write(metadata, file_path, transfer_ownership=False) + def write(metadata, file_path='', update_mtime=True, transfer_ownership=True): """Creates or updates an entry for that id """ @@ -496,6 +570,7 @@ def write(metadata, file_path='', update_mtime=True, transfer_ownership=True): return object_id + def _get_file_name(title, mime_type): file_name = title @@ -520,11 +595,12 @@ def _get_file_name(title, mime_type): return file_name + def _get_unique_file_name(mount_point, file_name): if os.path.exists(os.path.join(mount_point, file_name)): i = 1 + name, extension = os.path.splitext(file_name) while len(file_name) <= 255: - name, extension = os.path.splitext(file_name) file_name = name + '_' + str(i) + extension if not os.path.exists(os.path.join(mount_point, file_name)): break @@ -532,10 +608,7 @@ def _get_unique_file_name(mount_point, file_name): return file_name + def is_editable(metadata): mountpoint = metadata.get('mountpoint', '/') return mountpoint == '/' - -created = dispatch.Signal() -updated = dispatch.Signal() -deleted = dispatch.Signal() diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py index 49af3e6..ecb8ecf 100644 --- a/src/jarabe/journal/objectchooser.py +++ b/src/jarabe/journal/objectchooser.py @@ -29,14 +29,13 @@ from jarabe.journal.listmodel import ListModel 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])) + 'response': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([int])), } def __init__(self, parent=None, what_filter=''): @@ -136,6 +135,7 @@ class ObjectChooser(gtk.Window): visible = event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED self._list_view.set_is_visible(visible) + class TitleBox(VolumesToolbar): __gtype_name__ = 'TitleBox' @@ -162,6 +162,7 @@ class TitleBox(VolumesToolbar): self.insert(tool_item, -1) tool_item.show() + class ChooserListView(BaseListView): __gtype_name__ = 'ChooserListView' @@ -187,7 +188,7 @@ class ChooserListView(BaseListView): if event.window != tree_view.get_bin_window(): return False - pos = tree_view.get_path_at_pos(event.x, event.y) + pos = tree_view.get_path_at_pos(int(event.x), int(event.y)) if pos is None: return False @@ -196,4 +197,3 @@ class ChooserListView(BaseListView): self.emit('entry-activated', uid) return False - diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index 0e7702d..9ae1afb 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -28,20 +28,19 @@ from sugar.graphics.icon import Icon from sugar.graphics.xocolor import XoColor from sugar import mime -from jarabe.model import bundleregistry from jarabe.model import friends from jarabe.model import filetransfer from jarabe.model import mimeregistry from jarabe.journal import misc from jarabe.journal import model + class ObjectPalette(Palette): __gtype_name__ = 'ObjectPalette' __gsignals__ = { - 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), } @@ -54,7 +53,7 @@ class ObjectPalette(Palette): activity_icon.props.file = misc.get_icon_name(metadata) activity_icon.props.xo_color = misc.get_icon_color(metadata) - if metadata.has_key('title'): + if 'title' in metadata: title = gobject.markup_escape_text(metadata['title']) else: title = _('Untitled') @@ -62,22 +61,29 @@ class ObjectPalette(Palette): Palette.__init__(self, primary_text=title, icon=activity_icon) - if metadata.get('activity_id', ''): - resume_label = _('Resume') - resume_with_label = _('Resume with') - else: - resume_label = _('Start') - resume_with_label = _('Start with') - menu_item = MenuItem(resume_label, 'activity-start') - menu_item.connect('activate', self.__start_activate_cb) - self.menu.append(menu_item) - menu_item.show() + if misc.get_activities(metadata) or misc.is_bundle(metadata): + if metadata.get('activity_id', ''): + resume_label = _('Resume') + resume_with_label = _('Resume with') + else: + resume_label = _('Start') + resume_with_label = _('Start with') + menu_item = MenuItem(resume_label, 'activity-start') + menu_item.connect('activate', self.__start_activate_cb) + self.menu.append(menu_item) + menu_item.show() - menu_item = MenuItem(resume_with_label, 'activity-start') - self.menu.append(menu_item) - menu_item.show() - start_with_menu = StartWithMenu(self._metadata) - menu_item.set_submenu(start_with_menu) + menu_item = MenuItem(resume_with_label, 'activity-start') + self.menu.append(menu_item) + menu_item.show() + start_with_menu = StartWithMenu(self._metadata) + menu_item.set_submenu(start_with_menu) + + else: + menu_item = MenuItem(_('No activity to start entry')) + menu_item.set_sensitive(False) + self.menu.append(menu_item) + menu_item.show() client = gconf.client_get_default() color = XoColor(client.get_string('/desktop/sugar/user/color')) @@ -128,11 +134,6 @@ class ObjectPalette(Palette): self._temp_file_path = None def __erase_activate_cb(self, menu_item): - registry = bundleregistry.get_registry() - - bundle = misc.get_bundle(self._metadata) - if bundle is not None and registry.is_installed(bundle): - registry.uninstall(bundle) model.delete(self._metadata['uid']) def __detail_activate_cb(self, menu_item): @@ -152,12 +153,13 @@ class ObjectPalette(Palette): filetransfer.start_transfer(buddy, file_name, title, description, mime_type) + class FriendsMenu(gtk.Menu): __gtype_name__ = 'JournalFriendsMenu' __gsignals__ = { - 'friend-selected' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([object])), + 'friend-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), } def __init__(self): diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py index 74b974c..2d842f1 100644 --- a/src/jarabe/journal/volumestoolbar.py +++ b/src/jarabe/journal/volumestoolbar.py @@ -15,6 +15,8 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import logging +import os +import statvfs from gettext import gettext as _ import gobject @@ -25,17 +27,20 @@ import gconf from sugar.graphics.radiotoolbutton import RadioToolButton from sugar.graphics.palette import Palette from sugar.graphics.xocolor import XoColor +from sugar import env from jarabe.journal import model from jarabe.view.palettes import VolumePalette + class VolumesToolbar(gtk.Toolbar): __gtype_name__ = 'VolumesToolbar' __gsignals__ = { - 'volume-changed': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([str])) + 'volume-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), } def __init__(self): @@ -44,7 +49,6 @@ class VolumesToolbar(gtk.Toolbar): self._mount_removed_hid = None button = JournalButton() - button.set_palette(Palette(_('Journal'))) button.connect('toggled', self._button_toggled_cb) self.insert(button, 0) button.show() @@ -61,10 +65,10 @@ class VolumesToolbar(gtk.Toolbar): def _set_up_volumes(self): volume_monitor = gio.volume_monitor_get() - self._mount_added_hid = \ - volume_monitor.connect('mount-added', self.__mount_added_cb) - self._mount_removed_hid = \ - volume_monitor.connect('mount-removed', self.__mount_removed_cb) + self._mount_added_hid = volume_monitor.connect('mount-added', + self.__mount_added_cb) + self._mount_removed_hid = volume_monitor.connect('mount-removed', + self.__mount_removed_cb) for mount in volume_monitor.get_mounts(): self._add_button(mount) @@ -81,6 +85,7 @@ class VolumesToolbar(gtk.Toolbar): button = VolumeButton(mount) button.props.group = self._volume_buttons[0] button.connect('toggled', self._button_toggled_cb) + button.connect('volume-error', self.__volume_error_cb) position = self.get_item_index(self._volume_buttons[-1]) + 1 self.insert(button, position) button.show() @@ -90,6 +95,9 @@ class VolumesToolbar(gtk.Toolbar): if len(self.get_children()) > 1: self.show() + def __volume_error_cb(self, button, strerror, severity): + self.emit('volume-error', strerror, severity) + def _button_toggled_cb(self, button): if button.props.active: self.emit('volume-changed', button.mount_point) @@ -122,7 +130,13 @@ class VolumesToolbar(gtk.Toolbar): button = self._get_button_for_mount(mount) button.props.active = True + class BaseButton(RadioToolButton): + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + def __init__(self, mount_point): RadioToolButton.__init__(self) @@ -133,11 +147,26 @@ class BaseButton(RadioToolButton): 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): + def _drag_data_received_cb(self, widget, drag_context, x, y, + selection_data, info, timestamp): object_id = selection_data.data metadata = model.get(object_id) - model.copy(metadata, self.mount_point) + file_path = model.get_file(metadata['uid']) + if not file_path or not os.path.exists(file_path): + logging.warn('Entries without a file cannot be copied.') + self.emit('volume-error', + _('Entries without a file cannot be copied.'), + _('Warning')) + return + + try: + model.copy(metadata, self.mount_point) + except IOError, e: + logging.exception('Error while copying the entry. %s', e.strerror) + self.emit('volume-error', + _('Error while copying the entry. %s') % e.strerror, + _('Error')) + class VolumeButton(BaseButton): def __init__(self, mount): @@ -169,6 +198,7 @@ class VolumeButton(BaseButton): #palette.set_group_id('frame') return palette + class JournalButton(BaseButton): def __init__(self): BaseButton.__init__(self, mount_point='/') @@ -179,3 +209,36 @@ class JournalButton(BaseButton): color = XoColor(client.get_string('/desktop/sugar/user/color')) self.props.xo_color = color + def create_palette(self): + palette = JournalButtonPalette(self) + return palette + + +class JournalButtonPalette(Palette): + + def __init__(self, mount): + Palette.__init__(self, _('Journal')) + 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) + + def __popup_cb(self, palette): + 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/model/Makefile.am b/src/jarabe/model/Makefile.am index e9f0700..92e8712 100644 --- a/src/jarabe/model/Makefile.am +++ b/src/jarabe/model/Makefile.am @@ -1,5 +1,6 @@ sugardir = $(pythondir)/jarabe/model sugar_PYTHON = \ + adhoc.py \ __init__.py \ buddy.py \ bundleregistry.py \ @@ -7,12 +8,12 @@ sugar_PYTHON = \ friends.py \ invites.py \ olpcmesh.py \ - owner.py \ - mimeregistry.py \ + mimeregistry.py \ neighborhood.py \ network.py \ notifications.py \ shell.py \ screen.py \ session.py \ - sound.py + sound.py \ + telepathyclient.py diff --git a/src/jarabe/model/__init__.py b/src/jarabe/model/__init__.py index a9dd95a..85f6a24 100644 --- a/src/jarabe/model/__init__.py +++ b/src/jarabe/model/__init__.py @@ -13,4 +13,3 @@ # 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/adhoc.py b/src/jarabe/model/adhoc.py new file mode 100644 index 0000000..8842a5c --- /dev/null +++ b/src/jarabe/model/adhoc.py @@ -0,0 +1,294 @@ +# Copyright (C) 2010 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 dbus +import gobject + +from jarabe.model import network +from jarabe.model.network import Settings +from sugar.util import unique_id +from jarabe.model.network import IP4Config + + +_NM_SERVICE = 'org.freedesktop.NetworkManager' +_NM_IFACE = 'org.freedesktop.NetworkManager' +_NM_PATH = '/org/freedesktop/NetworkManager' +_NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' +_NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +_NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +_NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' + +_adhoc_manager_instance = None + + +def get_adhoc_manager_instance(): + global _adhoc_manager_instance + if _adhoc_manager_instance is None: + _adhoc_manager_instance = AdHocManager() + return _adhoc_manager_instance + + +class AdHocManager(gobject.GObject): + """To mimic the mesh behavior on devices where mesh hardware is + not available we support the creation of an Ad-hoc network on + three channels 1, 6, 11. If Sugar sees no "known" network when it + starts, it does autoconnect to an Ad-hoc network. + + """ + + __gsignals__ = { + 'members-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + } + + _AUTOCONNECT_TIMEOUT = 30 + _CHANNEL_1 = 1 + _CHANNEL_6 = 6 + _CHANNEL_11 = 11 + + def __init__(self): + gobject.GObject.__init__(self) + + self._bus = dbus.SystemBus() + self._device = None + self._idle_source = 0 + self._listening_called = 0 + self._device_state = network.DEVICE_STATE_UNKNOWN + + self._current_channel = None + self._networks = {self._CHANNEL_1: None, + self._CHANNEL_6: None, + self._CHANNEL_11: None} + + def start_listening(self, device): + self._listening_called += 1 + if self._listening_called > 1: + raise RuntimeError('The start listening method can' \ + ' only be called once.') + + self._device = device + props = dbus.Interface(device, dbus.PROPERTIES_IFACE) + self._device_state = props.Get(_NM_DEVICE_IFACE, 'State') + + self._bus.add_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=_NM_WIRELESS_IFACE) + + def stop_listening(self): + self._bus.remove_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=_NM_WIRELESS_IFACE) + + def __device_state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update_state() + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveAccessPoint' in properties and \ + properties['ActiveAccessPoint'] != '/': + active_ap = self._bus.get_object(_NM_SERVICE, + properties['ActiveAccessPoint']) + props = dbus.Interface(active_ap, dbus.PROPERTIES_IFACE) + props.GetAll(_NM_ACCESSPOINT_IFACE, byte_arrays=True, + reply_handler=self.__get_all_ap_props_reply_cb, + error_handler=self.__get_all_ap_props_error_cb) + + def __get_all_ap_props_reply_cb(self, properties): + if properties['Mode'] == network.NM_802_11_MODE_ADHOC and \ + 'Frequency' in properties: + frequency = properties['Frequency'] + self._current_channel = network.frequency_to_channel(frequency) + else: + self._current_channel = None + self._update_state() + + def __get_all_ap_props_error_cb(self, err): + logging.error('Error getting the access point properties: %s', err) + + def _update_state(self): + self.emit('state-changed', self._current_channel, self._device_state) + + def _have_configured_connections(self): + return len(network.get_settings().connections) > 0 + + def autoconnect(self): + """Autoconnect to an Ad-hoc network""" + if self._device_state != network.DEVICE_STATE_DISCONNECTED: + return + elif self._have_configured_connections(): + self._autoconnect_adhoc_timer() + else: + self._autoconnect_adhoc() + + def _autoconnect_adhoc_timer(self): + """Start a timer which basically looks for 30 seconds of inactivity + on the device, then does autoconnect to an Ad-hoc network. + + """ + if self._idle_source != 0: + gobject.source_remove(self._idle_source) + self._idle_source = gobject.timeout_add_seconds( \ + self._AUTOCONNECT_TIMEOUT, self.__idle_check_cb) + + def __idle_check_cb(self): + if self._device_state == network.DEVICE_STATE_DISCONNECTED: + logging.debug('Connect to Ad-hoc network due to inactivity.') + self._autoconnect_adhoc() + return False + + def _autoconnect_adhoc(self): + """First we try if there is an Ad-hoc network that is used by other + learners in the area, if not we default to channel 1. + + """ + if self._networks[self._CHANNEL_1] is not None: + self._connect(self._CHANNEL_1) + elif self._networks[self._CHANNEL_6] is not None: + self._connect(self._CHANNEL_6) + elif self._networks[self._CHANNEL_11] is not None: + self._connect(self._CHANNEL_11) + else: + self._connect(self._CHANNEL_1) + + def activate_channel(self, channel): + """Activate a sugar Ad-hoc network. + + Keyword arguments: + channel -- Channel to connect to (should be 1, 6, 11) + + """ + self._connect(channel) + + def _connect(self, channel): + name = 'Ad-hoc Network %d' % channel + connection = network.find_connection_by_ssid(name) + if connection is None: + settings = Settings() + settings.connection.id = name + settings.connection.uuid = unique_id() + settings.connection.type = '802-11-wireless' + settings.wireless.ssid = dbus.ByteArray(name) + settings.wireless.band = 'bg' + settings.wireless.channel = channel + settings.wireless.mode = 'adhoc' + settings.ip4_config = IP4Config() + settings.ip4_config.method = 'link-local' + + connection = network.add_connection(name, settings) + + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + + netmgr.ActivateConnection(network.SETTINGS_SERVICE, + connection.path, + self._device.object_path, + '/', + reply_handler=self.__activate_reply_cb, + error_handler=self.__activate_error_cb) + + def deactivate_active_channel(self): + """Deactivate the current active channel.""" + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + netmgr_props.Get(_NM_IFACE, 'ActiveConnections', \ + reply_handler=self.__get_active_connections_reply_cb, + error_handler=self.__get_active_connections_error_cb) + + def __get_active_connections_reply_cb(self, active_connections_o): + for connection_o in active_connections_o: + obj = self._bus.get_object(_NM_IFACE, connection_o) + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) + state = props.Get(_NM_ACTIVE_CONN_IFACE, 'State') + if state == network.NM_ACTIVE_CONNECTION_STATE_ACTIVATED: + access_point_o = props.Get(_NM_ACTIVE_CONN_IFACE, + 'SpecificObject') + if access_point_o != '/': + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + netmgr.DeactivateConnection(connection_o) + + def __get_active_connections_error_cb(self, err): + logging.error('Error getting the active connections: %s', err) + + def __activate_reply_cb(self, connection): + logging.debug('Ad-hoc network created: %s', connection) + + def __activate_error_cb(self, err): + logging.error('Failed to create Ad-hoc network: %s', err) + + def add_access_point(self, access_point): + """Add an access point to a network and notify the view to idicate + the member change. + + Keyword arguments: + access_point -- Access Point + + """ + if access_point.name.endswith(' 1'): + self._networks[self._CHANNEL_1] = access_point + self.emit('members-changed', self._CHANNEL_1, True) + elif access_point.name.endswith(' 6'): + self._networks[self._CHANNEL_6] = access_point + self.emit('members-changed', self._CHANNEL_6, True) + elif access_point.name.endswith('11'): + self._networks[self._CHANNEL_11] = access_point + self.emit('members-changed', self._CHANNEL_11, True) + + def is_sugar_adhoc_access_point(self, ap_object_path): + """Checks whether an access point is part of a sugar Ad-hoc network. + + Keyword arguments: + ap_object_path -- Access Point object path + + Return: Boolean + + """ + for access_point in self._networks.values(): + if access_point is not None: + if access_point.model.object_path == ap_object_path: + return True + return False + + def remove_access_point(self, ap_object_path): + """Remove an access point from a sugar Ad-hoc network. + + Keyword arguments: + ap_object_path -- Access Point object path + + """ + for channel in self._networks: + if self._networks[channel] is not None: + if self._networks[channel].model.object_path == ap_object_path: + self.emit('members-changed', channel, False) + self._networks[channel] = None + break diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py index 5978bae..c580e68 100644 --- a/src/jarabe/model/buddy.py +++ b/src/jarabe/model/buddy.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> # # 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 @@ -14,169 +15,220 @@ # 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 logging + import gobject +import gconf +import dbus +from telepathy.client import Connection +from telepathy.interfaces import CONNECTION + +from sugar.graphics.xocolor import XoColor +from sugar.profile import get_profile + +from jarabe.util.telepathy import connection_watcher + -_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, - ([])), - 'tags-changed': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])), - '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) +CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +_owner_instance = None + + +class BaseBuddyModel(gobject.GObject): + __gtype_name__ = 'SugarBaseBuddyModel' + + def __init__(self, **kwargs): + self._key = None + self._nick = None self._color = None self._tags = 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) + self._current_activity = None - def get_key(self): - return self._key + gobject.GObject.__init__(self, **kwargs) def get_nick(self): return self._nick + def set_nick(self, nick): + self._nick = nick + + nick = gobject.property(type=object, getter=get_nick, setter=set_nick) + + def get_key(self): + return self._key + + def set_key(self, key): + self._key = key + + key = gobject.property(type=object, getter=get_key, setter=set_key) + def get_color(self): return self._color + def set_color(self, color): + self._color = color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + def get_tags(self): return self._tags - def get_buddy(self): - return self._buddy + tags = gobject.property(type=object, getter=get_tags) + + def get_current_activity(self): + return self._current_activity + + def set_current_activity(self, current_activity): + if self._current_activity != current_activity: + self._current_activity = current_activity + self.notify('current-activity') + + current_activity = gobject.property(type=object, + getter=get_current_activity, + setter=set_current_activity) + + def is_owner(self): + raise NotImplementedError + + +class OwnerBuddyModel(BaseBuddyModel): + __gtype_name__ = 'SugarOwnerBuddyModel' + + def __init__(self): + BaseBuddyModel.__init__(self) + + client = gconf.client_get_default() + self.props.nick = client.get_string('/desktop/sugar/user/nick') + color = client.get_string('/desktop/sugar/user/color') + self.props.color = XoColor(color) + + self.props.key = get_profile().pubkey + + self.connect('notify::nick', self.__property_changed_cb) + self.connect('notify::color', self.__property_changed_cb) + self.connect('notify::current-activity', + self.__current_activity_changed_cb) + + bus = dbus.SessionBus() + bus.add_signal_receiver( + self.__name_owner_changed_cb, + signal_name='NameOwnerChanged', + dbus_interface='org.freedesktop.DBus') + + bus_object = bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH) + for service in bus_object.ListNames( + dbus_interface=dbus.BUS_DAEMON_IFACE): + if service.startswith(CONNECTION + '.'): + path = '/%s' % service.replace('.', '/') + Connection(service, path, bus, + ready_handler=self.__connection_ready_cb) + + def __connection_ready_cb(self, connection): + self._sync_properties_on_connection(connection) + + def __name_owner_changed_cb(self, name, old, new): + if name.startswith(CONNECTION + '.') and not old and new: + path = '/' + name.replace('.', '/') + Connection(name, path, ready_handler=self.__connection_ready_cb) + + def __property_changed_cb(self, buddy, pspec): + self._sync_properties() + + def __current_activity_changed_cb(self, buddy, pspec): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): + if self.props.current_activity is not None: + activity_id = self.props.current_activity.activity_id + room_handle = self.props.current_activity.room_handle + else: + activity_id = '' + room_handle = 0 + + connection[CONNECTION_INTERFACE_BUDDY_INFO].SetCurrentActivity( + activity_id, + room_handle, + reply_handler=self.__set_current_activity_cb, + error_handler=self.__error_handler_cb) + + def __set_current_activity_cb(self): + logging.debug('__set_current_activity_cb') + + def _sync_properties(self): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): + self._sync_properties_on_connection(connection) + + def _sync_properties_on_connection(self, connection): + if CONNECTION_INTERFACE_BUDDY_INFO in connection: + properties = {} + if self.props.key is not None: + properties['key'] = dbus.ByteArray(self.props.key) + if self.props.color is not None: + properties['color'] = self.props.color.to_string() + + logging.debug('calling SetProperties with %r', properties) + connection[CONNECTION_INTERFACE_BUDDY_INFO].SetProperties( + properties, + reply_handler=self.__set_properties_cb, + error_handler=self.__error_handler_cb) + + def __set_properties_cb(self): + logging.debug('__set_properties_cb') + + def __error_handler_cb(self, error): + raise RuntimeError(error) + + def __connection_added_cb(self, conn_watcher, connection): + self._sync_properties_on_connection(connection) def is_owner(self): - if not self._buddy: - return False - return self._buddy.props.owner + return True + + +def get_owner_instance(): + global _owner_instance + if _owner_instance is None: + _owner_instance = OwnerBuddyModel() + return _owner_instance + + +class BuddyModel(BaseBuddyModel): + __gtype_name__ = 'SugarBuddyModel' + + def __init__(self, **kwargs): + + self._account = None + self._contact_id = None + self._handle = None + + BaseBuddyModel.__init__(self, **kwargs) - def is_present(self): - if self._buddy: - return True + def is_owner(self): 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._tags = self._buddy.props.tags - 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()) - if 'tags' in keys: - self._tags = self._buddy.props.tags - self.emit('tags-changed', self.get_tags()) - - 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') + def get_account(self): + return self._account + + def set_account(self, account): + self._account = account + + account = gobject.property(type=object, getter=get_account, + setter=set_account) + + def get_contact_id(self): + return self._contact_id + + def set_contact_id(self, contact_id): + self._contact_id = contact_id + + contact_id = gobject.property(type=object, getter=get_contact_id, + setter=set_contact_id) + + def get_handle(self): + return self._handle + + def set_handle(self, handle): + self._handle = handle + + handle = gobject.property(type=object, getter=get_handle, + setter=set_handle) diff --git a/src/jarabe/model/bundleregistry.py b/src/jarabe/model/bundleregistry.py index 86a2738..84d55c0 100644 --- a/src/jarabe/model/bundleregistry.py +++ b/src/jarabe/model/bundleregistry.py @@ -17,14 +17,15 @@ import os import logging -import traceback +import gconf import gobject import gio import simplejson from sugar.bundle.activitybundle import ActivityBundle from sugar.bundle.contentbundle import ContentBundle +from sugar.bundle.bundleversion import NormalizedVersion from jarabe.journal.journalentrybundle import JournalEntryBundle from sugar.bundle.bundle import MalformedBundleException, \ AlreadyInstalledException, RegistrationException @@ -33,16 +34,20 @@ from sugar import env from jarabe import config from jarabe.model import mimeregistry + +_instance = None + + class BundleRegistry(gobject.GObject): """Tracks the available activity bundles""" __gsignals__ = { - 'bundle-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])), + 'bundle-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), 'bundle-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), 'bundle-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])) + ([gobject.TYPE_PYOBJECT])), } def __init__(self): @@ -66,6 +71,14 @@ class BundleRegistry(gobject.GObject): self._last_defaults_mtime = -1 self._favorite_bundles = {} + client = gconf.client_get_default() + self._protected_activities = client.get_list( + '/desktop/sugar/protected_activities', + gconf.VALUE_STRING) + + if self._protected_activities is None: + self._protected_activities = [] + try: self._load_favorites() except Exception: @@ -144,14 +157,16 @@ class BundleRegistry(gobject.GObject): return for bundle_id in default_activities: - max_version = -1 + max_version = '0' for bundle in self._bundles: if bundle.get_bundle_id() == bundle_id and \ - max_version < bundle.get_activity_version(): + NormalizedVersion(max_version) < \ + NormalizedVersion(bundle.get_activity_version()): max_version = bundle.get_activity_version() key = self._get_favorite_key(bundle_id, max_version) - if max_version > -1 and key not in self._favorite_bundles: + if NormalizedVersion(max_version) > NormalizedVersion('0') and \ + key not in self._favorite_bundles: self._favorite_bundles[key] = None logging.debug('After merging: %r', self._favorite_bundles) @@ -164,7 +179,7 @@ class BundleRegistry(gobject.GObject): if bundle.get_bundle_id() == bundle_id: return bundle return None - + def __iter__(self): return self._bundles.__iter__() @@ -185,19 +200,18 @@ class BundleRegistry(gobject.GObject): if os.path.isdir(bundle_dir): bundles[bundle_dir] = os.stat(bundle_dir).st_mtime except Exception: - logging.error('Error while processing installed activity ' \ - 'bundle %s:\n%s' % \ - (bundle_dir, traceback.format_exc())) + logging.exception('Error while processing installed activity' + ' bundle %s:', bundle_dir) bundle_dirs = bundles.keys() bundle_dirs.sort(lambda d1, d2: cmp(bundles[d1], bundles[d2])) for folder in bundle_dirs: try: self._add_bundle(folder) - except Exception, e: - logging.error('Error while processing installed activity ' \ - 'bundle %s:\n%s' % \ - (folder, traceback.format_exc())) + except: + # pylint: disable=W0702 + logging.exception('Error while processing installed activity' + ' bundle %s:', folder) def add_bundle(self, bundle_path, install_mime_type=False): bundle = self._add_bundle(bundle_path, install_mime_type) @@ -224,8 +238,8 @@ class BundleRegistry(gobject.GObject): installed = self.get_bundle(bundle_id) if installed is not None: - if installed.get_activity_version() >= \ - bundle.get_activity_version(): + if NormalizedVersion(installed.get_activity_version()) >= \ + NormalizedVersion(bundle.get_activity_version()): logging.debug('Skip old version for %s', bundle_id) return None else: @@ -251,8 +265,7 @@ class BundleRegistry(gobject.GObject): default_bundle = None for bundle in self._bundles: - if bundle.get_mime_types() and mime_type in bundle.get_mime_types(): - + if mime_type in (bundle.get_mime_types() or []): if bundle.get_bundle_id() == default_bundle_id: default_bundle = bundle elif self.get_default_for_type(mime_type) == \ @@ -267,10 +280,7 @@ class BundleRegistry(gobject.GObject): return result def get_default_for_type(self, mime_type): - if self._mime_defaults.has_key(mime_type): - return self._mime_defaults[mime_type] - else: - return None + return self._mime_defaults.get(mime_type) def _find_bundle(self, bundle_id, version): for bundle in self._bundles: @@ -302,10 +312,14 @@ class BundleRegistry(gobject.GObject): key = self._get_favorite_key(bundle_id, version) return key in self._favorite_bundles + def is_activity_protected(self, bundle_id): + return bundle_id in self._protected_activities + def set_bundle_position(self, bundle_id, version, x, y): key = self._get_favorite_key(bundle_id, version) if key not in self._favorite_bundles: - raise ValueError('Bundle %s %s not favorite' % (bundle_id, version)) + raise ValueError('Bundle %s %s not favorite' % + (bundle_id, version)) if self._favorite_bundles[key] is None: self._favorite_bundles[key] = {} @@ -346,19 +360,22 @@ class BundleRegistry(gobject.GObject): for installed_bundle in self._bundles: if bundle.get_bundle_id() == installed_bundle.get_bundle_id() and \ - bundle.get_activity_version() == \ - installed_bundle.get_activity_version(): + NormalizedVersion(bundle.get_activity_version()) <= \ + NormalizedVersion(installed_bundle.get_activity_version()): return True return False - def install(self, bundle, uid=None): + def install(self, bundle, uid=None, force_downgrade=False): activities_path = env.get_user_activities_path() for installed_bundle in self._bundles: if bundle.get_bundle_id() == installed_bundle.get_bundle_id() and \ - bundle.get_activity_version() <= \ - installed_bundle.get_activity_version(): - raise AlreadyInstalledException + NormalizedVersion(bundle.get_activity_version()) <= \ + NormalizedVersion(installed_bundle.get_activity_version()): + if not force_downgrade: + raise AlreadyInstalledException + else: + self.uninstall(installed_bundle, force=True) elif bundle.get_bundle_id() == installed_bundle.get_bundle_id(): self.uninstall(installed_bundle, force=True) @@ -376,7 +393,7 @@ class BundleRegistry(gobject.GObject): elif not self.add_bundle(install_path): raise RegistrationException - def uninstall(self, bundle, force=False): + def uninstall(self, bundle, force=False, delete_profile=False): # TODO treat ContentBundle in special way # needs rethinking while fixing ContentBundle support if isinstance(bundle, ContentBundle) or \ @@ -399,7 +416,7 @@ class BundleRegistry(gobject.GObject): install_path = act.get_path() - bundle.uninstall(install_path, force) + bundle.uninstall(install_path, force, delete_profile) if not self.remove_bundle(install_path): raise RegistrationException @@ -415,20 +432,17 @@ class BundleRegistry(gobject.GObject): try: self.uninstall(bundle, force=True) except Exception: - logging.error('Uninstall failed, still trying to install ' \ - 'newer bundle:\n' + \ - traceback.format_exc()) + logging.exception('Uninstall failed, still trying to install' + ' newer bundle:') else: - logging.warning('Unable to uninstall system activity, ' \ + logging.warning('Unable to uninstall system activity, ' 'installing upgraded version in user activities') self.install(bundle) -_instance = None def get_registry(): global _instance if not _instance: _instance = BundleRegistry() return _instance - diff --git a/src/jarabe/model/filetransfer.py b/src/jarabe/model/filetransfer.py index 46b246d..710c3a4 100644 --- a/src/jarabe/model/filetransfer.py +++ b/src/jarabe/model/filetransfer.py @@ -31,6 +31,8 @@ from sugar.presence import presenceservice from sugar import dispatch from jarabe.util.telepathy import connection_watcher +from jarabe.model import neighborhood + FT_STATE_NONE = 0 FT_STATE_PENDING = 1 @@ -51,14 +53,18 @@ FT_REASON_REMOTE_ERROR = 6 CHANNEL_TYPE_FILE_TRANSFER = \ 'org.freedesktop.Telepathy.Channel.Type.FileTransfer' +new_file_transfer = dispatch.Signal() + + # TODO Move to use splice_async() in Sugar 0.88 class StreamSplicer(gobject.GObject): - _CHUNK_SIZE = 10240 # 10K + _CHUNK_SIZE = 10240 # 10K __gsignals__ = { 'finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } + def __init__(self, input_stream, output_stream): gobject.GObject.__init__(self) @@ -104,6 +110,7 @@ class StreamSplicer(gobject.GObject): gobject.PRIORITY_LOW, user_data=data) + class BaseFileTransfer(gobject.GObject): def __init__(self, connection): @@ -140,11 +147,7 @@ class BaseFileTransfer(gobject.GObject): self.mime_type = props['ContentType'] handle = channel_properties.Get(CHANNEL, 'TargetHandle') - presence_service = presenceservice.get_instance() - self.buddy = presence_service.get_buddy_by_telepathy_handle( - self._connection.service_name, - self._connection.object_path, - handle) + self.buddy = neighborhood.get_model().get_buddy_by_handle(handle) def __transferred_bytes_changed_cb(self, transferred_bytes): logging.debug('__transferred_bytes_changed_cb %r', transferred_bytes) @@ -179,6 +182,7 @@ class BaseFileTransfer(gobject.GObject): def cancel(self): self.channel[CHANNEL].Close() + class IncomingFileTransfer(BaseFileTransfer): def __init__(self, connection, object_path, props): BaseFileTransfer.__init__(self, connection) @@ -223,6 +227,7 @@ class IncomingFileTransfer(BaseFileTransfer): self._splicer = StreamSplicer(input_stream, output_stream) self._splicer.start() + class OutgoingFileTransfer(BaseFileTransfer): def __init__(self, buddy, file_name, title, description, mime_type): @@ -240,20 +245,18 @@ class OutgoingFileTransfer(BaseFileTransfer): self._splicer = None self._output_stream = None - self.buddy = buddy.get_buddy() + self.buddy = buddy self.title = title self.file_size = os.stat(file_name).st_size self.description = description self.mime_type = mime_type def __connection_ready_cb(self, connection): - handle = self._get_buddy_handle() - requests = connection[CONNECTION_INTERFACE_REQUESTS] object_path, properties_ = requests.CreateChannel({ CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, - CHANNEL + '.TargetHandle': handle, + CHANNEL + '.TargetHandle': self.buddy.handle, CHANNEL_TYPE_FILE_TRANSFER + '.ContentType': self.mime_type, CHANNEL_TYPE_FILE_TRANSFER + '.Filename': self.title, CHANNEL_TYPE_FILE_TRANSFER + '.Size': self.file_size, @@ -267,21 +270,6 @@ class OutgoingFileTransfer(BaseFileTransfer): SOCKET_ADDRESS_TYPE_UNIX, SOCKET_ACCESS_CONTROL_LOCALHOST, '', byte_arrays=True) - def _get_buddy_handle(self): - object_path = self.buddy.object_path() - - bus = dbus.SessionBus() - remote_object = bus.get_object('org.laptop.Sugar.Presence', object_path) - ps_buddy = dbus.Interface(remote_object, - 'org.laptop.Sugar.Presence.Buddy') - - handles = ps_buddy.GetTelepathyHandles() - logging.debug('_get_buddy_handle %r', handles) - - bus_name, object_path, handle = handles[0] - - return handle - def __notify_state_cb(self, file_transfer, pspec): logging.debug('__notify_state_cb %r', self.props.state) if self.props.state == FT_STATE_OPEN: @@ -303,6 +291,7 @@ class OutgoingFileTransfer(BaseFileTransfer): def cancel(self): self.channel[CHANNEL].Close() + def _new_channels_cb(connection, channels): for object_path, props in channels: if props[CHANNEL + '.ChannelType'] == CHANNEL_TYPE_FILE_TRANSFER and \ @@ -314,35 +303,39 @@ def _new_channels_cb(connection, channels): object_path, props) new_file_transfer.send(None, file_transfer=incoming_file_transfer) + def _monitor_connection(connection): logging.debug('connection added %r', connection) connection[CONNECTION_INTERFACE_REQUESTS].connect_to_signal('NewChannels', lambda channels: _new_channels_cb(connection, channels)) -def _connection_addded_cb(conn_watcher, connection): + +def _connection_added_cb(conn_watcher, connection): _monitor_connection(connection) + def _connection_removed_cb(conn_watcher, connection): logging.debug('connection removed %r', connection) -_conn_watcher = None def init(): - global _conn_watcher - _conn_watcher = connection_watcher.ConnectionWatcher() - _conn_watcher.connect('connection-added', _connection_addded_cb) - _conn_watcher.connect('connection-removed', _connection_removed_cb) + conn_watcher = connection_watcher.get_instance() + conn_watcher.connect('connection-added', _connection_added_cb) + conn_watcher.connect('connection-removed', _connection_removed_cb) - for connection in _conn_watcher.get_connections(): + for connection in conn_watcher.get_connections(): _monitor_connection(connection) + def start_transfer(buddy, file_name, title, description, mime_type): outgoing_file_transfer = OutgoingFileTransfer(buddy, file_name, title, description, mime_type) new_file_transfer.send(None, file_transfer=outgoing_file_transfer) + def file_transfer_available(): - for connection in _conn_watcher.get_connections(): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): properties_iface = connection[dbus.PROPERTIES_IFACE] properties = properties_iface.GetAll(CONNECTION_INTERFACE_REQUESTS) @@ -359,18 +352,17 @@ def file_transfer_available(): return False -new_file_transfer = dispatch.Signal() if __name__ == '__main__': import tempfile - input_stream = gio.File('/home/tomeu/isos/Soas2-200904031934.iso').read() - output_stream = gio.File(tempfile.mkstemp()[1]).append_to() + test_file_name = '/home/tomeu/isos/Soas2-200904031934.iso' + test_input_stream = gio.File(test_file_name).read() + test_output_stream = gio.File(tempfile.mkstemp()[1]).append_to() # TODO: Use splice_async when it gets implemented - splicer = StreamSplicer(input_stream, output_stream) + splicer = StreamSplicer(test_input_stream, test_output_stream) splicer.start() loop = gobject.MainLoop() loop.run() - diff --git a/src/jarabe/model/friends.py b/src/jarabe/model/friends.py index b7bf7f1..192f683 100644 --- a/src/jarabe/model/friends.py +++ b/src/jarabe/model/friends.py @@ -21,15 +21,83 @@ from ConfigParser import ConfigParser import gobject import dbus -from jarabe.model.buddy import BuddyModel from sugar import env +from sugar.graphics.xocolor import XoColor + +from jarabe.model.buddy import BuddyModel +from jarabe.model import neighborhood + + +_model = None + + +class FriendBuddyModel(BuddyModel): + __gtype_name__ = 'SugarFriendBuddyModel' + + _NOT_PRESENT_COLOR = '#D5D5D5,#FFFFFF' + + def __init__(self, nick, key): + self._online_buddy = None + + BuddyModel.__init__(self, nick=nick, key=key) + + neighborhood_model = neighborhood.get_model() + neighborhood_model.connect('buddy-added', self.__buddy_added_cb) + neighborhood_model.connect('buddy-removed', self.__buddy_removed_cb) + + buddy = neighborhood_model.get_buddy_by_key(key) + if buddy is not None: + self._set_online_buddy(buddy) + + def __buddy_added_cb(self, model_, buddy): + if buddy.key != self.key: + return + self._set_online_buddy(buddy) + + def _set_online_buddy(self, buddy): + self._online_buddy = buddy + self._online_buddy.connect('notify::color', self.__notify_color_cb) + self.notify('color') + self.notify('present') + + def __buddy_removed_cb(self, model_, buddy): + if buddy.key != self.key: + return + self._online_buddy = None + self.notify('color') + self.notify('present') + + def __notify_color_cb(self, buddy, pspec): + self.notify('color') + + def is_present(self): + return self._online_buddy is not None + + present = gobject.property(type=bool, default=False, getter=is_present) + + def get_color(self): + if self._online_buddy is not None: + return self._online_buddy.color + else: + return XoColor(FriendBuddyModel._NOT_PRESENT_COLOR) + + color = gobject.property(type=object, getter=get_color) + + def get_handle(self): + if self._online_buddy is not None: + return self._online_buddy.handle + else: + return None + + handle = gobject.property(type=object, getter=get_handle) + 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])) + 'friend-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'friend-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), } def __init__(self): @@ -41,7 +109,7 @@ class Friends(gobject.GObject): self.load() def has_buddy(self, buddy): - return self._friends.has_key(buddy.get_key()) + return buddy.get_key() in self._friends def add_friend(self, buddy_info): self._friends[buddy_info.get_key()] = buddy_info @@ -49,6 +117,7 @@ class Friends(gobject.GObject): def make_friend(self, buddy): if not self.has_buddy(buddy): + buddy = FriendBuddyModel(key=buddy.key, nick=buddy.nick) self.add_friend(buddy) self.save() @@ -70,7 +139,7 @@ class Friends(gobject.GObject): # HACK: don't screw up on old friends files if len(key) < 20: continue - buddy = BuddyModel(key=key, nick=cp.get(key, 'nick')) + buddy = FriendBuddyModel(key=key, nick=cp.get(key, 'nick')) self.add_friend(buddy) except Exception: logging.exception('Error parsing friends file') @@ -82,7 +151,6 @@ class Friends(gobject.GObject): 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) @@ -113,7 +181,6 @@ class Friends(gobject.GObject): reply_handler=friends_synced, error_handler=friends_synced_error) -_model = None def get_model(): global _model diff --git a/src/jarabe/model/invites.py b/src/jarabe/model/invites.py index c918308..d2a2e0c 100644 --- a/src/jarabe/model/invites.py +++ b/src/jarabe/model/invites.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> # # 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 @@ -14,110 +15,227 @@ # 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. +import logging +from functools import partial - bundle_id: string, e.g. 'org.laptop.Chat' - """ - self._bundle_id = bundle_id +import gobject +import dbus +from telepathy.interfaces import CHANNEL, \ + CHANNEL_DISPATCHER, \ + CHANNEL_DISPATCH_OPERATION, \ + CHANNEL_TYPE_CONTACT_LIST, \ + CHANNEL_TYPE_DBUS_TUBE, \ + CHANNEL_TYPE_STREAMED_MEDIA, \ + CHANNEL_TYPE_STREAM_TUBE, \ + CHANNEL_TYPE_TEXT, \ + CLIENT +from telepathy.constants import HANDLE_TYPE_ROOM - def get_bundle_id(self): - return self._bundle_id +from sugar.graphics.xocolor import XoColor +from jarabe.model import telepathyclient +from jarabe.model import bundleregistry +from jarabe.model import neighborhood +from jarabe.journal import misc -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 +CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \ + 'org.laptop.Telepathy.ActivityProperties' +_instance = None -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. +class ActivityInvite(object): + """Invitation to a shared activity.""" + def __init__(self, dispatch_operation_path, handle, handler, + activity_properties): + self.dispatch_operation_path = dispatch_operation_path + self._handle = handle + self._handler = handler - 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 + if activity_properties is not None: + self._activity_properties = activity_properties + else: + self._activity_properties = {} - def get_private_channel(self): - """Telepathy channel info from private invitation""" - return self._private_channel + def get_bundle_id(self): + if CLIENT in self._handler: + return self._handler[len(CLIENT + '.'):] + else: + return None + + def get_color(self): + color = self._activity_properties.get('color', None) + return XoColor(color) + + def join(self): + logging.error('ActivityInvite.join handler %r', self._handler) + + registry = bundleregistry.get_registry() + bundle_id = self.get_bundle_id() + bundle = registry.get_bundle(bundle_id) + if bundle is None: + self._call_handle_with() + else: + bus = dbus.SessionBus() + bus.add_signal_receiver(self.__name_owner_changed_cb, + 'NameOwnerChanged', + 'org.freedesktop.DBus', + arg0=self._handler) + + model = neighborhood.get_model() + activity_id = model.get_activity_by_room(self._handle).activity_id + misc.launch(bundle, color=self.get_color(), invited=True, + activity_id=activity_id) + + def __name_owner_changed_cb(self, name, old_owner, new_owner): + logging.debug('ActivityInvite.__name_owner_changed_cb %r %r %r', name, + new_owner, old_owner) + if name == self._handler and new_owner and not old_owner: + self._call_handle_with() + + def _call_handle_with(self): + bus = dbus.Bus() + obj = bus.get_object(CHANNEL_DISPATCHER, self.dispatch_operation_path) + dispatch_operation = dbus.Interface(obj, CHANNEL_DISPATCH_OPERATION) + dispatch_operation.HandleWith(self._handler, + reply_handler=self.__handle_with_reply_cb, + error_handler=self.__handle_with_reply_cb) + + def __handle_with_reply_cb(self, error=None): + if error is not None: + raise error + else: + logging.debug('__handle_with_reply_cb') 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])) + '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 + self._dispatch_operations = {} + + client_handler = telepathyclient.get_instance() + client_handler.got_dispatch_operation.connect( + self.__got_dispatch_operation_cb) + + def __got_dispatch_operation_cb(self, **kwargs): + logging.debug('__got_dispatch_operation_cb') + dispatch_operation_path = kwargs['dispatch_operation_path'] + channel_path, channel_properties = kwargs['channels'][0] + properties = kwargs['properties'] + channel_type = channel_properties[CHANNEL + '.ChannelType'] + handle_type = channel_properties[CHANNEL + '.TargetHandleType'] + handle = channel_properties[CHANNEL + '.TargetHandle'] + + if handle_type == HANDLE_TYPE_ROOM and \ + channel_type == CHANNEL_TYPE_TEXT: + logging.debug('May be an activity, checking its properties') + connection_path = properties[CHANNEL_DISPATCH_OPERATION + + '.Connection'] + connection_name = connection_path.replace('/', '.')[1:] + + bus = dbus.Bus() + connection = bus.get_object(connection_name, connection_path) + connection.GetProperties( + channel_properties[CHANNEL + '.TargetHandle'], + dbus_interface=CONNECTION_INTERFACE_ACTIVITY_PROPERTIES, + reply_handler=partial(self.__get_properties_cb, + handle, + dispatch_operation_path), + error_handler=partial(self.__error_handler_cb, + handle, + channel_properties, + dispatch_operation_path)) + else: + self._dispatch_non_sugar_invitation(channel_path, + channel_properties, + dispatch_operation_path) + + def __get_properties_cb(self, handle, dispatch_operation_path, properties): + logging.debug('__get_properties_cb %r', properties) + handler = '%s.%s' % (CLIENT, properties['type']) + self._add_invite(dispatch_operation_path, handle, handler, properties) + + def __error_handler_cb(self, handle, channel_properties, + dispatch_operation_path, error): + logging.debug('__error_handler_cb %r', error) + exception_name = 'org.freedesktop.Telepathy.Error.NotAvailable' + if error.get_dbus_name() == exception_name: + self._dispatch_non_sugar_invitation(handle, + channel_properties, + dispatch_operation_path) + else: + raise error + + def _dispatch_non_sugar_invitation(self, handle, channel_properties, + dispatch_operation_path): + handler = None + channel_type = channel_properties[CHANNEL + '.ChannelType'] + if channel_type == CHANNEL_TYPE_CONTACT_LIST: + self._handle_with(dispatch_operation_path, CLIENT + '.Sugar') + elif channel_type == CHANNEL_TYPE_TEXT: + handler = CLIENT + '.org.laptop.Chat' + elif channel_type == CHANNEL_TYPE_STREAMED_MEDIA: + handler = CLIENT + '.org.laptop.VideoChat' + elif channel_type == CHANNEL_TYPE_DBUS_TUBE: + handler = channel_properties[CHANNEL_TYPE_DBUS_TUBE + + '.ServiceName'] + elif channel_type == CHANNEL_TYPE_STREAM_TUBE: + handler = channel_properties[CHANNEL_TYPE_STREAM_TUBE + '.Service'] + else: + self._handle_with(dispatch_operation_path, '') + + if handler is not None: + logging.debug('Adding an invite from a non-Sugar client') + self._add_invite(dispatch_operation_path, handle, handler) + + def _handle_with(self, dispatch_operation_path, handler): + logging.debug('_handle_with %r %r', dispatch_operation_path, handler) + bus = dbus.Bus() + obj = bus.get_object(CHANNEL_DISPATCHER, dispatch_operation_path) + dispatch_operation = dbus.Interface(obj, CHANNEL_DISPATCH_OPERATION) + dispatch_operation.HandleWith(handler, + reply_handler=self.__handle_with_reply_cb, + error_handler=self.__handle_with_reply_cb) + + def __handle_with_reply_cb(self, error=None): + if error is not None: + logging.error('__handle_with_reply_cb %r', error) + else: + logging.debug('__handle_with_reply_cb') + + def _add_invite(self, dispatch_operation_path, handle, handler, + activity_properties=None): + logging.debug('_add_invite %r %r %r', dispatch_operation_path, handle, + handler) + if dispatch_operation_path in self._dispatch_operations: + # there is no point to have more than one invite for the same + # dispatch operation 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 + invite = ActivityInvite(dispatch_operation_path, handle, handler, + activity_properties) + self._dispatch_operations[dispatch_operation_path] = invite self.emit('invite-added', invite) def remove_invite(self, invite): - del self._dict[invite.get_activity_id()] + del self._dispatch_operations[invite.dispatch_operation_path] 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 __iter__(self): + return self._dispatch_operations.values().__iter__() - def _owner_joined_cb(self, owner, activity): - self.remove_activity(activity.props.id) - def __iter__(self): - return self._dict.values().__iter__() +def get_instance(): + global _instance + if not _instance: + _instance = Invites() + return _instance diff --git a/src/jarabe/model/mimeregistry.py b/src/jarabe/model/mimeregistry.py index 537f6f3..7fb5bcf 100644 --- a/src/jarabe/model/mimeregistry.py +++ b/src/jarabe/model/mimeregistry.py @@ -21,6 +21,7 @@ import gconf _DEFAULTS_KEY = '/desktop/sugar/journal/defaults' _GCONF_INVALID_CHARS = re.compile('[^a-zA-Z0-9-_/.]') + _instance = None diff --git a/src/jarabe/model/neighborhood.py b/src/jarabe/model/neighborhood.py index 53e5581..ca4c5bf 100644 --- a/src/jarabe/model/neighborhood.py +++ b/src/jarabe/model/neighborhood.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> # # 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 @@ -14,257 +14,992 @@ # 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 functools import partial +from hashlib import sha1 + import gobject import gconf -import logging +import dbus +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import ACCOUNT, \ + ACCOUNT_MANAGER, \ + CHANNEL, \ + CHANNEL_INTERFACE_GROUP, \ + CHANNEL_TYPE_CONTACT_LIST, \ + CHANNEL_TYPE_FILE_TRANSFER, \ + CLIENT, \ + CONNECTION, \ + CONNECTION_INTERFACE_ALIASING, \ + CONNECTION_INTERFACE_CONTACTS, \ + CONNECTION_INTERFACE_CONTACT_CAPABILITIES, \ + CONNECTION_INTERFACE_REQUESTS, \ + CONNECTION_INTERFACE_SIMPLE_PRESENCE +from telepathy.constants import HANDLE_TYPE_CONTACT, \ + HANDLE_TYPE_LIST, \ + CONNECTION_PRESENCE_TYPE_OFFLINE, \ + CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED +from telepathy.client import Connection, Channel from sugar.graphics.xocolor import XoColor -from sugar.presence import presenceservice -from sugar import activity +from sugar.profile import get_profile -from jarabe.model.buddy import BuddyModel +from jarabe.model.buddy import BuddyModel, get_owner_instance from jarabe.model import bundleregistry -from jarabe.util.telepathy import connection_watcher -from dbus import PROPERTIES_IFACE -from telepathy.interfaces import CONNECTION_INTERFACE_REQUESTS -from telepathy.interfaces import CHANNEL_INTERFACE -from telepathy.client import Channel -CONN_INTERFACE_GADGET = 'org.laptop.Telepathy.Gadget' -CHAN_INTERFACE_VIEW = 'org.laptop.Telepathy.Channel.Interface.View' -CHAN_INTERFACE_BUDBY_VIEW = 'org.laptop.Telepathy.Channel.Type.BuddyView' -CHAN_INTERFACE_ACTIVITY_VIEW = 'org.laptop.Telepathy.Channel.Type.ActivityView' +ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager' +ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager' +CHANNEL_DISPATCHER_SERVICE = 'org.freedesktop.Telepathy.ChannelDispatcher' +CHANNEL_DISPATCHER_PATH = '/org/freedesktop/Telepathy/ChannelDispatcher' +SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \ + 'org.laptop.Telepathy.ActivityProperties' -NB_RANDOM_BUDDIES = 20 -NB_RANDOM_ACTIVITIES = 40 +_QUERY_DBUS_TIMEOUT = 200 +""" +Time in seconds to wait when querying contact properties. Some jabber servers +will be very slow in returning these queries, so just be patient. +""" -class ActivityModel: - def __init__(self, act, bundle): - self.activity = act - self.bundle = bundle +_model = None + + +class ActivityModel(gobject.GObject): + __gsignals__ = { + 'current-buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'current-buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } - def get_id(self): - return self.activity.props.id + def __init__(self, activity_id, room_handle): + gobject.GObject.__init__(self) - def get_icon_name(self): - return self.bundle.get_icon() + self.activity_id = activity_id + self.room_handle = room_handle + self._bundle = None + self._color = None + self._private = True + self._name = None + self._current_buddies = [] + self._buddies = [] def get_color(self): - return XoColor(self.activity.props.color) + return self._color + + def set_color(self, color): + self._color = color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def get_bundle(self): + return self._bundle + + def set_bundle(self, bundle): + self._bundle = bundle + + bundle = gobject.property(type=object, getter=get_bundle, + setter=set_bundle) + + def get_name(self): + return self._name + + def set_name(self, name): + self._name = name + + name = gobject.property(type=object, getter=get_name, setter=set_name) + + def is_private(self): + return self._private + + def set_private(self, private): + self._private = private + + private = gobject.property(type=object, getter=is_private, + setter=set_private) + + def get_buddies(self): + return self._buddies + + def add_buddy(self, buddy): + self._buddies.append(buddy) + self.notify('buddies') + self.emit('buddy-added', buddy) + + def remove_buddy(self, buddy): + self._buddies.remove(buddy) + self.notify('buddies') + self.emit('buddy-removed', buddy) + + buddies = gobject.property(type=object, getter=get_buddies) + + def get_current_buddies(self): + return self._current_buddies + + def add_current_buddy(self, buddy): + self._current_buddies.append(buddy) + self.notify('current-buddies') + self.emit('current-buddy-added', buddy) + + def remove_current_buddy(self, buddy): + self._current_buddies.remove(buddy) + self.notify('current-buddies') + self.emit('current-buddy-removed', buddy) + + current_buddies = gobject.property(type=object, getter=get_current_buddies) + + +class _Account(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'activity-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object, object])), + 'buddy-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'buddy-left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'current-activity-updated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object, object])), + 'connected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'disconnected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, account_path): + gobject.GObject.__init__(self) + + self.object_path = account_path + + self._connection = None + self._buddy_handles = {} + self._activity_handles = {} + self._self_handle = None + + self._buddies_per_activity = {} + self._activities_per_buddy = {} + + self._start_listening() + + def _start_listening(self): + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Get(ACCOUNT, 'Connection', + reply_handler=self.__got_connection_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.GetConnection')) + obj.connect_to_signal( + 'AccountPropertyChanged', self.__account_property_changed_cb) + + def __error_handler_cb(self, function_name, error): + raise RuntimeError('Error when calling %s: %s' % (function_name, + error)) + + def __got_connection_cb(self, connection_path): + logging.debug('_Account.__got_connection_cb %r', connection_path) + + if connection_path == '/': + self._check_registration_error() + return + + self._prepare_connection(connection_path) + + def _check_registration_error(self): + """ + See if a previous connection attempt failed and we need to unset + the register flag. + """ + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Get(ACCOUNT, 'ConnectionError', + reply_handler=self.__got_connection_error_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.GetConnectionError')) + + def __got_connection_error_cb(self, error): + logging.debug('_Account.__got_connection_error_cb %r', error) + if error == 'org.freedesktop.Telepathy.Error.RegistrationExists': + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.UpdateParameters({'register': False}, [], + dbus_interface=ACCOUNT) + + def __account_property_changed_cb(self, properties): + logging.debug('_Account.__account_property_changed_cb %r %r %r', + self.object_path, properties.get('Connection', None), + self._connection) + if 'Connection' not in properties: + return + if properties['Connection'] == '/': + self._check_registration_error() + self._connection = None + elif self._connection is None: + self._prepare_connection(properties['Connection']) + + def _prepare_connection(self, connection_path): + connection_name = connection_path.replace('/', '.')[1:] + + self._connection = Connection(connection_name, connection_path, + ready_handler=self.__connection_ready_cb) + + def __connection_ready_cb(self, connection): + logging.debug('_Account.__connection_ready_cb %r', + connection.object_path) + connection.connect_to_signal('StatusChanged', + self.__status_changed_cb) + + connection[PROPERTIES_IFACE].Get(CONNECTION, + 'Status', + reply_handler=self.__get_status_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetStatus')) + + def __get_status_cb(self, status): + logging.debug('_Account.__get_status_cb %r %r', + self._connection.object_path, status) + self._update_status(status) + + def __status_changed_cb(self, status, reason): + logging.debug('_Account.__status_changed_cb %r %r', status, reason) + self._update_status(status) + + def _update_status(self, status): + if status == CONNECTION_STATUS_CONNECTED: + self._connection[PROPERTIES_IFACE].Get(CONNECTION, + 'SelfHandle', + reply_handler=self.__get_self_handle_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetSelfHandle')) + self.emit('connected') + else: + for contact_handle, contact_id in self._buddy_handles.items(): + if contact_id is not None: + self.emit('buddy-removed', contact_id) + + for room_handle, activity_id in self._activity_handles.items(): + self.emit('activity-removed', activity_id) + + self._buddy_handles = {} + self._activity_handles = {} + self._buddies_per_activity = {} + self._activities_per_buddy = {} + + self.emit('disconnected') + + if status == CONNECTION_STATUS_DISCONNECTED: + self._connection = None + + def __get_self_handle_cb(self, self_handle): + self._self_handle = self_handle + + if CONNECTION_INTERFACE_CONTACT_CAPABILITIES in self._connection: + interface = CONNECTION_INTERFACE_CONTACT_CAPABILITIES + connection = self._connection[interface] + client_name = CLIENT + '.Sugar.FileTransfer' + file_transfer_channel_class = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, + CHANNEL + '.TargetHandleType': HANDLE_TYPE_CONTACT} + capabilities = [] + connection.UpdateCapabilities( + [(client_name, [file_transfer_channel_class], capabilities)], + reply_handler=self.__update_capabilities_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.UpdateCapabilities')) + + connection = self._connection[CONNECTION_INTERFACE_ALIASING] + connection.connect_to_signal('AliasesChanged', + self.__aliases_changed_cb) + + connection = self._connection[CONNECTION_INTERFACE_SIMPLE_PRESENCE] + connection.connect_to_signal('PresencesChanged', + self.__presences_changed_cb) + + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.connect_to_signal('PropertiesChanged', + self.__buddy_info_updated_cb, + byte_arrays=True) + + connection.connect_to_signal('ActivitiesChanged', + self.__buddy_activities_changed_cb) + + connection.connect_to_signal('CurrentActivityChanged', + self.__current_activity_changed_cb) + else: + logging.warning('Connection %s does not support OLPC buddy ' + 'properties', self._connection.object_path) + + if CONNECTION_INTERFACE_ACTIVITY_PROPERTIES in self._connection: + connection = self._connection[ + CONNECTION_INTERFACE_ACTIVITY_PROPERTIES] + connection.connect_to_signal( + 'ActivityPropertiesChanged', + self.__activity_properties_changed_cb) + else: + logging.warning('Connection %s does not support OLPC activity ' + 'properties', self._connection.object_path) + + properties = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_CONTACT_LIST, + CHANNEL + '.TargetHandleType': HANDLE_TYPE_LIST, + CHANNEL + '.TargetID': 'subscribe', + } + properties = dbus.Dictionary(properties, signature='sv') + connection = self._connection[CONNECTION_INTERFACE_REQUESTS] + is_ours, channel_path, properties = \ + connection.EnsureChannel(properties) + + channel = Channel(self._connection.service_name, channel_path) + channel[CHANNEL_INTERFACE_GROUP].connect_to_signal( + 'MembersChanged', self.__members_changed_cb) + + channel[PROPERTIES_IFACE].Get(CHANNEL_INTERFACE_GROUP, + 'Members', + reply_handler=self.__get_members_ready_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetMembers')) + + def __update_capabilities_cb(self): + pass + + def __aliases_changed_cb(self, aliases): + logging.debug('_Account.__aliases_changed_cb') + for handle, alias in aliases: + if handle in self._buddy_handles: + logging.debug('Got handle %r with nick %r, going to update', + handle, alias) + properties = {CONNECTION_INTERFACE_ALIASING + '/alias': alias} + self.emit('buddy-updated', self._buddy_handles[handle], + properties) + + def __presences_changed_cb(self, presences): + logging.debug('_Account.__presences_changed_cb %r', presences) + for handle, presence in presences.iteritems(): + if handle in self._buddy_handles: + presence_type, status_, message_ = presence + if presence_type == CONNECTION_PRESENCE_TYPE_OFFLINE: + contact_id = self._buddy_handles[handle] + del self._buddy_handles[handle] + self.emit('buddy-removed', contact_id) + + def __buddy_info_updated_cb(self, handle, properties): + logging.debug('_Account.__buddy_info_updated_cb %r', handle) + self.emit('buddy-updated', self._buddy_handles[handle], properties) + + def __current_activity_changed_cb(self, contact_handle, activity_id, + room_handle): + logging.debug('_Account.__current_activity_changed_cb %r %r %r', + contact_handle, activity_id, room_handle) + if contact_handle in self._buddy_handles: + contact_id = self._buddy_handles[contact_handle] + if not activity_id and room_handle: + activity_id = self._activity_handles.get(room_handle, '') + self.emit('current-activity-updated', contact_id, activity_id) + + def __get_current_activity_cb(self, contact_handle, activity_id, + room_handle): + logging.debug('_Account.__get_current_activity_cb %r %r %r', + contact_handle, activity_id, room_handle) + contact_id = self._buddy_handles[contact_handle] + self.emit('current-activity-updated', contact_id, activity_id) + + def __buddy_activities_changed_cb(self, buddy_handle, activities): + self._update_buddy_activities(buddy_handle, activities) + + def _update_buddy_activities(self, buddy_handle, activities): + logging.debug('_Account._update_buddy_activities') + if not buddy_handle in self._buddy_handles: + self._buddy_handles[buddy_handle] = None + + if not buddy_handle in self._activities_per_buddy: + self._activities_per_buddy[buddy_handle] = set() + + for activity_id, room_handle in activities: + if room_handle not in self._activity_handles: + self._activity_handles[room_handle] = activity_id + self.emit('activity-added', room_handle, activity_id) + + connection = self._connection[ + CONNECTION_INTERFACE_ACTIVITY_PROPERTIES] + connection.GetProperties(room_handle, + reply_handler=partial(self.__get_properties_cb, + room_handle), + error_handler=partial(self.__error_handler_cb, + 'ActivityProperties.GetProperties')) + + # Sometimes we'll get CurrentActivityChanged before we get to + # know about the activity so we miss the event. In that case, + # request again the current activity for this buddy. + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.GetCurrentActivity( + buddy_handle, + reply_handler=partial(self.__get_current_activity_cb, + buddy_handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetCurrentActivity')) + + if not activity_id in self._buddies_per_activity: + self._buddies_per_activity[activity_id] = set() + self._buddies_per_activity[activity_id].add(buddy_handle) + if activity_id not in self._activities_per_buddy[buddy_handle]: + self._activities_per_buddy[buddy_handle].add(activity_id) + if self._buddy_handles[buddy_handle] is not None: + self.emit('buddy-joined-activity', + self._buddy_handles[buddy_handle], + activity_id) + + current_activity_ids = \ + [activity_id for activity_id, room_handle in activities] + for activity_id in self._activities_per_buddy[buddy_handle].copy(): + if not activity_id in current_activity_ids: + self._remove_buddy_from_activity(buddy_handle, activity_id) + + def __get_properties_cb(self, room_handle, properties): + logging.debug('_Account.__get_properties_cb %r %r', room_handle, + properties) + if properties: + self._update_activity(room_handle, properties) + + def _remove_buddy_from_activity(self, buddy_handle, activity_id): + if buddy_handle in self._buddies_per_activity[activity_id]: + self._buddies_per_activity[activity_id].remove(buddy_handle) + + if activity_id in self._activities_per_buddy[buddy_handle]: + self._activities_per_buddy[buddy_handle].remove(activity_id) + + if self._buddy_handles[buddy_handle] is not None: + self.emit('buddy-left-activity', + self._buddy_handles[buddy_handle], + activity_id) + + if not self._buddies_per_activity[activity_id]: + del self._buddies_per_activity[activity_id] + + for room_handle in self._activity_handles.copy(): + if self._activity_handles[room_handle] == activity_id: + del self._activity_handles[room_handle] + break + + self.emit('activity-removed', activity_id) + + def __activity_properties_changed_cb(self, room_handle, properties): + logging.debug('_Account.__activity_properties_changed_cb %r %r', + room_handle, properties) + self._update_activity(room_handle, properties) + + def _update_activity(self, room_handle, properties): + if room_handle in self._activity_handles: + self.emit('activity-updated', self._activity_handles[room_handle], + properties) + else: + logging.debug('_Account.__activity_properties_changed_cb unknown ' + 'activity') + # We don't get ActivitiesChanged for the owner of the connection, + # so we query for its activities in order to find out. + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + handle = self._self_handle + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.GetActivities( + handle, + reply_handler=partial(self.__got_activities_cb, handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.Getactivities')) + + def __members_changed_cb(self, message, added, removed, local_pending, + remote_pending, actor, reason): + self._add_buddy_handles(added) + + def __get_members_ready_cb(self, handles): + logging.debug('_Account.__get_members_ready_cb %r', handles) + if not handles: + return + + self._add_buddy_handles(handles) + + def _add_buddy_handles(self, handles): + logging.debug('_Account._add_buddy_handles %r', handles) + interfaces = [CONNECTION, CONNECTION_INTERFACE_ALIASING] + self._connection[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes( + handles, interfaces, False, + reply_handler=self.__get_contact_attributes_cb, + error_handler=partial(self.__error_handler_cb, + 'Contacts.GetContactAttributes')) + + def __got_buddy_info_cb(self, handle, nick, properties): + logging.debug('_Account.__got_buddy_info_cb %r', handle) + self.emit('buddy-updated', self._buddy_handles[handle], properties) + + def __get_contact_attributes_cb(self, attributes): + logging.debug('_Account.__get_contact_attributes_cb %r', + attributes.keys()) + + for handle in attributes.keys(): + nick = attributes[handle][CONNECTION_INTERFACE_ALIASING + '/alias'] + + if handle in self._buddy_handles and \ + not self._buddy_handles[handle] is None: + logging.debug('Got handle %r with nick %r, going to update', + handle, nick) + self.emit('buddy-updated', self._buddy_handles[handle], + attributes[handle]) + else: + logging.debug('Got handle %r with nick %r, going to add', + handle, nick) + + contact_id = attributes[handle][CONNECTION + '/contact-id'] + self._buddy_handles[handle] = contact_id + + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + connection = \ + self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + + connection.GetProperties( + handle, + reply_handler=partial(self.__got_buddy_info_cb, handle, + nick), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetProperties'), + byte_arrays=True, + timeout=_QUERY_DBUS_TIMEOUT) + + connection.GetActivities( + handle, + reply_handler=partial(self.__got_activities_cb, + handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetActivities'), + timeout=_QUERY_DBUS_TIMEOUT) + + connection.GetCurrentActivity( + handle, + reply_handler=partial(self.__get_current_activity_cb, + handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetCurrentActivity'), + timeout=_QUERY_DBUS_TIMEOUT) + + self.emit('buddy-added', contact_id, nick, handle) + + def __got_activities_cb(self, buddy_handle, activities): + logging.debug('_Account.__got_activities_cb %r %r', buddy_handle, + activities) + self._update_buddy_activities(buddy_handle, activities) + + def enable(self): + logging.debug('_Account.enable %s', self.object_path) + self._set_enabled(True) + + def disable(self): + logging.debug('_Account.disable %s', self.object_path) + self._set_enabled(False) + self._connection = None + + def _set_enabled(self, value): + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Set(ACCOUNT, 'Enabled', value, + reply_handler=self.__set_enabled_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.SetEnabled'), + dbus_interface=dbus.PROPERTIES_IFACE) + + def __set_enabled_cb(self): + logging.debug('_Account.__set_enabled_cb success') - def get_bundle_id(self): - return self.bundle.get_bundle_id() class Neighborhood(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])) + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), } def __init__(self): gobject.GObject.__init__(self) + self._buddies = {None: get_owner_instance()} self._activities = {} - self._buddies = {} - - 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) - - self._conn_watcher = connection_watcher.ConnectionWatcher() - self._conn_watcher.connect('connection-added', self.__conn_addded_cb) - - for conn in self._conn_watcher.get_connections(): - self.__conn_addded_cb(self._conn_watcher, conn) - - gconf_client = gconf.client_get_default() - gconf_client.add_dir('/desktop/sugar/collaboration', - gconf.CLIENT_PRELOAD_NONE) - gconf_client.notify_add('/desktop/sugar/collaboration/publish_gadget', - self.__publish_gadget_changed_cb) - - def __conn_addded_cb(self, watcher, conn): - if CONN_INTERFACE_GADGET not in conn: + self._link_local_account = None + self._server_account = None + + client = gconf.client_get_default() + client.add_dir('/desktop/sugar/collaboration', + gconf.CLIENT_PRELOAD_NONE) + client.notify_add('/desktop/sugar/collaboration/jabber_server', + self.__jabber_server_changed_cb) + client.add_dir('/desktop/sugar/user/nick', gconf.CLIENT_PRELOAD_NONE) + client.notify_add('/desktop/sugar/user/nick', self.__nick_changed_cb) + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts', + dbus_interface=PROPERTIES_IFACE, + reply_handler=self.__got_accounts_cb, + error_handler=self.__error_handler_cb) + + def __got_accounts_cb(self, account_paths): + self._link_local_account = \ + self._ensure_link_local_account(account_paths) + self._connect_to_account(self._link_local_account) + + self._server_account = self._ensure_server_account(account_paths) + self._connect_to_account(self._server_account) + + def __error_handler_cb(self, error): + raise RuntimeError(error) + + def _connect_to_account(self, account): + account.connect('buddy-added', self.__buddy_added_cb) + account.connect('buddy-updated', self.__buddy_updated_cb) + account.connect('buddy-removed', self.__buddy_removed_cb) + account.connect('buddy-joined-activity', + self.__buddy_joined_activity_cb) + account.connect('buddy-left-activity', self.__buddy_left_activity_cb) + account.connect('activity-added', self.__activity_added_cb) + account.connect('activity-updated', self.__activity_updated_cb) + account.connect('activity-removed', self.__activity_removed_cb) + account.connect('current-activity-updated', + self.__current_activity_updated_cb) + account.connect('connected', self.__account_connected_cb) + account.connect('disconnected', self.__account_disconnected_cb) + + def __account_connected_cb(self, account): + logging.debug('__account_connected_cb %s', account.object_path) + if account == self._server_account: + self._link_local_account.disable() + + def __account_disconnected_cb(self, account): + logging.debug('__account_disconnected_cb %s', account.object_path) + if account == self._server_account: + self._link_local_account.enable() + + def _ensure_link_local_account(self, account_paths): + for account_path in account_paths: + if 'salut' in account_path: + logging.debug('Already have a Salut account') + account = _Account(account_path) + account.enable() + return account + + logging.debug('Still dont have a Salut account, creating one') + + client = gconf.client_get_default() + nick = client.get_string('/desktop/sugar/user/nick') + + params = { + 'nickname': nick, + 'first-name': '', + 'last-name': '', + 'jid': self._get_jabber_account_id(), + 'published-name': nick, + } + + properties = { + ACCOUNT + '.Enabled': True, + ACCOUNT + '.Nickname': nick, + ACCOUNT + '.ConnectAutomatically': True, + } + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_path = account_manager.CreateAccount('salut', 'local-xmpp', + 'salut', params, + properties) + return _Account(account_path) + + def _ensure_server_account(self, account_paths): + for account_path in account_paths: + if 'gabble' in account_path: + logging.debug('Already have a Gabble account') + account = _Account(account_path) + account.enable() + return account + + logging.debug('Still dont have a Gabble account, creating one') + + client = gconf.client_get_default() + nick = client.get_string('/desktop/sugar/user/nick') + server = client.get_string('/desktop/sugar/collaboration' + '/jabber_server') + key_hash = get_profile().privkey_hash + + params = { + 'account': self._get_jabber_account_id(), + 'password': key_hash, + 'server': server, + 'resource': 'sugar', + 'require-encryption': True, + 'ignore-ssl-errors': True, + 'register': True, + 'old-ssl': True, + 'port': dbus.UInt32(5223), + } + + properties = { + ACCOUNT + '.Enabled': True, + ACCOUNT + '.Nickname': nick, + ACCOUNT + '.ConnectAutomatically': True, + } + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_path = account_manager.CreateAccount('gabble', 'jabber', + 'jabber', params, + properties) + return _Account(account_path) + + def _get_jabber_account_id(self): + public_key_hash = sha1(get_profile().pubkey).hexdigest() + client = gconf.client_get_default() + server = client.get_string('/desktop/sugar/collaboration' + '/jabber_server') + return '%s@%s' % (public_key_hash, server) + + def __jabber_server_changed_cb(self, client, timestamp, entry, *extra): + logging.debug('__jabber_server_changed_cb') + + bus = dbus.Bus() + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._server_account.object_path) + + server = client.get_string( + '/desktop/sugar/collaboration/jabber_server') + account_id = self._get_jabber_account_id() + needs_reconnect = account.UpdateParameters({'server': server, + 'account': account_id, + 'register': True}, + dbus.Array([], 's'), + dbus_interface=ACCOUNT) + if needs_reconnect: + account.Reconnect() + + self._update_jid() + + def __nick_changed_cb(self, client, timestamp, entry, *extra): + logging.debug('__nick_changed_cb') + + nick = client.get_string('/desktop/sugar/user/nick') + for account in self._server_account, self._link_local_account: + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account.object_path) + obj.Set(ACCOUNT, 'Nickname', nick, dbus_interface=PROPERTIES_IFACE) + + self._update_jid() + + def _update_jid(self): + bus = dbus.Bus() + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._link_local_account.object_path) + + account_id = self._get_jabber_account_id() + needs_reconnect = account.UpdateParameters({'jid': account_id}, + dbus.Array([], 's'), + dbus_interface=ACCOUNT) + if needs_reconnect: + account.Reconnect() + + def __buddy_added_cb(self, account, contact_id, nick, handle): + logging.debug('__buddy_added_cb %r', contact_id) + + if contact_id in self._buddies: + logging.debug('__buddy_added_cb buddy already tracked') return - conn[CONN_INTERFACE_GADGET].connect_to_signal('GadgetDiscovered', - lambda: self._gadget_discovered(conn)) - - gadget_discovered = conn[PROPERTIES_IFACE].Get(CONN_INTERFACE_GADGET, - 'GadgetAvailable') - if gadget_discovered: - self._gadget_discovered(conn) - - def _gadget_discovered(self, conn): - gconf_client = gconf.client_get_default() - key = '/desktop/sugar/collaboration/publish_gadget' - publish = gconf_client.get_bool(key) - logging.debug('Gadget discovered on connection %s.' - ' Publish our status: %r', conn.service_name.split('.')[-1], - publish) - conn[CONN_INTERFACE_GADGET].Publish(publish) - - self._request_random_buddies(conn, NB_RANDOM_BUDDIES) - self._request_random_activities(conn, NB_RANDOM_ACTIVITIES) - - def _request_random_buddies(self, conn, nb): - logging.debug('Request %d random buddies', nb) - - path, props_ = conn[CONNECTION_INTERFACE_REQUESTS].CreateChannel( - { 'org.freedesktop.Telepathy.Channel.ChannelType': - 'org.laptop.Telepathy.Channel.Type.BuddyView', - 'org.laptop.Telepathy.Channel.Interface.View.MaxSize': nb - }) - - view = Channel(conn.service_name, path) - view[CHANNEL_INTERFACE].connect_to_signal('Closed', - lambda: self.__respawnable_view_closed_cb( - lambda: self._request_random_buddies(conn, nb))) - - def _request_random_activities(self, conn, nb): - logging.debug('Request %d random activities', nb) - - path, props_ = conn[CONNECTION_INTERFACE_REQUESTS].CreateChannel( - { 'org.freedesktop.Telepathy.Channel.ChannelType': - 'org.laptop.Telepathy.Channel.Type.ActivityView', - 'org.laptop.Telepathy.Channel.Interface.View.MaxSize': nb - }) - - view = Channel(conn.service_name, path) - view[CHANNEL_INTERFACE].connect_to_signal('Closed', - lambda: self.__respawnable_view_closed_cb( - lambda: self._request_random_activities(conn, nb))) - - def __publish_gadget_changed_cb(self, client_, cnxn_id_, entry, - user_data=None): - if entry.value.type == gconf.VALUE_BOOL: - publish = entry.value.get_bool() - - for conn in self._conn_watcher.get_connections(): - if CONN_INTERFACE_GADGET not in conn: - continue - - gadget_discovered = conn[PROPERTIES_IFACE].Get( - CONN_INTERFACE_GADGET, 'GadgetAvailable') - if gadget_discovered: - logging.debug("publish_gadget gconf key changed." - " Publish our status on %s: %r" % - (conn.service_name.split('.')[-1], publish)) - conn[CONN_INTERFACE_GADGET].Publish(publish) - - def __respawnable_view_closed_cb(self, request_fct): - # Views are closed if the Gadget component is restarted. As we always - # want to have the random views opened, we re-request them if they are - # closed. - logging.debug('View closed. Re-request it') - request_fct() - - 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) + buddy = BuddyModel( + nick=nick, + account=account.object_path, + contact_id=contact_id, + handle=handle) + self._buddies[contact_id] = buddy + + def __buddy_updated_cb(self, account, contact_id, properties): + logging.debug('__buddy_updated_cb %r', contact_id) + if contact_id is None: + # Don't know the contact-id yet, will get the full state later + return - def get_activities(self): - return self._activities.values() + if contact_id not in self._buddies: + logging.debug('__buddy_updated_cb Unknown buddy with contact_id' + ' %r', contact_id) + return - def get_buddies(self): - return self._buddies.values() + buddy = self._buddies[contact_id] - 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) + is_new = buddy.props.key is None and 'key' in properties + + if 'color' in properties: + buddy.props.color = XoColor(properties['color']) + + if 'key' in properties: + buddy.props.key = properties['key'] + + if 'nick' in properties: + buddy.props.nick = properties['nick'] - def _buddy_appeared_cb(self, pservice, buddy): - if self._buddies.has_key(buddy.object_path()): + if is_new: + self.emit('buddy-added', buddy) + + def __buddy_removed_cb(self, account, contact_id): + logging.debug('Neighborhood.__buddy_removed_cb %r', contact_id) + if contact_id not in self._buddies: + logging.debug('Neighborhood.__buddy_removed_cb Unknown buddy with ' + 'contact_id %r', contact_id) 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) + buddy = self._buddies[contact_id] + del self._buddies[contact_id] - cur_activity = buddy.props.current_activity - if cur_activity: - self._buddy_activity_changed_cb(model, cur_activity) + if buddy.props.key is not None: + self.emit('buddy-removed', buddy) - def _buddy_disappeared_cb(self, pservice, buddy): - if not self._buddies.has_key(buddy.object_path()): + def __activity_added_cb(self, account, room_handle, activity_id): + logging.debug('__activity_added_cb %r %r', room_handle, activity_id) + if activity_id in self._activities: + logging.debug('__activity_added_cb activity already tracked') 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) + activity = ActivityModel(activity_id, room_handle) + self._activities[activity_id] = activity + + def __activity_updated_cb(self, account, activity_id, properties): + logging.debug('__activity_updated_cb %r %r', activity_id, properties) + if activity_id not in self._activities: + logging.debug('__activity_updated_cb Unknown activity with ' + 'activity_id %r', activity_id) + return - def _check_activity(self, presence_activity): registry = bundleregistry.get_registry() - bundle = registry.get_bundle(presence_activity.props.type) + bundle = registry.get_bundle(properties['type']) if not bundle: + logging.warning('Ignoring shared activity we don''t have') + return + + activity = self._activities[activity_id] + + is_new = activity.props.bundle is None + + activity.props.color = XoColor(properties['color']) + activity.props.bundle = bundle + activity.props.name = properties['name'] + activity.props.private = properties['private'] + + if is_new: + self.emit('activity-added', activity) + + def __activity_removed_cb(self, account, activity_id): + logging.debug('__activity_removed_cb %r', activity_id) + if activity_id not in self._activities: + logging.debug('Unknown activity with id %s. Already removed?', + activity_id) + return + activity = self._activities[activity_id] + del self._activities[activity_id] + + if activity.props.bundle is not None: + self.emit('activity-removed', activity) + + def __current_activity_updated_cb(self, account, contact_id, activity_id): + logging.debug('__current_activity_updated_cb %r %r', contact_id, + activity_id) + if contact_id not in self._buddies: + logging.debug('__current_activity_updated_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + if activity_id and activity_id not in self._activities: + logging.debug('__current_activity_updated_cb Unknown activity with' + ' id %s', activity_id) + activity_id = '' + + buddy = self._buddies[contact_id] + if buddy.props.current_activity is not None: + if buddy.props.current_activity.activity_id == activity_id: + return + buddy.props.current_activity.remove_current_buddy(buddy) + + if activity_id: + activity = self._activities[activity_id] + buddy.props.current_activity = activity + activity.add_current_buddy(buddy) + else: + buddy.props.current_activity = None + + def __buddy_joined_activity_cb(self, account, contact_id, activity_id): + if contact_id not in self._buddies: + logging.debug('__buddy_joined_activity_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + + if activity_id not in self._activities: + logging.debug('__buddy_joined_activity_cb Unknown activity with ' + 'activity_id %r', activity_id) + return + + self._activities[activity_id].add_buddy(self._buddies[contact_id]) + + def __buddy_left_activity_cb(self, account, contact_id, activity_id): + if contact_id not in self._buddies: + logging.debug('__buddy_left_activity_cb Unknown buddy with ' + 'contact_id %r', contact_id) return - if self.has_activity(presence_activity.props.id): + + if activity_id not in self._activities: + logging.debug('__buddy_left_activity_cb Unknown activity with ' + 'activity_id %r', activity_id) return - self.add_activity(bundle, presence_activity) - def has_activity(self, activity_id): - return self._activities.has_key(activity_id) + self._activities[activity_id].remove_buddy(self._buddies[contact_id]) + + def get_buddies(self): + return self._buddies.values() + + def get_buddy_by_key(self, key): + for buddy in self._buddies.values(): + if buddy.key == key: + return buddy + return None + + def get_buddy_by_handle(self, contact_handle): + for buddy in self._buddies.values(): + if not buddy.is_owner() and buddy.handle == contact_handle: + return buddy + return None 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] + return self._activities.get(activity_id, None) + + def get_activity_by_room(self, room_handle): + for activity in self._activities.values(): + if activity.room_handle == room_handle: + return activity + return None + + def get_activities(self): + return self._activities.values() -_model = None def get_model(): global _model diff --git a/src/jarabe/model/network.py b/src/jarabe/model/network.py index 3a949da..f265ae4 100644 --- a/src/jarabe/model/network.py +++ b/src/jarabe/model/network.py @@ -1,6 +1,6 @@ # Copyright (C) 2008 Red Hat, Inc. # Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer -# Copyright (C) 2009 One Laptop per Child +# Copyright (C) 2009-2010 One Laptop per Child # Copyright (C) 2009 Paraguay Educa, Martin Abente # Copyright (C) 2010 Plan Ceibal, Daniel Castelo # @@ -18,19 +18,23 @@ # 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 time import dbus +import dbus.service import gobject import ConfigParser import gconf +import ctypes from sugar import dispatch from sugar import env from sugar.util import unique_id + DEVICE_TYPE_802_3_ETHERNET = 1 DEVICE_TYPE_802_11_WIRELESS = 2 DEVICE_TYPE_GSM_MODEM = 3 @@ -54,6 +58,49 @@ NM_ACTIVE_CONNECTION_STATE_UNKNOWN = 0 NM_ACTIVE_CONNECTION_STATE_ACTIVATING = 1 NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2 + +NM_DEVICE_STATE_REASON_UNKNOWN = 0 +NM_DEVICE_STATE_REASON_NONE = 1 +NM_DEVICE_STATE_REASON_NOW_MANAGED = 2 +NM_DEVICE_STATE_REASON_NOW_UNMANAGED = 3 +NM_DEVICE_STATE_REASON_CONFIG_FAILED = 4 +NM_DEVICE_STATE_REASON_CONFIG_UNAVAILABLE = 5 +NM_DEVICE_STATE_REASON_CONFIG_EXPIRED = 6 +NM_DEVICE_STATE_REASON_NO_SECRETS = 7 +NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 +NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED = 9 +NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED = 10 +NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT = 11 +NM_DEVICE_STATE_REASON_PPP_START_FAILED = 12 +NM_DEVICE_STATE_REASON_PPP_DISCONNECT = 13 +NM_DEVICE_STATE_REASON_PPP_FAILED = 14 +NM_DEVICE_STATE_REASON_DHCP_START_FAILED = 15 +NM_DEVICE_STATE_REASON_DHCP_ERROR = 16 +NM_DEVICE_STATE_REASON_DHCP_FAILED = 17 +NM_DEVICE_STATE_REASON_SHARED_START_FAILED = 18 +NM_DEVICE_STATE_REASON_SHARED_FAILED = 19 +NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED = 20 +NM_DEVICE_STATE_REASON_AUTOIP_ERROR = 21 +NM_DEVICE_STATE_REASON_AUTOIP_FAILED = 22 +NM_DEVICE_STATE_REASON_MODEM_BUSY = 23 +NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE = 24 +NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER = 25 +NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT = 26 +NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED = 27 +NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED = 28 +NM_DEVICE_STATE_REASON_GSM_APN_FAILED = 29 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING = 30 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED = 31 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT = 32 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED = 33 +NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED = 34 +NM_DEVICE_STATE_REASON_FIRMWARE_MISSING = 35 +NM_DEVICE_STATE_REASON_REMOVED = 36 +NM_DEVICE_STATE_REASON_SLEEPING = 37 +NM_DEVICE_STATE_REASON_CONNECTION_REMOVED = 38 +NM_DEVICE_STATE_REASON_USER_REQUESTED = 39 +NM_DEVICE_STATE_REASON_CARRIER = 40 + NM_802_11_AP_FLAGS_NONE = 0x00000000 NM_802_11_AP_FLAGS_PRIVACY = 0x00000001 @@ -83,11 +130,16 @@ NM_802_11_DEVICE_CAP_RSN = 0x00000020 SETTINGS_SERVICE = 'org.freedesktop.NetworkManagerUserSettings' +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_PATH = '/org/freedesktop/NetworkManager' +NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' NM_SETTINGS_PATH = '/org/freedesktop/NetworkManagerSettings' NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManagerSettings' NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManagerSettings.Connection' NM_SECRETS_IFACE = 'org.freedesktop.NetworkManagerSettings.Connection.Secrets' NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' GSM_USERNAME_PATH = '/desktop/sugar/network/gsm/username' GSM_PASSWORD_PATH = '/desktop/sugar/network/gsm/password' @@ -99,6 +151,137 @@ GSM_PUK_PATH = '/desktop/sugar/network/gsm/puk' _nm_settings = None _conn_counter = 0 +_nm_device_state_reason_description = None + + +def get_error_by_reason(reason): + global _nm_device_state_reason_description + + if _nm_device_state_reason_description is None: + _nm_device_state_reason_description = { + NM_DEVICE_STATE_REASON_UNKNOWN: + _('The reason for the device state change is unknown.'), + NM_DEVICE_STATE_REASON_NONE: + _('The state change is normal.'), + NM_DEVICE_STATE_REASON_NOW_MANAGED: + _('The device is now managed.'), + NM_DEVICE_STATE_REASON_NOW_UNMANAGED: + _('The device is no longer managed.'), + NM_DEVICE_STATE_REASON_CONFIG_FAILED: + _('The device could not be readied for configuration.'), + NM_DEVICE_STATE_REASON_CONFIG_UNAVAILABLE: + _('IP configuration could not be reserved ' + '(no available address, timeout, etc).'), + NM_DEVICE_STATE_REASON_CONFIG_EXPIRED: + _('The IP configuration is no longer valid.'), + NM_DEVICE_STATE_REASON_NO_SECRETS: + _('Secrets were required, but not provided.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT: + _('The 802.1X supplicant disconnected from ' + 'the access point or authentication server.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED: + _('Configuration of the 802.1X supplicant failed.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED: + _('The 802.1X supplicant quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT: + _('The 802.1X supplicant took too long to authenticate.'), + NM_DEVICE_STATE_REASON_PPP_START_FAILED: + _('The PPP service failed to start within the allowed time.'), + NM_DEVICE_STATE_REASON_PPP_DISCONNECT: + _('The PPP service disconnected unexpectedly.'), + NM_DEVICE_STATE_REASON_PPP_FAILED: + _('The PPP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_DHCP_START_FAILED: + _('The DHCP service failed to start within the allowed time.'), + NM_DEVICE_STATE_REASON_DHCP_ERROR: + _('The DHCP service reported an unexpected error.'), + NM_DEVICE_STATE_REASON_DHCP_FAILED: + _('The DHCP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_SHARED_START_FAILED: + _('The shared connection service failed to start.'), + NM_DEVICE_STATE_REASON_SHARED_FAILED: + _('The shared connection service quit or failed' + ' unexpectedly.'), + NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED: + _('The AutoIP service failed to start.'), + NM_DEVICE_STATE_REASON_AUTOIP_ERROR: + _('The AutoIP service reported an unexpected error.'), + NM_DEVICE_STATE_REASON_AUTOIP_FAILED: + _('The AutoIP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_MODEM_BUSY: + _('Dialing failed because the line was busy.'), + NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE: + _('Dialing failed because there was no dial tone.'), + NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER: + _('Dialing failed because there was no carrier.'), + NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT: + _('Dialing timed out.'), + NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED: + _('Dialing failed.'), + NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED: + _('Modem initialization failed.'), + NM_DEVICE_STATE_REASON_GSM_APN_FAILED: + _('Failed to select the specified GSM APN'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING: + _('Not searching for networks.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED: + _('Network registration was denied.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT: + _('Network registration timed out.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED: + _('Failed to register with the requested GSM network.'), + NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED: + _('PIN check failed.'), + NM_DEVICE_STATE_REASON_FIRMWARE_MISSING: + _('Necessary firmware for the device may be missing.'), + NM_DEVICE_STATE_REASON_REMOVED: + _('The device was removed.'), + NM_DEVICE_STATE_REASON_SLEEPING: + _('NetworkManager went to sleep.'), + NM_DEVICE_STATE_REASON_CONNECTION_REMOVED: + _("The device's active connection was removed " + "or disappeared."), + NM_DEVICE_STATE_REASON_USER_REQUESTED: + _('A user or client requested the disconnection.'), + NM_DEVICE_STATE_REASON_CARRIER: + _("The device's carrier/link changed.")} + + return _nm_device_state_reason_description[reason] + + +def frequency_to_channel(frequency): + """Returns the channel matching a given radio channel frequency. If a + frequency is not in the dictionary channel 1 will be returned. + + Keyword arguments: + frequency -- The radio channel frequency in MHz. + + Return: Channel + + """ + ftoc = {2412: 1, 2417: 2, 2422: 3, 2427: 4, + 2432: 5, 2437: 6, 2442: 7, 2447: 8, + 2452: 9, 2457: 10, 2462: 11, 2467: 12, + 2472: 13} + if frequency not in ftoc: + logging.warning('The frequency %s can not be mapped to a channel, ' + 'defaulting to channel 1.', frequency) + return 1 + return ftoc[frequency] + + +def is_sugar_adhoc_network(ssid): + """Checks whether an access point is a sugar Ad-hoc network. + + Keyword arguments: + ssid -- Ssid of the access point. + + Return: Boolean + + """ + return ssid.startswith('Ad-hoc Network') + + class WirelessSecurity(object): def __init__(self): self.key_mgmt = None @@ -118,14 +301,16 @@ class WirelessSecurity(object): wireless_security['group'] = self.group return wireless_security + class Wireless(object): - nm_name = "802-11-wireless" + nm_name = '802-11-wireless' def __init__(self): self.ssid = None self.security = None self.mode = None self.band = None + self.channel = None def get_dict(self): wireless = {'ssid': self.ssid} @@ -135,11 +320,13 @@ class Wireless(object): wireless['mode'] = self.mode if self.band: wireless['band'] = self.band + if self.channel: + wireless['channel'] = self.channel return wireless class OlpcMesh(object): - nm_name = "802-11-olpc-mesh" + nm_name = '802-11-olpc-mesh' def __init__(self, channel, anycast_addr): self.channel = channel @@ -147,12 +334,12 @@ class OlpcMesh(object): def get_dict(self): ret = { - "ssid": dbus.ByteArray("olpc-mesh"), - "channel": self.channel, + 'ssid': dbus.ByteArray('olpc-mesh'), + 'channel': self.channel, } if self.anycast_addr: - ret["dhcp-anycast-address"] = dbus.ByteArray(self.anycast_addr) + ret['dhcp-anycast-address'] = dbus.ByteArray(self.anycast_addr) return ret @@ -173,6 +360,7 @@ class Connection(object): connection['timestamp'] = self.timestamp return connection + class IP4Config(object): def __init__(self): self.method = None @@ -183,6 +371,7 @@ class IP4Config(object): ip4_config['method'] = self.method return ip4_config + class Serial(object): def __init__(self): self.baud = None @@ -195,6 +384,7 @@ class Serial(object): return serial + class Ppp(object): def __init__(self): pass @@ -203,6 +393,7 @@ class Ppp(object): ppp = {} return ppp + class Gsm(object): def __init__(self): self.apn = None @@ -221,6 +412,7 @@ class Gsm(object): return gsm + class Settings(object): def __init__(self, wireless_cfg=None): self.connection = Connection() @@ -243,6 +435,7 @@ class Settings(object): settings['ipv4'] = self.ip4_config.get_dict() return settings + class Secrets(object): def __init__(self, settings): self.settings = settings @@ -268,6 +461,7 @@ class Secrets(object): return settings + class SettingsGsm(object): def __init__(self): self.connection = Connection() @@ -287,22 +481,24 @@ class SettingsGsm(object): return settings + class SecretsGsm(object): def __init__(self): self.password = None self.pin = None self.puk = None - + def get_dict(self): secrets = {} if self.password is not None: secrets['password'] = self.password if self.pin is not None: secrets['pin'] = self.pin - if self.puk is not None: + if self.puk is not None: secrets['puk'] = self.puk return {'gsm': secrets} + class NMSettings(dbus.service.Object): def __init__(self): bus = dbus.SystemBus() @@ -330,10 +526,19 @@ class NMSettings(dbus.service.Object): self.secrets_request.send(self, connection=sender, response=kwargs['response']) + def clear_wifi_connections(self): + for uuid in self.connections.keys(): + conn = self.connections[uuid] + if conn._settings.connection.type == \ + NM_CONNECTION_TYPE_802_11_WIRELESS: + conn.Removed() + self.connections.pop(uuid) + + class SecretsResponse(object): - ''' Intermediate object to report the secrets from the dialog + """Intermediate object to report the secrets from the dialog back to the connection object and which will inform NM - ''' + """ def __init__(self, connection, reply_cb, error_cb): self._connection = connection self._reply_cb = reply_cb @@ -346,6 +551,7 @@ class SecretsResponse(object): def set_error(self, error): self._error_cb(error) + class NMSettingsConnection(dbus.service.Object): def __init__(self, path, settings, secrets): bus = dbus.SystemBus() @@ -358,27 +564,57 @@ class NMSettingsConnection(dbus.service.Object): self._settings = settings self._secrets = secrets + @dbus.service.signal(dbus_interface=NM_CONNECTION_IFACE, + signature='') + def Removed(self): + pass + + @dbus.service.signal(dbus_interface=NM_CONNECTION_IFACE, + signature='a{sa{sv}}') + def Updated(self, settings): + pass + def set_connected(self): if self._settings.connection.type == NM_CONNECTION_TYPE_GSM: self._settings.connection.timestamp = int(time.time()) - else: - if not self._settings.connection.autoconnect: - self._settings.connection.autoconnect = True - self._settings.connection.timestamp = int(time.time()) - if self._settings.connection.type == NM_CONNECTION_TYPE_802_11_WIRELESS: - self.save() + elif not self._settings.connection.autoconnect: + self._settings.connection.autoconnect = True + self._settings.connection.timestamp = int(time.time()) + if (self._settings.connection.type == + NM_CONNECTION_TYPE_802_11_WIRELESS): + self.Updated(self._settings.get_dict()) + self.save() + + try: + # try to flush resolver cache - SL#1940 + # ctypes' syntactic sugar does not work + # so we must get the func ptr explicitly + libc = ctypes.CDLL('libc.so.6') + res_init = getattr(libc, '__res_init') + res_init(None) + except: + # pylint: disable=W0702 + logging.exception('Error calling libc.__res_init') + + def disable_autoconnect(self): + if self._settings.connection.type != NM_CONNECTION_TYPE_GSM and \ + self._settings.connection.autoconnect: + self._settings.connection.autoconnect = False + self._settings.connection.timestamp = None + self.Updated(self._settings.get_dict()) + self.save() def set_secrets(self, secrets): self._secrets = secrets - if self._settings.connection.type == NM_CONNECTION_TYPE_802_11_WIRELESS: + if self._settings.connection.type == \ + NM_CONNECTION_TYPE_802_11_WIRELESS: self.save() def get_settings(self): return self._settings def save(self): - profile_path = env.get_profile_path() - config_path = os.path.join(profile_path, 'nm', 'connections.cfg') + config_path = _get_wifi_connections_path() config = ConfigParser.ConfigParser() try: @@ -443,22 +679,31 @@ class NMSettingsConnection(dbus.service.Object): in_signature='sasb', out_signature='a{sa{sv}}') def GetSecrets(self, setting_name, hints, request_new, reply, error): logging.debug('Secrets requested for connection %s request_new=%s', - self.path, request_new) - if request_new or self._secrets is None: - # request_new is for example the case when the pw on the AP changes - response = SecretsResponse(self, reply, error) - try: - self.secrets_request.send(self, response=response) - except Exception: - logging.exception('Error requesting the secrets via dialog') + self.path, request_new) + if self._settings.connection.type is not NM_CONNECTION_TYPE_GSM: + if request_new or self._secrets is None: + # request_new is for example the case when the pw on the AP + # changes + response = SecretsResponse(self, reply, error) + try: + self.secrets_request.send(self, response=response) + except Exception: + logging.exception('Error requesting the secrets via' + ' dialog') + else: + reply(self._secrets.get_dict()) else: - reply(self._secrets.get_dict()) + if not request_new: + reply(self._secrets.get_dict()) + else: + raise Exception('The stored GSM secret has already been' + ' supplied') class AccessPoint(gobject.GObject): __gsignals__ = { 'props-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])) + ([gobject.TYPE_PYOBJECT])), } def __init__(self, device, model): @@ -475,10 +720,10 @@ class AccessPoint(gobject.GObject): self.wpa_flags = 0 self.rsn_flags = 0 self.mode = 0 + self.channel = 0 def initialize(self): - model_props = dbus.Interface(self.model, - 'org.freedesktop.DBus.Properties') + model_props = dbus.Interface(self.model, dbus.PROPERTIES_IFACE) model_props.GetAll(NM_ACCESSPOINT_IFACE, byte_arrays=True, reply_handler=self._ap_properties_changed_cb, error_handler=self._get_all_props_error_cb) @@ -523,7 +768,7 @@ class AccessPoint(gobject.GObject): else: fl |= 1 << 6 - hashstr = str(fl) + "@" + self.name + hashstr = str(fl) + '@' + self.name return hash(hashstr) def _update_properties(self, properties): @@ -544,6 +789,9 @@ class AccessPoint(gobject.GObject): self.rsn_flags = properties['RsnFlags'] if 'Mode' in properties: self.mode = properties['Mode'] + if 'Frequency' in properties: + self.channel = frequency_to_channel(properties['Frequency']) + self._initialized = True self.emit('props-changed', old_hash) @@ -559,6 +807,7 @@ class AccessPoint(gobject.GObject): path=self.model.object_path, dbus_interface=NM_ACCESSPOINT_IFACE) + def get_settings(): global _nm_settings if _nm_settings is None: @@ -569,17 +818,20 @@ def get_settings(): load_connections() return _nm_settings + def find_connection_by_ssid(ssid): connections = get_settings().connections for conn_index in connections: connection = connections[conn_index] - if connection._settings.connection.type == NM_CONNECTION_TYPE_802_11_WIRELESS: - if connection._settings.wireless.ssid == ssid: - return connection + if connection._settings.connection.type == \ + NM_CONNECTION_TYPE_802_11_WIRELESS and \ + connection._settings.wireless.ssid == ssid: + return connection return None + def add_connection(uuid, settings, secrets=None): global _conn_counter @@ -590,19 +842,26 @@ def add_connection(uuid, settings, secrets=None): _nm_settings.add_connection(uuid, conn) return conn -def load_wifi_connections(): + +def _get_wifi_connections_path(): profile_path = env.get_profile_path() - config_path = os.path.join(profile_path, 'nm', 'connections.cfg') + return os.path.join(profile_path, 'nm', 'connections.cfg') - config = ConfigParser.ConfigParser() + +def _create_wifi_connections(config_path): + if not os.path.exists(os.path.dirname(config_path)): + os.makedirs(os.path.dirname(config_path), 0755) + f = open(config_path, 'w') + f.close() + + +def load_wifi_connections(): + config_path = _get_wifi_connections_path() if not os.path.exists(config_path): - if not os.path.exists(os.path.dirname(config_path)): - os.makedirs(os.path.dirname(config_path), 0755) - f = open(config_path, 'w') - config.write(f) - f.close() + _create_wifi_connections(config_path) + config = ConfigParser.ConfigParser() try: if not config.read(config_path): logging.error('Error reading the nm config file') @@ -672,10 +931,10 @@ def load_gsm_connection(): if username and number and apn: settings = SettingsGsm() - settings.gsm.username = username + settings.gsm.username = username settings.gsm.number = number settings.gsm.apn = apn - + secrets = SecretsGsm() secrets.pin = pin secrets.puk = puk @@ -693,12 +952,14 @@ def load_gsm_connection(): except Exception: logging.exception('Error adding gsm connection to NMSettings.') else: - logging.exception("No gsm connection was set in GConf.") + logging.warning('No gsm connection was set in GConf.') + def load_connections(): load_wifi_connections() load_gsm_connection() + def find_gsm_connection(): connections = get_settings().connections @@ -708,3 +969,39 @@ def find_gsm_connection(): logging.debug('There is no gsm connection in the NMSettings.') return None + + +def have_wifi_connections(): + return bool(get_settings().connections) + + +def clear_wifi_connections(): + if _nm_settings is not None: + _nm_settings.clear_wifi_connections() + + config_path = _get_wifi_connections_path() + _create_wifi_connections(config_path) + + +def disconnect_access_points(ap_paths): + """ + Disconnect all devices connected to any of the given access points. + """ + bus = dbus.SystemBus() + netmgr_obj = bus.get_object(NM_SERVICE, NM_PATH) + netmgr = dbus.Interface(netmgr_obj, NM_IFACE) + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + active_connection_paths = netmgr_props.Get(NM_IFACE, 'ActiveConnections') + + for conn_path in active_connection_paths: + conn_obj = bus.get_object(NM_IFACE, conn_path) + conn_props = dbus.Interface(conn_obj, dbus.PROPERTIES_IFACE) + ap_path = conn_props.Get(NM_ACTIVE_CONN_IFACE, 'SpecificObject') + if ap_path == '/' or ap_path not in ap_paths: + continue + + dev_paths = conn_props.Get(NM_ACTIVE_CONN_IFACE, 'Devices') + for dev_path in dev_paths: + dev_obj = bus.get_object(NM_SERVICE, dev_path) + dev = dbus.Interface(dev_obj, NM_DEVICE_IFACE) + dev.Disconnect() diff --git a/src/jarabe/model/notifications.py b/src/jarabe/model/notifications.py index 10a0237..ec14056 100644 --- a/src/jarabe/model/notifications.py +++ b/src/jarabe/model/notifications.py @@ -23,9 +23,13 @@ from sugar import dispatch from jarabe import config -_DBUS_SERVICE = "org.freedesktop.Notifications" -_DBUS_IFACE = "org.freedesktop.Notifications" -_DBUS_PATH = "/org/freedesktop/Notifications" + +_DBUS_SERVICE = 'org.freedesktop.Notifications' +_DBUS_IFACE = 'org.freedesktop.Notifications' +_DBUS_PATH = '/org/freedesktop/Notifications' + +_instance = None + class NotificationService(dbus.service.Object): def __init__(self): @@ -43,7 +47,8 @@ class NotificationService(dbus.service.Object): hints, expire_timeout): logging.debug('Received notification: %r', [app_name, replaces_id, - app_icon, summary, body, actions, hints, expire_timeout]) + '<app_icon>', summary, body, actions, '<hints>', + expire_timeout]) if replaces_id > 0: notification_id = replaces_id @@ -73,16 +78,14 @@ class NotificationService(dbus.service.Object): def GetServerInformation(self, name, vendor, version): return 'Sugar Shell', 'Sugar', config.version - - @dbus.service.signal(_DBUS_IFACE, signature="uu") + @dbus.service.signal(_DBUS_IFACE, signature='uu') def NotificationClosed(self, notification_id, reason): pass - @dbus.service.signal(_DBUS_IFACE, signature="us") + @dbus.service.signal(_DBUS_IFACE, signature='us') def ActionInvoked(self, notification_id, action_key): pass -_instance = None def get_service(): global _instance @@ -90,6 +93,6 @@ def get_service(): _instance = NotificationService() return _instance + def init(): get_service() - diff --git a/src/jarabe/model/olpcmesh.py b/src/jarabe/model/olpcmesh.py index cbd7ddd..f070100 100644 --- a/src/jarabe/model/olpcmesh.py +++ b/src/jarabe/model/olpcmesh.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 One Laptop per Child +# Copyright (C) 2009, 2010 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 @@ -24,13 +24,14 @@ from jarabe.model.network import Settings from jarabe.model.network import OlpcMesh as OlpcMeshSettings from sugar.util import unique_id + _NM_SERVICE = 'org.freedesktop.NetworkManager' _NM_IFACE = 'org.freedesktop.NetworkManager' _NM_PATH = '/org/freedesktop/NetworkManager' _NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' _NM_OLPC_MESH_IFACE = 'org.freedesktop.NetworkManager.Device.OlpcMesh' -_XS_ANYCAST = "\xc0\x27\xc0\x27\xc0\x00" +_XS_ANYCAST = '\xc0\x27\xc0\x27\xc0\x00' DEVICE_STATE_UNKNOWN = 0 DEVICE_STATE_UNMANAGED = 1 @@ -43,6 +44,7 @@ DEVICE_STATE_IP_CONFIG = 7 DEVICE_STATE_ACTIVATED = 8 DEVICE_STATE_FAILED = 9 + class OlpcMeshManager(object): def __init__(self, mesh_device): self._bus = dbus.SystemBus() @@ -56,14 +58,12 @@ class OlpcMeshManager(object): """ - props = dbus.Interface(self.mesh_device, - 'org.freedesktop.DBus.Properties') + props = dbus.Interface(self.mesh_device, dbus.PROPERTIES_IFACE) props.Get(_NM_DEVICE_IFACE, 'State', reply_handler=self.__get_mesh_state_reply_cb, error_handler=self.__get_state_error_cb) - props = dbus.Interface(self.eth_device, - 'org.freedesktop.DBus.Properties') + props = dbus.Interface(self.eth_device, dbus.PROPERTIES_IFACE) props.Get(_NM_DEVICE_IFACE, 'State', reply_handler=self.__get_eth_state_reply_cb, error_handler=self.__get_state_error_cb) @@ -83,13 +83,12 @@ class OlpcMeshManager(object): self._eth_device_state = DEVICE_STATE_UNKNOWN if self._have_configured_connections(): - self._start_automesh() - else: self._start_automesh_timer() + else: + self._start_automesh() def _get_companion_device(self): - props = dbus.Interface(self.mesh_device, - 'org.freedesktop.DBus.Properties') + props = dbus.Interface(self.mesh_device, dbus.PROPERTIES_IFACE) eth_device_o = props.Get(_NM_OLPC_MESH_IFACE, 'Companion') return self._bus.get_object(_NM_SERVICE, eth_device_o) @@ -119,7 +118,7 @@ class OlpcMeshManager(object): def __eth_device_state_changed_cb(self, new_state, old_state, reason): """If a connection is activated on the eth device, stop trying our automatic connections. - + """ self._eth_device_state = new_state self._maybe_schedule_idle_check() @@ -147,7 +146,7 @@ class OlpcMeshManager(object): def _idle_check(self): if self._mesh_device_state == DEVICE_STATE_DISCONNECTED \ and self._eth_device_state == DEVICE_STATE_DISCONNECTED: - logging.debug("starting automesh due to inactivity") + logging.debug('starting automesh due to inactivity') self._start_automesh() return False @@ -170,8 +169,8 @@ class OlpcMeshManager(object): logging.error('Failed to activate connection: %s', err) def _activate_connection(self, channel, anycast_address=None): - logging.debug("activate channel %d anycast %s", - (channel, repr(anycast_address))) + logging.debug('activate channel %d anycast %r', + channel, anycast_address) proxy = self._bus.get_object(_NM_SERVICE, _NM_PATH) network_manager = dbus.Interface(proxy, _NM_IFACE) connection = self._make_connection(channel, anycast_address) @@ -211,4 +210,3 @@ class OlpcMeshManager(object): self._connection_queue.append((6, _XS_ANYCAST)) self._connection_queue.append((1, _XS_ANYCAST)) self._try_next_connection_from_queue() - diff --git a/src/jarabe/model/owner.py b/src/jarabe/model/owner.py deleted file mode 100644 index 17996e6..0000000 --- a/src/jarabe/model/owner.py +++ /dev/null @@ -1,113 +0,0 @@ -# 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 gconf -import simplejson - -from telepathy.interfaces import CHANNEL_TYPE_TEXT - -from sugar import env -from sugar.presence import presenceservice -from sugar import util -from jarabe.model.invites import Invites - -class Owner(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) - - client = gconf.client_get_default() - self._nick = client.get_string("/desktop/sugar/user/nick") - - 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 hashlib - digest = hashlib.md5(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) - -_model = None - -def get_model(): - global _model - if _model is None: - _model = Owner() - return _model diff --git a/src/jarabe/model/screen.py b/src/jarabe/model/screen.py index 4403c1c..7d34d45 100644 --- a/src/jarabe/model/screen.py +++ b/src/jarabe/model/screen.py @@ -18,12 +18,14 @@ import logging import dbus + _HARDWARE_MANAGER_INTERFACE = 'org.freedesktop.ohm.Keystore' _HARDWARE_MANAGER_SERVICE = 'org.freedesktop.ohm' _HARDWARE_MANAGER_OBJECT_PATH = '/org/freedesktop/ohm/Keystore' _ohm_service = None + def _get_ohm(): global _ohm_service if _ohm_service is None: @@ -35,9 +37,9 @@ def _get_ohm(): return _ohm_service + def set_dcon_freeze(frozen): try: - _get_ohm().SetKey("display.dcon_freeze", frozen) + _get_ohm().SetKey('display.dcon_freeze', frozen) except dbus.DBusException: logging.error('Cannot unfreeze the DCON') - diff --git a/src/jarabe/model/session.py b/src/jarabe/model/session.py index 9e0f087..9b277ff 100644 --- a/src/jarabe/model/session.py +++ b/src/jarabe/model/session.py @@ -24,8 +24,10 @@ import logging from sugar import session from sugar import env + _session_manager = None + class SessionManager(session.SessionManager): MODE_LOGOUT = 0 MODE_SHUTDOWN = 1 @@ -53,15 +55,15 @@ class SessionManager(session.SessionManager): elif self._logout_mode != self.MODE_LOGOUT: try: bus = dbus.SystemBus() - proxy = bus.get_object('org.freedesktop.Hal', - '/org/freedesktop/Hal/devices/computer') + proxy = bus.get_object('org.freedesktop.ConsoleKit', + '/org/freedesktop/ConsoleKit/Manager') pm = dbus.Interface(proxy, - 'org.freedesktop.Hal.Device.SystemPowerManagement') + 'org.freedesktop.ConsoleKit.Manager') if self._logout_mode == self.MODE_SHUTDOWN: - pm.Shutdown() + pm.Stop() elif self._logout_mode == self.MODE_REBOOT: - pm.Reboot() + pm.Restart() except: logging.exception('Can not stop sugar') self.session.cancel_shutdown() @@ -73,14 +75,15 @@ class SessionManager(session.SessionManager): def _close_emulator(self): gtk.main_quit() - if os.environ.has_key('SUGAR_EMULATOR_PID'): + if 'SUGAR_EMULATOR_PID' in os.environ: pid = int(os.environ['SUGAR_EMULATOR_PID']) os.kill(pid, signal.SIGTERM) - # Need to call this ASAP so the atexit handlers get called before we get - # killed by the X (dis)connection + # Need to call this ASAP so the atexit handlers get called before we + # get killed by the X (dis)connection sys.exit() + def get_session_manager(): global _session_manager diff --git a/src/jarabe/model/shell.py b/src/jarabe/model/shell.py index e03e0f7..63f6173 100644 --- a/src/jarabe/model/shell.py +++ b/src/jarabe/model/shell.py @@ -27,13 +27,16 @@ import dbus from sugar import wm from sugar import dispatch from sugar.graphics.xocolor import XoColor -from sugar.presence import presenceservice from jarabe.model.bundleregistry import get_registry +from jarabe.model import neighborhood + +_SERVICE_NAME = 'org.laptop.Activity' +_SERVICE_PATH = '/org/laptop/Activity' +_SERVICE_INTERFACE = 'org.laptop.Activity' + +_model = None -_SERVICE_NAME = "org.laptop.Activity" -_SERVICE_PATH = "/org/laptop/Activity" -_SERVICE_INTERFACE = "org.laptop.Activity" class Activity(gobject.GObject): """Activity which appears in the "Home View" of the Sugar shell @@ -46,10 +49,9 @@ class Activity(gobject.GObject): __gtype_name__ = 'SugarHomeActivity' - __gproperties__ = { - 'launching' : (bool, None, None, False, - gobject.PARAM_READWRITE), - } + LAUNCHING = 0 + LAUNCH_FAILED = 1 + LAUNCHED = 2 def __init__(self, activity_info, activity_id, window=None): """Initialise the HomeActivity @@ -60,19 +62,20 @@ class Activity(gobject.GObject): the "type" of activity being created. activity_id -- unique identifier for this instance of the activity type - window -- Main WnckWindow of the activity + _windows -- WnckWindows registered for the activity. The lowest + one in the stack is the main window. """ gobject.GObject.__init__(self) - self._window = None + self._windows = [] self._service = None self._activity_id = activity_id self._activity_info = activity_info self._launch_time = time.time() - self._launching = True + self._launch_status = Activity.LAUNCHING if window is not None: - self.set_window(window) + self.add_window(window) self._retrieve_service() @@ -81,18 +84,32 @@ class Activity(gobject.GObject): 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") + signal_name='NameOwnerChanged', + dbus_interface='org.freedesktop.DBus') - def set_window(self, window): - """Set the window for the activity + self._launch_completed_hid = get_model().connect('launch-completed', + self.__launch_completed_cb) + self._launch_failed_hid = get_model().connect('launch-failed', + self.__launch_failed_cb) - We allow resetting the window for an activity so that we - can replace the launcher once we get its real window. - """ + def get_launch_status(self): + return self._launch_status + + launch_status = gobject.property(getter=get_launch_status) + + def add_window(self, window): + """Add a window to the windows stack.""" if not window: - raise ValueError("window must be valid") - self._window = window + raise ValueError('window must be valid') + self._windows.append(window) + + def remove_window_by_xid(self, xid): + """Remove a window from the windows stack.""" + for wnd in self._windows: + if wnd.get_xid() == xid: + self._windows.remove(wnd) + return True + return False def get_service(self): """Get the activity service @@ -106,8 +123,8 @@ class Activity(gobject.GObject): def get_title(self): """Retrieve the application's root window's suggested title""" - if self._window: - return self._window.get_name() + if self._windows: + return self._windows[0].get_name() else: return '' @@ -135,21 +152,19 @@ class Activity(gobject.GObject): have an entry (implying that this is not a Sugar-shared application) uses the local user's profile 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: + for act in neighborhood.get_model().get_activities(): + if self._activity_id == act.activity_id: activity = act break if activity != None: - return XoColor(activity.props.color) + return activity.props.color else: client = gconf.client_get_default() - return XoColor(client.get_string("/desktop/sugar/user/color")) + return XoColor(client.get_string('/desktop/sugar/user/color')) def get_activity_id(self): """Retrieve the "activity_id" passed in to our constructor @@ -161,31 +176,44 @@ class Activity(gobject.GObject): def get_xid(self): """Retrieve the X-windows ID of our root window""" - if self._window is not None: - return self._window.get_xid() + if self._windows: + return self._windows[0].get_xid() else: return None + def has_xid(self, xid): + """Check if an X-window with the given xid is in the windows stack""" + if self._windows: + for wnd in self._windows: + if wnd.get_xid() == xid: + return True + return False + def get_window(self): """Retrieve the X-windows root window of this application - This was stored by the set_window method, which was + This was stored by the add_window method, which was called by HomeModel._add_activity, which was called via a callback that looks for all 'window-opened' events. + We keep a stack of the windows. The lowest window in the + stack that is still valid we consider the main one. + HomeModel currently uses a dbus service query on the activity to determine to which HomeActivity the newly launched window belongs. """ - return self._window + if self._windows: + return self._windows[0] + return None def get_type(self): """Retrieve the activity bundle id for future reference""" - if self._window is None: + if not self._windows: return None else: - return wm.get_bundle_id(self._window) + return wm.get_bundle_id(self._windows[0]) def is_journal(self): """Returns boolean if the activity is of type JournalActivity""" @@ -201,7 +229,9 @@ class Activity(gobject.GObject): def get_pid(self): """Returns the activity's PID""" - return self._window.get_pid() + if not self._windows: + return None + return self._windows[0].get_pid() def get_bundle_path(self): """Returns the activity's bundle directory""" @@ -220,18 +250,10 @@ class Activity(gobject.GObject): 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() + if self._windows[0].get_xid() and activity.get_xid(): + return self._windows[0].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 @@ -245,17 +267,24 @@ class Activity(gobject.GObject): try: bus = dbus.SessionBus() proxy = bus.get_object(self._get_service_name(), - _SERVICE_PATH + "/" + self._activity_id) + _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 + if old and not new: + logging.debug('Activity._name_owner_changed_cb: ' \ + 'activity %s went away', name) + self._name_owner_changed_handler.remove() + self._name_owner_changed_handler = None + self._service = None + elif not old and new: + logging.debug('Activity._name_owner_changed_cb: ' \ + 'activity %s started up', name) + self._retrieve_service() + self.set_active(True) def set_active(self, state): """Propagate the current state to the activity object""" @@ -268,7 +297,24 @@ class Activity(gobject.GObject): pass def _set_active_error(self, err): - logging.error("set_active() failed: %s", err) + logging.error('set_active() failed: %s', err) + + def _set_launch_status(self, value): + get_model().disconnect(self._launch_completed_hid) + get_model().disconnect(self._launch_failed_hid) + self._launch_completed_hid = None + self._launch_failed_hid = None + self._launch_status = value + self.notify('launch_status') + + def __launch_completed_cb(self, model, home_activity): + if home_activity is self: + self._set_launch_status(Activity.LAUNCHED) + + def __launch_failed_cb(self, model, home_activity): + if home_activity is self: + self._set_launch_status(Activity.LAUNCH_FAILED) + class ShellModel(gobject.GObject): """Model of the shell (activity management) @@ -286,27 +332,22 @@ class ShellModel(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])), + '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])), + ([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])) + 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])), } ZOOM_MESH = 0 @@ -331,10 +372,20 @@ class ShellModel(gobject.GObject): self._activities = [] self._active_activity = None self._tabbing_activity = None - self._pservice = presenceservice.get_instance() + self._launchers = {} self._screen.toggle_showing_desktop(True) + def get_launcher(self, activity_id): + return self._launchers.get(str(activity_id)) + + def register_launcher(self, activity_id, launcher): + self._launchers[activity_id] = launcher + + def unregister_launcher(self, activity_id): + if activity_id in self._launchers: + del self._launchers[activity_id] + def _update_zoom_level(self, window): if window.get_window_type() == wnck.WINDOW_DIALOG: return @@ -426,7 +477,7 @@ class ShellModel(gobject.GObject): 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) + self.emit('tabbing-activity-changed', self._tabbing_activity) def _set_active_activity(self, home_activity): if self._active_activity == home_activity: @@ -454,6 +505,21 @@ class ShellModel(gobject.GObject): return self._activities.index(obj) def _window_opened_cb(self, screen, window): + """Handle the callback for the 'window opened' event. + + Most activities will register 2 windows during + their lifetime: the launcher window, and the 'main' + app window. + + When the main window appears, we send a signal to + the launcher window to close. + + Some activities (notably non-native apps) open several + windows during their lifetime, switching from one to + the next as the 'main' window. We use a stack to track + them. + + """ if window.get_window_type() == wnck.WINDOW_NORMAL: home_activity = None @@ -476,31 +542,36 @@ class ShellModel(gobject.GObject): window.maximize() if not home_activity: + logging.debug('first window registered for %s' % activity_id) home_activity = Activity(activity_info, activity_id, window) self._add_activity(home_activity) else: - home_activity.set_window(window) - - if wm.get_sugar_window_type(window) != 'launcher': - home_activity.props.launching = False - if not home_activity.is_journal(): - self.emit('launch-completed', home_activity) + logging.debug('window registered for %s' % activity_id) + home_activity.add_window(window) + if wm.get_sugar_window_type(window) != 'launcher' \ + and home_activity.get_launch_status() == Activity.LAUNCHING: + self.emit('launch-completed', home_activity) startup_time = time.time() - home_activity.get_launch_time() - logging.debug('%s launched in %f seconds.', - home_activity.get_type(), startup_time) + logging.debug('%s launched in %f seconds.' % + (activity_id, startup_time)) 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: - if self._get_activity_by_xid(window.get_xid()) is not None: - self._remove_activity_by_xid(window.get_xid()) + xid = window.get_xid() + activity = self._get_activity_by_xid(xid) + if activity is not None: + activity.remove_window_by_xid(xid) + if activity.get_window() is None: + logging.debug('last window gone - remove activity %s' % activity) + self._remove_activity(activity) def _get_activity_by_xid(self, xid): for home_activity in self._activities: - if home_activity.get_xid() == xid: + if home_activity.has_xid(xid): return home_activity return None @@ -545,13 +616,6 @@ class ShellModel(gobject.GObject): 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_bundle(service_name) @@ -560,7 +624,6 @@ class ShellModel(gobject.GObject): " was not found in the bundle registry." % service_name) home_activity = Activity(activity_info, activity_id) - home_activity.props.launching = True self._add_activity(home_activity) self._set_active_activity(home_activity) @@ -575,11 +638,12 @@ class ShellModel(gobject.GObject): 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, + logging.debug('Activity %s (%s) launch failed', activity_id, home_activity.get_type()) - if home_activity.props.launching: + if self.get_launcher(activity_id) is not None: self.emit('launch-failed', home_activity) else: + # activity sent failure notification after closing launcher self._remove_activity(home_activity) else: logging.error('Model for activity id %s does not exist.', @@ -592,18 +656,15 @@ class ShellModel(gobject.GObject): logging.debug('Activity %s has been closed already.', activity_id) return False - if home_activity.props.launching: + if self.get_launcher(activity_id) is not None: logging.debug('Activity %s still launching, assuming it failed.', activity_id) self.notify_launch_failed(activity_id) return False -_model = None - def get_model(): global _model if _model is None: _model = ShellModel() return _model - diff --git a/src/jarabe/model/sound.py b/src/jarabe/model/sound.py index 65090a4..9e1e748 100644 --- a/src/jarabe/model/sound.py +++ b/src/jarabe/model/sound.py @@ -20,17 +20,23 @@ from sugar import env from sugar import _sugarext from sugar import dispatch + VOLUME_STEP = 10 muted_changed = dispatch.Signal() volume_changed = dispatch.Signal() +_volume = _sugarext.VolumeAlsa() + + def get_muted(): return _volume.get_mute() + def get_volume(): return _volume.get_volume() + def set_volume(new_volume): old_volume = _volume.get_volume() _volume.set_volume(new_volume) @@ -38,6 +44,7 @@ def set_volume(new_volume): volume_changed.send(None) save() + def set_muted(new_state): old_state = _volume.get_mute() _volume.set_mute(new_state) @@ -45,14 +52,14 @@ def set_muted(new_state): muted_changed.send(None) save() + def save(): if env.is_emulator() is False: client = gconf.client_get_default() client.set_int('/desktop/sugar/sound/volume', get_volume()) + def restore(): if env.is_emulator() is False: client = gconf.client_get_default() set_volume(client.get_int('/desktop/sugar/sound/volume')) - -_volume = _sugarext.VolumeAlsa() diff --git a/src/jarabe/model/telepathyclient.py b/src/jarabe/model/telepathyclient.py new file mode 100644 index 0000000..c6fbac1 --- /dev/null +++ b/src/jarabe/model/telepathyclient.py @@ -0,0 +1,103 @@ +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> +# +# 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 +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import CLIENT, \ + CLIENT_APPROVER, \ + CLIENT_HANDLER, \ + CLIENT_INTERFACE_REQUESTS +from telepathy.server import DBusProperties + +from sugar import dispatch + + +SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +_instance = None + + +class TelepathyClient(dbus.service.Object, DBusProperties): + def __init__(self): + self._interfaces = set([CLIENT, CLIENT_HANDLER, + CLIENT_INTERFACE_REQUESTS, PROPERTIES_IFACE, + CLIENT_APPROVER]) + + bus = dbus.Bus() + bus_name = dbus.service.BusName(SUGAR_CLIENT_SERVICE, bus=bus) + + dbus.service.Object.__init__(self, bus_name, SUGAR_CLIENT_PATH) + DBusProperties.__init__(self) + + self._implement_property_get(CLIENT, { + 'Interfaces': lambda: list(self._interfaces), + }) + self._implement_property_get(CLIENT_HANDLER, { + 'HandlerChannelFilter': self.__get_filters_cb, + }) + self._implement_property_get(CLIENT_APPROVER, { + 'ApproverChannelFilter': self.__get_filters_cb, + }) + + self.got_channel = dispatch.Signal() + self.got_dispatch_operation = dispatch.Signal() + + def __get_filters_cb(self): + logging.debug('__get_filters_cb') + filter_dict = dbus.Dictionary({}, signature='sv') + return dbus.Array([filter_dict], signature='a{sv}') + + @dbus.service.method(dbus_interface=CLIENT_HANDLER, + in_signature='ooa(oa{sv})aota{sv}', out_signature='') + def HandleChannels(self, account, connection, channels, requests_satisfied, + user_action_time, handler_info): + logging.debug('HandleChannels\n%r\n%r\n%r\n%r\n%r\n%r\n', account, + connection, channels, requests_satisfied, + user_action_time, handler_info) + for channel in channels: + self.got_channel.send(self, account=account, + connection=connection, channel=channel) + + @dbus.service.method(dbus_interface=CLIENT_INTERFACE_REQUESTS, + in_signature='oa{sv}', out_signature='') + def AddRequest(self, request, properties): + logging.debug('AddRequest\n%r\n%r', request, properties) + + @dbus.service.method(dbus_interface=CLIENT_APPROVER, + in_signature='a(oa{sv})oa{sv}', out_signature='', + async_callbacks=('success_cb', 'error_cb_')) + def AddDispatchOperation(self, channels, dispatch_operation_path, + properties, success_cb, error_cb_): + success_cb() + try: + logging.debug('AddDispatchOperation\n%r\n%r\n%r', channels, + dispatch_operation_path, properties) + + self.got_dispatch_operation.send(self, channels=channels, + dispatch_operation_path=dispatch_operation_path, + properties=properties) + except Exception, e: + logging.exception(e) + + +def get_instance(): + global _instance + if not _instance: + _instance = TelepathyClient() + return _instance diff --git a/src/jarabe/util/__init__.py b/src/jarabe/util/__init__.py index 1610dd0..9c80ecb 100644 --- a/src/jarabe/util/__init__.py +++ b/src/jarabe/util/__init__.py @@ -16,4 +16,3 @@ # 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/util/emulator.py b/src/jarabe/util/emulator.py index d9dca7f..fda1b59 100644 --- a/src/jarabe/util/emulator.py +++ b/src/jarabe/util/emulator.py @@ -20,6 +20,7 @@ import subprocess import sys import time from optparse import OptionParser +from gettext import gettext as _ import gtk import gobject @@ -29,36 +30,37 @@ from sugar import env ERROR_NO_DISPLAY = 30 ERROR_NO_SERVER = 31 +default_dimensions = (800, 600) -default_dimensions = (800, 600) def _run_xephyr(display, dpi, dimensions, fullscreen): - cmd = [ 'Xephyr' ] + cmd = ['Xephyr'] cmd.append(':%d' % display) - cmd.append('-ac') + cmd.append('-ac') + cmd += ['-title', _('Sugar in a window')] screen_size = (gtk.gdk.screen_width(), gtk.gdk.screen_height()) if (not dimensions) and (fullscreen is None) and \ - (screen_size < default_dimensions) : + (screen_size <= default_dimensions): # no forced settings, screen too small => fit screen fullscreen = True - elif (not dimensions) : + elif not dimensions: # screen is big enough or user has en/disabled fullscreen manually # => use default size (will get ignored for fullscreen) dimensions = '%dx%d' % default_dimensions - if not dpi : + if not dpi: dpi = gtk.settings_get_default().get_property('gtk-xft-dpi') / 1024 - if fullscreen : + if fullscreen: cmd.append('-fullscreen') - if dimensions : + if dimensions: cmd.append('-screen') cmd.append(dimensions) - if dpi : + if dpi: cmd.append('-dpi') cmd.append('%d' % dpi) @@ -71,15 +73,13 @@ def _run_xephyr(display, dpi, dimensions, fullscreen): sys.stderr.write('Error executing server: %s\n' % (exc, )) return None - os.environ['DISPLAY'] = ":%d" % (display) - os.environ['SUGAR_EMULATOR_PID'] = str(pipe.pid) return pipe def _check_server(display): result = subprocess.call(['xdpyinfo', '-display', ':%d' % display], - stdout=open(os.devnull, "w"), - stderr=open(os.devnull, "w")) + stdout=open(os.devnull, 'w'), + stderr=open(os.devnull, 'w')) return result == 0 @@ -98,17 +98,17 @@ def _start_xephyr(dpi, dimensions, fullscreen): if not _check_server(display): pipe = _run_xephyr(display, dpi, dimensions, fullscreen) if pipe is None: - return None + return None, None for i_ in range(10): if _check_server(display): - return pipe + return pipe, display time.sleep(0.1) _kill_pipe(pipe) - return None + return None, None def _start_window_manager(): @@ -118,21 +118,31 @@ def _start_window_manager(): gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) -def _setup_env(): + +def _setup_env(display, scaling, emulator_pid): 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['MC_LOGFILE'] = os.path.join( + env.get_profile_path(), 'logs', 'mission-control.log') os.environ['STREAM_ENGINE_LOGFILE'] = os.path.join( env.get_profile_path(), 'logs', 'telepathy-stream-engine.log') + os.environ['DISPLAY'] = ':%d' % (display) + os.environ['SUGAR_EMULATOR_PID'] = emulator_pid + os.environ['MC_ACCOUNT_DIR'] = os.path.join( + env.get_profile_path(), 'accounts') + + if scaling: + os.environ['SUGAR_SCALING'] = scaling def main(): """Script-level operations""" parser = OptionParser() - parser.add_option('-d', '--dpi', dest='dpi', type="int", + parser.add_option('-d', '--dpi', dest='dpi', type='int', help='Emulator dpi') parser.add_option('-s', '--scaling', dest='scaling', help='Sugar scaling in %') @@ -150,16 +160,14 @@ def main(): sys.stderr.write('DISPLAY not set, cannot connect to host X server.\n') return ERROR_NO_DISPLAY - _setup_env() - - server = _start_xephyr(options.dpi, options.dimensions, options.fullscreen) + server, display = _start_xephyr(options.dpi, options.dimensions, + options.fullscreen) if server is None: sys.stderr.write('Failed to start server. Please check output above' ' for any error message.\n') return ERROR_NO_SERVER - if options.scaling: - os.environ['SUGAR_SCALING'] = options.scaling + _setup_env(display, options.scaling, str(server.pid)) command = ['dbus-launch', '--exit-with-session'] diff --git a/src/jarabe/util/telepathy/__init__.py b/src/jarabe/util/telepathy/__init__.py index 387d09c..eee4abb 100644 --- a/src/jarabe/util/telepathy/__init__.py +++ b/src/jarabe/util/telepathy/__init__.py @@ -16,4 +16,3 @@ # 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/util/telepathy/connection_watcher.py b/src/jarabe/util/telepathy/connection_watcher.py index 4a4c6e0..96af1cf 100644 --- a/src/jarabe/util/telepathy/connection_watcher.py +++ b/src/jarabe/util/telepathy/connection_watcher.py @@ -28,12 +28,16 @@ from telepathy.interfaces import CONN_INTERFACE from telepathy.constants import CONNECTION_STATUS_CONNECTED, \ CONNECTION_STATUS_DISCONNECTED + +_instance = None + + class ConnectionWatcher(gobject.GObject): __gsignals__ = { 'connection-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), 'connection-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])) + ([gobject.TYPE_PYOBJECT])), } def __init__(self, bus=None): @@ -93,14 +97,22 @@ class ConnectionWatcher(gobject.GObject): def get_connections(self): return self._connections.values() + +def get_instance(): + global _instance + if _instance is None: + _instance = ConnectionWatcher() + return _instance + + if __name__ == '__main__': dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) def connection_added_cb(conn_watcher, conn): - print "new connection", conn.service_name + print 'new connection', conn.service_name def connection_removed_cb(conn_watcher, conn): - print "removed connection", conn.service_name + print 'removed connection', conn.service_name watcher = ConnectionWatcher() watcher.connect('connection-added', connection_added_cb) diff --git a/src/jarabe/view/__init__.py b/src/jarabe/view/__init__.py index a9dd95a..85f6a24 100644 --- a/src/jarabe/view/__init__.py +++ b/src/jarabe/view/__init__.py @@ -13,4 +13,3 @@ # 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/buddyicon.py b/src/jarabe/view/buddyicon.py index 13edb2c..a274605 100644 --- a/src/jarabe/view/buddyicon.py +++ b/src/jarabe/view/buddyicon.py @@ -19,42 +19,45 @@ 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) + self._buddy.connect('notify::present', self.__buddy_notify_present_cb) + self._buddy.connect('notify::color', self.__buddy_notify_color_cb) - palette = BuddyMenu(buddy) - self.set_palette(palette) + self.palette_invoker.cache_palette = False self._update_color() - def _buddy_presence_change_cb(self, buddy, color=None): + def create_palette(self): + return BuddyMenu(self._buddy) + + def __buddy_notify_present_cb(self, buddy, pspec): # Update the icon's color when the buddy comes and goes self._update_color() - def _update_color(self): + def __buddy_notify_color_cb(self, buddy, pspec): + 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() + if palette is not None: + palette.props.icon.props.stroke_color = self.props.stroke_color + palette.props.icon.props.fill_color = self.props.fill_color else: self.props.xo_color = self._buddy.get_color() - palette_icon.props.xo_color = self._buddy.get_color() + if palette is not None: + palette.props.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 index 4637751..f824e70 100644 --- a/src/jarabe/view/buddymenu.py +++ b/src/jarabe/view/buddymenu.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> # # 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 @@ -19,6 +20,7 @@ from gettext import gettext as _ import gtk import gconf +import dbus from sugar.graphics.palette import Palette from sugar.graphics.menuitem import MenuItem @@ -28,6 +30,8 @@ from jarabe.model import shell from jarabe.model import friends from jarabe.model.session import get_session_manager from jarabe.controlpanel.gui import ControlPanel +import jarabe.desktop.homewindow + class BuddyMenu(Palette): def __init__(self, buddy): @@ -42,8 +46,7 @@ class BuddyMenu(Palette): 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) + self._buddy.connect('notify::nick', self.__buddy_notify_nick_cb) if buddy.is_owner(): self._add_my_items() @@ -54,8 +57,7 @@ class BuddyMenu(Palette): if self._active_activity_changed_hid is not None: home_model = shell.get_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) + self._buddy.disconnect_by_func(self.__buddy_notify_nick_cb) def _add_buddy_items(self): if friends.get_model().has_buddy(self._buddy): @@ -86,6 +88,12 @@ class BuddyMenu(Palette): client = gconf.client_get_default() + if client.get_bool('/desktop/sugar/show_restart'): + item = MenuItem(_('Restart'), 'system-restart') + item.connect('activate', self.__reboot_activate_cb) + self.menu.append(item) + item.show() + if client.get_bool('/desktop/sugar/show_logout'): item = MenuItem(_('Logout'), 'system-logout') item.connect('activate', self.__logout_activate_cb) @@ -97,17 +105,18 @@ class BuddyMenu(Palette): self.menu.append(item) item.show() + def _quit(self, action): + home_window = jarabe.desktop.homewindow.get_instance() + home_window.busy_during_delayed_action(action) + def __logout_activate_cb(self, menu_item): - session_manager = get_session_manager() - session_manager.logout() + self._quit(get_session_manager().logout) def __reboot_activate_cb(self, menu_item): - session_manager = get_session_manager() - session_manager.reboot() + self._quit(get_session_manager().reboot) def __shutdown_activate_cb(self, menu_item): - session_manager = get_session_manager() - session_manager.shutdown() + self._quit(get_session_manager().shutdown) def __controlpanel_activate_cb(self, menu_item): panel = ControlPanel() @@ -115,9 +124,9 @@ class BuddyMenu(Palette): panel.show() def _update_invite_menu(self, activity): - buddy_activity = self._buddy.get_current_activity() + buddy_activity = self._buddy.props.current_activity if buddy_activity is not None: - buddy_activity_id = buddy_activity.props.id + buddy_activity_id = buddy_activity.activity_id else: buddy_activity_id = None @@ -139,11 +148,8 @@ class BuddyMenu(Palette): 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 __buddy_notify_nick_cb(self, buddy, pspec): + self.set_primary_text(buddy.props.nick) def _make_friend_cb(self, menuitem): friends.get_model().make_friend(self._buddy) @@ -155,7 +161,17 @@ class BuddyMenu(Palette): activity = shell.get_model().get_active_activity() service = activity.get_service() if service: - buddy = self._buddy.get_buddy() - service.Invite(buddy.props.key) + try: + service.InviteContact(self._buddy.props.account, + self._buddy.props.contact_id) + except dbus.DBusException, e: + expected_exceptions = [ + 'org.freedesktop.DBus.Error.UnknownMethod', + 'org.freedesktop.DBus.Python.NotImplementedError'] + if e.get_dbus_name() in expected_exceptions: + logging.warning('Trying deprecated Activity.Invite') + service.Invite(self._buddy.props.key) + else: + raise else: logging.error('Invite failed, activity service not ') diff --git a/src/jarabe/view/keyhandler.py b/src/jarabe/view/keyhandler.py index 8c07975..d79bfe6 100644 --- a/src/jarabe/view/keyhandler.py +++ b/src/jarabe/view/keyhandler.py @@ -17,7 +17,6 @@ import os import logging -import traceback import dbus import gtk @@ -32,39 +31,46 @@ from jarabe.model.shell import ShellModel from jarabe import config from jarabe.journal import journalactivity + _VOLUME_STEP = sound.VOLUME_STEP _VOLUME_MAX = 100 _TABBING_MODIFIER = gtk.gdk.MOD1_MASK + _actions_table = { - 'F1' : 'zoom_mesh', - 'F2' : 'zoom_group', - 'F3' : 'zoom_home', - 'F4' : 'zoom_activity', - 'XF86AudioMute' : 'volume_mute', - 'F11' : 'volume_down', - 'XF86AudioLowerVolume' : 'volume_down', - 'F12' : 'volume_up', - 'XF86AudioRaiseVolume' : 'volume_up', - '<alt>F11' : 'volume_min', - '<alt>F12' : 'volume_max', - '0x93' : 'frame', - '<alt>Tab' : 'next_window', - '<alt><shift>Tab' : 'previous_window', - '<alt>Escape' : 'close_window', - '0xDC' : 'open_search', + 'F1': 'zoom_mesh', + 'F2': 'zoom_group', + 'F3': 'zoom_home', + 'F4': 'zoom_activity', + 'F5': 'open_search', + 'F6': 'frame', + 'XF86AudioMute': 'volume_mute', + 'F11': 'volume_down', + 'XF86AudioLowerVolume': 'volume_down', + 'F12': 'volume_up', + 'XF86AudioRaiseVolume': 'volume_up', + '<alt>F11': 'volume_min', + '<alt>F12': 'volume_max', + '0x93': 'frame', + '<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', - 'XF86Search' : 'open_search', - '<alt><shift>o' : 'open_search', - '<alt><shift>s' : 'say_text', + '<alt><shift>f': 'frame', + '<alt><shift>q': 'quit_emulator', + 'XF86Search': 'open_search', + '<alt><shift>o': 'open_search', + '<alt><shift>s': 'say_text', } SPEECH_DBUS_SERVICE = 'org.laptop.Speech' SPEECH_DBUS_PATH = '/org/laptop/Speech' SPEECH_DBUS_INTERFACE = 'org.laptop.Speech' +_instance = None + + class KeyHandler(object): def __init__(self, frame): self._frame = frame @@ -93,8 +99,7 @@ class KeyHandler(object): raise ValueError('Key %r is already bound' % key) _actions_table[key] = module except Exception: - logging.error('Exception while loading extension:\n' + \ - traceback.format_exc()) + logging.exception('Exception while loading extension:') self._key_grabber.grab_keys(_actions_table.keys()) @@ -124,11 +129,11 @@ class KeyHandler(object): 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, \ + self._get_speech_proxy().SayText(text, reply_handler=lambda: None, error_handler=self._on_speech_err) def handle_say_text(self, event_time): - clipboard = gtk.clipboard_get(selection="PRIMARY") + clipboard = gtk.clipboard_get(selection='PRIMARY') clipboard.request_text(self._primary_selection_cb) def handle_previous_window(self, event_time): @@ -195,7 +200,7 @@ class KeyHandler(object): 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"]: + if not action in ['next_window', 'previous_window']: self._tabbing_handler.stop(event_time) return True @@ -218,7 +223,7 @@ class KeyHandler(object): return False def _key_released_cb(self, grabber, keycode, state, event_time): - logging.debug('_key_released_cb: %i %i' % (keycode, state)) + logging.debug('_key_released_cb: %i %i', 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. @@ -228,7 +233,6 @@ class KeyHandler(object): return True return False -_instance = None def setup(frame): global _instance @@ -237,4 +241,3 @@ def setup(frame): del _instance _instance = KeyHandler(frame) - diff --git a/src/jarabe/view/launcher.py b/src/jarabe/view/launcher.py index 422a49a..89251e5 100644 --- a/src/jarabe/view/launcher.py +++ b/src/jarabe/view/launcher.py @@ -156,9 +156,6 @@ class _Animation(animator.Animation): self._icon.props.size = int(self.start_size + d) -_launchers = {} - - def setup(): model = shell.get_model() model.connect('launch-started', __launch_started_cb) @@ -167,14 +164,15 @@ def setup(): def add_launcher(activity_id, icon_path, icon_color): + model = shell.get_model() - if activity_id in _launchers: + if model.get_launcher(activity_id) is not None: return launch_window = LaunchWindow(activity_id, icon_path, icon_color) launch_window.show() - _launchers[activity_id] = launch_window + model.register_launcher(activity_id, launch_window) def __launch_started_cb(home_model, home_activity): @@ -184,7 +182,7 @@ def __launch_started_cb(home_model, home_activity): def __launch_failed_cb(home_model, home_activity): activity_id = home_activity.get_activity_id() - launcher = _launchers.get(activity_id) + launcher = shell.get_model().get_launcher(activity_id) if launcher is None: logging.error('Launcher for %s is missing', activity_id) @@ -209,8 +207,11 @@ def __launch_completed_cb(home_model, home_activity): def _destroy_launcher(home_activity): activity_id = home_activity.get_activity_id() - if activity_id in _launchers: - _launchers[activity_id].destroy() - del _launchers[activity_id] - else: - logging.error('Launcher for %s is missing', activity_id) + launcher = shell.get_model().get_launcher(activity_id) + if launcher is None: + if not home_activity.is_journal(): + logging.error('Launcher was not registered for %s', activity_id) + return + + shell.get_model().unregister_launcher(activity_id) + launcher.destroy() diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py index 2beceff..d9c1f6b 100644 --- a/src/jarabe/view/palettes.py +++ b/src/jarabe/view/palettes.py @@ -28,31 +28,42 @@ from sugar.graphics.menuitem import MenuItem from sugar.graphics.icon import Icon from sugar.graphics import style from sugar.graphics.xocolor import XoColor -from sugar.activity import activityfactory -from sugar.activity.activityhandle import ActivityHandle from jarabe.model import shell -from jarabe.view import launcher from jarabe.view.viewsource import setup_view_source +from jarabe.journal import misc + 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._notify_launch_hid = None + + if home_activity.props.launch_status == shell.Activity.LAUNCHING: + self._notify_launch_hid = home_activity.connect( \ + 'notify::launch-status', self.__notify_launch_status_cb) self.set_primary_text(_('Starting...')) + elif home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() 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 + def _on_failed_launch(self): + self.set_primary_text(_('Activity failed to start')) + + def __notify_launch_status_cb(self, home_activity, pspec): + home_activity.disconnect(self._notify_launch_hid) + self._notify_launch_hid = None + if home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() + else: + self.setup_palette() + + class CurrentActivityPalette(BasePalette): def __init__(self, home_activity): self._home_activity = home_activity @@ -97,10 +108,6 @@ class CurrentActivityPalette(BasePalette): self._home_activity.get_window().activate( \ gtk.get_current_event_time()) - def __active_window_changed_cb(self, screen, previous_window=None): - setup_view_source() - self._screen.disconnect(self._active_window_changed_sid) - def __stop_activate_cb(self, menu_item): self._home_activity.get_window().close(1) @@ -112,7 +119,7 @@ class ActivityPalette(Palette): self._activity_info = activity_info client = gconf.client_get_default() - color = XoColor(client.get_string("/desktop/sugar/user/color")) + color = XoColor(client.get_string('/desktop/sugar/user/color')) activity_icon = Icon(file=activity_info.get_icon(), xo_color=color, icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) @@ -133,17 +140,8 @@ class ActivityPalette(Palette): def __start_activate_cb(self, menu_item): self.popdown(immediate=True) + misc.launch(self._activity_info) - client = gconf.client_get_default() - xo_color = XoColor(client.get_string('/desktop/sugar/user/color')) - - activity_id = activityfactory.create_activity_id() - launcher.add_launcher(activity_id, - self._activity_info.get_icon(), - xo_color) - - handle = ActivityHandle(activity_id) - activityfactory.create(self._activity_info, handle) class JournalPalette(BasePalette): def __init__(self, home_activity): @@ -196,6 +194,7 @@ class JournalPalette(BasePalette): self._free_space_label.props.label = _('%(free_space)d MB Free') % \ {'free_space': free_space / (1024 * 1024)} + class VolumePalette(Palette): def __init__(self, mount): Palette.__init__(self, label=mount.get_name()) @@ -245,4 +244,3 @@ class VolumePalette(Palette): 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 index 43ec358..392a404 100644 --- a/src/jarabe/view/pulsingicon.py +++ b/src/jarabe/view/pulsingicon.py @@ -21,9 +21,11 @@ import gobject from sugar.graphics.icon import Icon, CanvasIcon + _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 @@ -83,6 +85,7 @@ class Pulser(object): return True + class PulsingIcon(Icon): __gtype_name__ = 'SugarPulsingIcon' @@ -161,6 +164,7 @@ class PulsingIcon(Icon): if self._palette is not None: self._palette.destroy() + class CanvasPulsingIcon(CanvasIcon): __gtype_name__ = 'SugarCanvasPulsingIcon' diff --git a/src/jarabe/view/service.py b/src/jarabe/view/service.py index fbcc961..29e46b2 100644 --- a/src/jarabe/view/service.py +++ b/src/jarabe/view/service.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> # # 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 @@ -20,13 +21,13 @@ import dbus import gtk from jarabe.model import shell -from jarabe.model import owner from jarabe.model import bundleregistry -_DBUS_SERVICE = "org.laptop.Shell" -_DBUS_SHELL_IFACE = "org.laptop.Shell" -_DBUS_OWNER_IFACE = "org.laptop.Shell.Owner" -_DBUS_PATH = "/org/laptop/Shell" + +_DBUS_SERVICE = 'org.laptop.Shell' +_DBUS_SHELL_IFACE = 'org.laptop.Shell' +_DBUS_PATH = '/org/laptop/Shell' + class UIService(dbus.service.Object): """Provides d-bus service to script the shell's operations @@ -54,17 +55,8 @@ class UIService(dbus.service.Object): self._shell_model = shell.get_model() - def start(self): - owner_model = owner.get_model() - owner_model.connect('nick-changed', self._owner_nick_changed_cb) - owner_model.connect('icon-changed', self._owner_icon_changed_cb) - owner_model.connect('color-changed', self._owner_color_changed_cb) - - self._shell_model.connect('active-activity-changed', - self._cur_activity_changed_cb) - @dbus.service.method(_DBUS_SHELL_IFACE, - in_signature="s", out_signature="s") + in_signature='s', out_signature='s') def GetBundlePath(self, bundle_id): bundle = bundleregistry.get_registry().get_bundle(bundle_id) if bundle: @@ -73,59 +65,26 @@ class UIService(dbus.service.Object): return '' @dbus.service.method(_DBUS_SHELL_IFACE, - in_signature="s", out_signature="b") + in_signature='s', out_signature='b') def ActivateActivity(self, activity_id): - """Switch to the window related to this activity_id and return a boolean - indicating if there is a real (ie. not a launcher window) activity - already open. + """Switch to the window related to this activity_id and return a + boolean indicating if there is a real (ie. not a launcher window) + activity already open. """ activity = self._shell_model.get_activity_by_id(activity_id) if activity is not None and activity.get_window() is not None: activity.get_window().activate(gtk.get_current_event_time()) - return not activity.props.launching + return self._shell_model.get_launcher(activity_id) is None return False @dbus.service.method(_DBUS_SHELL_IFACE, - in_signature="ss", out_signature="") + in_signature='ss', out_signature='') def NotifyLaunch(self, bundle_id, activity_id): shell.get_model().notify_launch(activity_id, bundle_id) @dbus.service.method(_DBUS_SHELL_IFACE, - in_signature="s", out_signature="") + in_signature='s', out_signature='') def NotifyLaunchFailure(self, activity_id): shell.get_model().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)) - - @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s") - def CurrentActivityChanged(self, activity_id): - pass - - def _cur_activity_changed_cb(self, shell_model, 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/view/tabbinghandler.py b/src/jarabe/view/tabbinghandler.py index f52bda3..0889792 100644 --- a/src/jarabe/view/tabbinghandler.py +++ b/src/jarabe/view/tabbinghandler.py @@ -21,8 +21,10 @@ import gtk from jarabe.model import shell + _RAISE_DELAY = 250 + class TabbingHandler(object): def __init__(self, frame, modifier): self._frame = frame @@ -145,4 +147,3 @@ class TabbingHandler(object): def is_tabbing(self): return self._tabbing - diff --git a/src/jarabe/view/viewsource.py b/src/jarabe/view/viewsource.py index 9905713..a1c0be3 100644 --- a/src/jarabe/view/viewsource.py +++ b/src/jarabe/view/viewsource.py @@ -17,7 +17,6 @@ import os import logging -import traceback from gettext import gettext as _ import gobject @@ -37,11 +36,13 @@ from sugar.bundle.activitybundle import ActivityBundle from sugar.datastore import datastore from sugar import mime + _SOURCE_FONT = pango.FontDescription('Monospace %d' % style.FONT_SIZE) _logger = logging.getLogger('ViewSource') map_activity_to_window = {} + def setup_view_source(activity): service = activity.get_service() if service is not None: @@ -52,9 +53,9 @@ def setup_view_source(activity): expected_exceptions = ['org.freedesktop.DBus.Error.UnknownMethod', 'org.freedesktop.DBus.Python.NotImplementedError'] if e.get_dbus_name() not in expected_exceptions: - logging.error(traceback.format_exc()) + logging.exception('Exception occured in HandleViewSource():') except Exception: - logging.error(traceback.format_exc()) + logging.exception('Exception occured in HandleViewSource():') window_xid = activity.get_xid() if window_xid is None: @@ -76,9 +77,9 @@ def setup_view_source(activity): expected_exceptions = ['org.freedesktop.DBus.Error.UnknownMethod', 'org.freedesktop.DBus.Python.NotImplementedError'] if e.get_dbus_name() not in expected_exceptions: - logging.error(traceback.format_exc()) + logging.exception('Exception occured in GetDocumentPath():') except Exception: - logging.error(traceback.format_exc()) + logging.exception('Exception occured in GetDocumentPath():') if bundle_path is None and document_path is None: _logger.debug('Activity without bundle_path nor document_path') @@ -89,6 +90,7 @@ def setup_view_source(activity): map_activity_to_window[window_xid] = view_source view_source.show() + class ViewSource(gtk.Window): __gtype_name__ = 'SugarViewSource' @@ -129,10 +131,10 @@ class ViewSource(gtk.Window): file_name = '' activity_bundle = ActivityBundle(bundle_path) - command = activity_bundle.get_command() + command = activity_bundle.get_command() if len(command.split(' ')) > 1: name = command.split(' ')[1].split('.')[0] - file_name = name + '.py' + file_name = name + '.py' path = os.path.join(activity_bundle.get_path(), file_name) self._selected_file = path @@ -195,6 +197,7 @@ class ViewSource(gtk.Window): else: self._source_display.file_path = None + class DocumentButton(RadioToolButton): __gtype_name__ = 'SugarDocumentButton' @@ -244,22 +247,20 @@ class DocumentButton(RadioToolButton): error_handler=self.__internal_save_error_cb) def __internal_save_cb(self): - logging.debug("Saved Source object to datastore.") + logging.debug('Saved Source object to datastore.') self._jobject.destroy() def __internal_save_error_cb(self, err): logging.debug('Error saving Source object to datastore: %s', err) self._jobject.destroy() + class Toolbar(gtk.Toolbar): __gtype_name__ = 'SugarViewSourceToolbar' __gsignals__ = { - 'stop-clicked': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, - ([])), - 'source-selected': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, + 'stop-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'source-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), } @@ -309,7 +310,6 @@ class Toolbar(gtk.Toolbar): stop = ToolButton(icon_name='dialog-cancel') stop.set_tooltip(_('Close')) stop.connect('clicked', self.__stop_clicked_cb) - stop.show() self.insert(stop, -1) stop.show() @@ -340,6 +340,7 @@ class Toolbar(gtk.Toolbar): if button.props.active: self.emit('source-selected', path) + class FileViewer(gtk.ScrolledWindow): __gtype_name__ = 'SugarFileViewer' @@ -406,6 +407,7 @@ class FileViewer(gtk.ScrolledWindow): file_path = model.get_value(tree_iter, 1) self.emit('file-selected', file_path) + class SourceDisplay(gtk.ScrolledWindow): __gtype_name__ = 'SugarSourceDisplay' @@ -462,4 +464,3 @@ class SourceDisplay(gtk.ScrolledWindow): return self._file_path file_path = property(_get_file_path, _set_file_path) - |