Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/shell/src
diff options
context:
space:
mode:
Diffstat (limited to 'shell/src')
-rw-r--r--shell/src/Makefile.am1
-rw-r--r--shell/src/jarabe/.gitignore1
-rw-r--r--shell/src/jarabe/Makefile.am16
-rw-r--r--shell/src/jarabe/__init__.py26
-rw-r--r--shell/src/jarabe/config.py.in26
-rw-r--r--shell/src/jarabe/controlpanel/Makefile.am10
-rw-r--r--shell/src/jarabe/controlpanel/__init__.py16
-rw-r--r--shell/src/jarabe/controlpanel/cmd.py158
-rw-r--r--shell/src/jarabe/controlpanel/gui.py431
-rw-r--r--shell/src/jarabe/controlpanel/inlinealert.py83
-rw-r--r--shell/src/jarabe/controlpanel/sectionview.py55
-rw-r--r--shell/src/jarabe/controlpanel/toolbar.py157
-rw-r--r--shell/src/jarabe/desktop/Makefile.am18
-rw-r--r--shell/src/jarabe/desktop/__init__.py16
-rw-r--r--shell/src/jarabe/desktop/activitieslist.py451
-rw-r--r--shell/src/jarabe/desktop/favoriteslayout.py488
-rw-r--r--shell/src/jarabe/desktop/favoritesview.py670
-rw-r--r--shell/src/jarabe/desktop/friendview.py87
-rw-r--r--shell/src/jarabe/desktop/grid.py201
-rw-r--r--shell/src/jarabe/desktop/groupbox.py92
-rw-r--r--shell/src/jarabe/desktop/homebox.py298
-rw-r--r--shell/src/jarabe/desktop/homewindow.py193
-rw-r--r--shell/src/jarabe/desktop/keydialog.py317
-rw-r--r--shell/src/jarabe/desktop/meshbox.py670
-rw-r--r--shell/src/jarabe/desktop/networkviews.py716
-rw-r--r--shell/src/jarabe/desktop/schoolserver.py127
-rw-r--r--shell/src/jarabe/desktop/snowflakelayout.py108
-rw-r--r--shell/src/jarabe/desktop/spreadlayout.py83
-rw-r--r--shell/src/jarabe/desktop/transitionbox.py96
-rw-r--r--shell/src/jarabe/frame/Makefile.am18
-rw-r--r--shell/src/jarabe/frame/__init__.py25
-rw-r--r--shell/src/jarabe/frame/activitiestray.py745
-rw-r--r--shell/src/jarabe/frame/clipboard.py149
-rw-r--r--shell/src/jarabe/frame/clipboardicon.py158
-rw-r--r--shell/src/jarabe/frame/clipboardmenu.py249
-rw-r--r--shell/src/jarabe/frame/clipboardobject.py142
-rw-r--r--shell/src/jarabe/frame/clipboardpanelwindow.py103
-rw-r--r--shell/src/jarabe/frame/clipboardtray.py216
-rw-r--r--shell/src/jarabe/frame/devicestray.py54
-rw-r--r--shell/src/jarabe/frame/eventarea.py151
-rw-r--r--shell/src/jarabe/frame/frame.py351
-rw-r--r--shell/src/jarabe/frame/frameinvoker.py36
-rw-r--r--shell/src/jarabe/frame/framewindow.py117
-rw-r--r--shell/src/jarabe/frame/friendstray.py118
-rw-r--r--shell/src/jarabe/frame/notification.py100
-rw-r--r--shell/src/jarabe/frame/zoomtoolbar.py89
-rw-r--r--shell/src/jarabe/intro/Makefile.am9
-rw-r--r--shell/src/jarabe/intro/__init__.py25
-rw-r--r--shell/src/jarabe/intro/colorpicker.py43
-rw-r--r--shell/src/jarabe/intro/default-picture.pngbin0 -> 10442 bytes
-rw-r--r--shell/src/jarabe/intro/window.py298
-rw-r--r--shell/src/jarabe/journal/Makefile.am17
-rw-r--r--shell/src/jarabe/journal/__init__.py15
-rw-r--r--shell/src/jarabe/journal/detailview.py117
-rw-r--r--shell/src/jarabe/journal/expandedentry.py429
-rw-r--r--shell/src/jarabe/journal/journalactivity.py371
-rw-r--r--shell/src/jarabe/journal/journalentrybundle.py94
-rw-r--r--shell/src/jarabe/journal/journaltoolbox.py458
-rw-r--r--shell/src/jarabe/journal/keepicon.py59
-rw-r--r--shell/src/jarabe/journal/listmodel.py201
-rw-r--r--shell/src/jarabe/journal/listview.py641
-rw-r--r--shell/src/jarabe/journal/misc.py262
-rw-r--r--shell/src/jarabe/journal/modalalert.py97
-rw-r--r--shell/src/jarabe/journal/model.py541
-rw-r--r--shell/src/jarabe/journal/objectchooser.py199
-rw-r--r--shell/src/jarabe/journal/palettes.py235
-rw-r--r--shell/src/jarabe/journal/volumestoolbar.py207
-rw-r--r--shell/src/jarabe/model/Makefile.am19
-rw-r--r--shell/src/jarabe/model/__init__.py16
-rw-r--r--shell/src/jarabe/model/adhoc.py292
-rw-r--r--shell/src/jarabe/model/buddy.py250
-rw-r--r--shell/src/jarabe/model/bundleregistry.py444
-rw-r--r--shell/src/jarabe/model/filetransfer.py374
-rw-r--r--shell/src/jarabe/model/friends.py122
-rw-r--r--shell/src/jarabe/model/invites.py239
-rw-r--r--shell/src/jarabe/model/mimeregistry.py49
-rw-r--r--shell/src/jarabe/model/neighborhood.py863
-rw-r--r--shell/src/jarabe/model/network.py751
-rw-r--r--shell/src/jarabe/model/notifications.py95
-rw-r--r--shell/src/jarabe/model/olpcmesh.py214
-rw-r--r--shell/src/jarabe/model/screen.py43
-rw-r--r--shell/src/jarabe/model/session.py89
-rw-r--r--shell/src/jarabe/model/shell.py641
-rw-r--r--shell/src/jarabe/model/sound.py58
-rw-r--r--shell/src/jarabe/model/telepathyclient.py100
-rw-r--r--shell/src/jarabe/util/Makefile.am7
-rw-r--r--shell/src/jarabe/util/__init__.py19
-rw-r--r--shell/src/jarabe/util/emulator.py177
-rw-r--r--shell/src/jarabe/util/telepathy/Makefile.am4
-rw-r--r--shell/src/jarabe/util/telepathy/__init__.py19
-rw-r--r--shell/src/jarabe/util/telepathy/connection_watcher.py118
-rw-r--r--shell/src/jarabe/view/Makefile.am12
-rw-r--r--shell/src/jarabe/view/__init__.py16
-rw-r--r--shell/src/jarabe/view/buddyicon.py61
-rw-r--r--shell/src/jarabe/view/buddymenu.py168
-rw-r--r--shell/src/jarabe/view/keyhandler.py242
-rw-r--r--shell/src/jarabe/view/launcher.py217
-rw-r--r--shell/src/jarabe/view/palettes.py250
-rw-r--r--shell/src/jarabe/view/pulsingicon.py229
-rw-r--r--shell/src/jarabe/view/service.py89
-rw-r--r--shell/src/jarabe/view/tabbinghandler.py148
-rw-r--r--shell/src/jarabe/view/viewsource.py464
102 files changed, 19649 insertions, 0 deletions
diff --git a/shell/src/Makefile.am b/shell/src/Makefile.am
new file mode 100644
index 0000000..83571a4
--- /dev/null
+++ b/shell/src/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = jarabe
diff --git a/shell/src/jarabe/.gitignore b/shell/src/jarabe/.gitignore
new file mode 100644
index 0000000..4acd06b
--- /dev/null
+++ b/shell/src/jarabe/.gitignore
@@ -0,0 +1 @@
+config.py
diff --git a/shell/src/jarabe/Makefile.am b/shell/src/jarabe/Makefile.am
new file mode 100644
index 0000000..84bb213
--- /dev/null
+++ b/shell/src/jarabe/Makefile.am
@@ -0,0 +1,16 @@
+SUBDIRS = \
+ controlpanel \
+ desktop \
+ frame \
+ journal \
+ model \
+ view \
+ intro \
+ util
+
+sugardir = $(pythondir)/jarabe
+sugar_PYTHON = \
+ __init__.py
+
+nodist_sugar_PYTHON = config.py
+
diff --git a/shell/src/jarabe/__init__.py b/shell/src/jarabe/__init__.py
new file mode 100644
index 0000000..41b4b1c
--- /dev/null
+++ b/shell/src/jarabe/__init__.py
@@ -0,0 +1,26 @@
+"""OLPC Sugar Graphical "Shell" Interface
+
+Provides the shell-level operations for managing
+the OLPC laptop computers. It interacts heavily
+with (and depends upon) the Sugar UI libraries.
+
+This is a "graphical" shell, the name does not
+refer to a command-line "shell" interface.
+"""
+
+# Copyright (C) 2006-2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
diff --git a/shell/src/jarabe/config.py.in b/shell/src/jarabe/config.py.in
new file mode 100644
index 0000000..6c418e9
--- /dev/null
+++ b/shell/src/jarabe/config.py.in
@@ -0,0 +1,26 @@
+# Copyright (C) 2008 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+# pylint: disable-msg=C0301
+
+prefix = '@prefix@'
+data_path = '@prefix@/share/sugar/data'
+shell_path = '@prefix@/share/sugar/shell'
+locale_path = '@prefix@/share/locale'
+ext_path = '@prefix@/share/sugar/extensions'
+activities_path = "@prefix@/share/sugar/activities"
+version = '@SUCROSE_VERSION@'
+
diff --git a/shell/src/jarabe/controlpanel/Makefile.am b/shell/src/jarabe/controlpanel/Makefile.am
new file mode 100644
index 0000000..1de2961
--- /dev/null
+++ b/shell/src/jarabe/controlpanel/Makefile.am
@@ -0,0 +1,10 @@
+sugardir = $(pythondir)/jarabe/controlpanel
+sugar_PYTHON = \
+ __init__.py \
+ cmd.py \
+ gui.py \
+ inlinealert.py \
+ sectionview.py \
+ toolbar.py
+
+
diff --git a/shell/src/jarabe/controlpanel/__init__.py b/shell/src/jarabe/controlpanel/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/shell/src/jarabe/controlpanel/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2006-2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
diff --git a/shell/src/jarabe/controlpanel/cmd.py b/shell/src/jarabe/controlpanel/cmd.py
new file mode 100644
index 0000000..7144b33
--- /dev/null
+++ b/shell/src/jarabe/controlpanel/cmd.py
@@ -0,0 +1,158 @@
+# Copyright (C) 2007, 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import sys
+import getopt
+import os
+from gettext import gettext as _
+import 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")
+
+def cmd_help():
+ '''Print the help to the screen'''
+ # TRANS: Translators, there's a empty line at the end of this string,
+ # which must appear in the translated string (msgstr) as well.
+ print _('Usage: sugar-control-panel [ option ] key [ args ... ] \n\
+ Control for the sugar environment. \n\
+ Options: \n\
+ -h show this help message and exit \n\
+ -l list all the available options \n\
+ -h key show information about this key \n\
+ -g key get the current value of the key \n\
+ -s key set the current value for the key \n\
+ -c key clear the current value for the key \n\
+ ')
+
+def note_restart():
+ '''Instructions how to restart sugar'''
+ print _('To apply your changes you have to restart sugar.\n' +
+ 'Hit ctrl+alt+erase on the keyboard to trigger a restart.')
+
+def load_modules():
+ '''Build a list of pointers to available modules and import them.
+ '''
+ modules = []
+
+ path = os.path.join(config.ext_path, 'cpsection')
+ folder = os.listdir(path)
+
+ for item in folder:
+ if os.path.isdir(os.path.join(path, item)) and \
+ os.path.exists(os.path.join(path, item, 'model.py')):
+ try:
+ 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())))
+ else:
+ modules.append(module)
+
+ return modules
+
+def main():
+ try:
+ options, args = getopt.getopt(sys.argv[1:], "h:s:g:c:l", [])
+ except getopt.GetoptError:
+ cmd_help()
+ sys.exit(2)
+
+ if not options:
+ cmd_help()
+ sys.exit(2)
+
+ modules = load_modules()
+
+ for option, key in options:
+ found = 0
+ if option in ("-h"):
+ for module in modules:
+ method = getattr(module, 'set_' + key, None)
+ if method:
+ found += 1
+ if found == 1:
+ print method.__doc__
+ else:
+ print _(_same_option_warning % (key, module))
+ if found == 0:
+ print _(_no_option_error % key)
+ if option in ("-l"):
+ for module in modules:
+ methods = dir(module)
+ print '%s:' % module.__name__.split('.')[1]
+ for method in methods:
+ if method.startswith('get_'):
+ print ' %s' % method[4:]
+ elif method.startswith('clear_'):
+ print " %s (use the -c argument with this option)" \
+ % method[6:]
+ if option in ("-g"):
+ for module in modules:
+ method = getattr(module, 'print_' + key, None)
+ if method:
+ found += 1
+ if found == 1:
+ try:
+ method()
+ except Exception, detail:
+ print _(_general_error % detail)
+ else:
+ print _(_same_option_warning % (key, module))
+ if found == 0:
+ print _(_no_option_error % key)
+ if option in ("-s"):
+ for module in modules:
+ method = getattr(module, 'set_' + key, None)
+ if method:
+ note = 0
+ found += 1
+ if found == 1:
+ try:
+ note = method(*args)
+ except Exception, detail:
+ print _(_general_error % detail)
+ if note == _RESTART:
+ note_restart()
+ else:
+ print _(_same_option_warning % (key, module))
+ if found == 0:
+ print _(_no_option_error % key)
+ if option in ("-c"):
+ for module in modules:
+ method = getattr(module, 'clear_' + key, None)
+ if method:
+ note = 0
+ found += 1
+ if found == 1:
+ try:
+ note = method(*args)
+ except Exception, detail:
+ print _(_general_error % detail)
+ if note == _RESTART:
+ note_restart()
+ else:
+ print _(_same_option_warning % (key, module))
+ if found == 0:
+ print _(_no_option_error % key)
diff --git a/shell/src/jarabe/controlpanel/gui.py b/shell/src/jarabe/controlpanel/gui.py
new file mode 100644
index 0000000..51d9820
--- /dev/null
+++ b/shell/src/jarabe/controlpanel/gui.py
@@ -0,0 +1,431 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+from gettext import gettext as _
+import sys
+import traceback
+
+import gobject
+import gtk
+
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+from sugar.graphics.alert import Alert
+
+from jarabe.model.session import get_session_manager
+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'
+
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ self.set_border_width(style.LINE_WIDTH)
+ offset = style.GRID_CELL_SIZE
+ width = gtk.gdk.screen_width() - offset * 2
+ height = gtk.gdk.screen_height() - offset * 2
+ self.set_size_request(width, height)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(True)
+
+ self._toolbar = None
+ self._canvas = None
+ self._table = None
+ self._scrolledwindow = None
+ self._separator = None
+ self._section_view = None
+ self._section_toolbar = None
+ self._main_toolbar = None
+
+ self._vbox = gtk.VBox()
+ self._hbox = gtk.HBox()
+ self._vbox.pack_start(self._hbox)
+ self._hbox.show()
+
+ self._main_view = gtk.EventBox()
+ self._hbox.pack_start(self._main_view)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_BLACK.get_gdk_color())
+ self._main_view.show()
+
+ self.add(self._vbox)
+ self._vbox.show()
+
+ self.connect("realize", self.__realize_cb)
+
+ self._options = self._get_options()
+ self._current_option = None
+ self._setup_main()
+ self._setup_section()
+ self._show_main_view()
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+
+ def _set_canvas(self, canvas):
+ if self._canvas:
+ self._main_view.remove(self._canvas)
+ if canvas:
+ self._main_view.add(canvas)
+ self._canvas = canvas
+
+ def _set_toolbar(self, toolbar):
+ if self._toolbar:
+ self._vbox.remove(self._toolbar)
+ self._vbox.pack_start(toolbar, False)
+ self._vbox.reorder_child(toolbar, 0)
+ self._toolbar = toolbar
+ if not self._separator:
+ self._separator = gtk.HSeparator()
+ self._vbox.pack_start(self._separator, False)
+ self._vbox.reorder_child(self._separator, 1)
+ self._separator.show()
+
+ def _setup_main(self):
+ self._main_toolbar = MainToolbar()
+
+ self._table = gtk.Table()
+ self._table.set_col_spacings(style.GRID_CELL_SIZE)
+ self._table.set_border_width(style.GRID_CELL_SIZE)
+
+ self._scrolledwindow = gtk.ScrolledWindow()
+ self._scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC,
+ gtk.POLICY_AUTOMATIC)
+ self._scrolledwindow.add_with_viewport(self._table)
+ child = self._scrolledwindow.get_child()
+ child.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color())
+
+ self._setup_options()
+ self._main_toolbar.connect('stop-clicked',
+ self.__stop_clicked_cb)
+ self._main_toolbar.connect('search-changed',
+ self.__search_changed_cb)
+
+ def _setup_options(self):
+ if not os.path.exists('/ofw'):
+ del self._options['power']
+
+ try:
+ import xklavier
+ except ImportError:
+ del self._options['keyboard']
+
+ row = 0
+ column = 2
+ options = self._options.keys()
+ options.sort()
+
+ for option in options:
+ sectionicon = _SectionIcon(icon_name=self._options[option]['icon'],
+ title=self._options[option]['title'],
+ xo_color=self._options[option]['color'],
+ pixel_size=style.GRID_CELL_SIZE)
+ sectionicon.connect('button_press_event',
+ self.__select_option_cb, option)
+ sectionicon.show()
+
+ if option == 'aboutme':
+ self._table.attach(sectionicon, 0, 1, 0, 1)
+ elif option == 'aboutcomputer':
+ self._table.attach(sectionicon, 1, 2, 0, 1)
+ else:
+ self._table.attach(sectionicon,
+ column, column + 1,
+ row, row + 1)
+ column += 1
+ if column == _MAX_COLUMNS:
+ column = 0
+ row += 1
+
+ self._options[option]['button'] = sectionicon
+
+ def _show_main_view(self):
+ self._set_toolbar(self._main_toolbar)
+ self._main_toolbar.show()
+ self._set_canvas(self._scrolledwindow)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_BLACK.get_gdk_color())
+ self._table.show()
+ self._scrolledwindow.show()
+ entry = self._main_toolbar.get_entry()
+ entry.grab_focus()
+ entry.set_text('')
+
+ def _update(self, query):
+ for option in self._options:
+ found = False
+ for key in self._options[option]['keywords']:
+ if query.lower() in key.lower():
+ self._options[option]['button'].set_sensitive(True)
+ found = True
+ break
+ if not found:
+ self._options[option]['button'].set_sensitive(False)
+
+ def _setup_section(self):
+ self._section_toolbar = SectionToolbar()
+ self._section_toolbar.connect('cancel-clicked',
+ self.__cancel_clicked_cb)
+ self._section_toolbar.connect('accept-clicked',
+ self.__accept_clicked_cb)
+
+ def show_section_view(self, option):
+ self._set_toolbar(self._section_toolbar)
+
+ icon = self._section_toolbar.get_icon()
+ icon.set_from_icon_name(self._options[option]['icon'],
+ gtk.ICON_SIZE_LARGE_TOOLBAR)
+ icon.props.xo_color = self._options[option]['color']
+ title = self._section_toolbar.get_title()
+ title.set_text(self._options[option]['title'])
+ self._section_toolbar.show()
+
+ self._current_option = option
+
+ mod = __import__('.'.join(('cpsection', option, 'view')),
+ globals(), locals(), ['view'])
+ view_class = getattr(mod, self._options[option]['view'], None)
+
+ mod = __import__('.'.join(('cpsection', option, 'model')),
+ globals(), locals(), ['model'])
+ model = ModelWrapper(mod)
+
+ self._section_view = view_class(model,
+ self._options[option]['alerts'])
+
+ self._set_canvas(self._section_view)
+ self._section_view.show()
+ self._section_view.connect('notify::is-valid',
+ self.__valid_section_cb)
+ self._section_view.connect('request-close',
+ self.__close_request_cb)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+
+ def set_section_view_auto_close(self):
+ '''Automatically close the control panel if there is "nothing to do"
+ '''
+ self._section_view.auto_close = True
+
+ def _get_options(self):
+ '''Get the available option information from the extensions
+ '''
+ options = {}
+
+ path = os.path.join(config.ext_path, 'cpsection')
+ folder = os.listdir(path)
+
+ for item in folder:
+ if os.path.isdir(os.path.join(path, item)) and \
+ os.path.exists(os.path.join(path, item, '__init__.py')):
+ try:
+ mod = __import__('.'.join(('cpsection', item)),
+ globals(), locals(), [item])
+ view_class = getattr(mod, 'CLASS', None)
+ if view_class is not None:
+ options[item] = {}
+ options[item]['alerts'] = []
+ options[item]['view'] = view_class
+ options[item]['icon'] = getattr(mod, 'ICON', item)
+ options[item]['title'] = getattr(mod, 'TITLE', item)
+ options[item]['color'] = getattr(mod, 'COLOR', None)
+ keywords = getattr(mod, 'KEYWORDS', [])
+ keywords.append(options[item]['title'].lower())
+ if item not in keywords:
+ keywords.append(item)
+ options[item]['keywords'] = keywords
+ else:
+ _logger.error('There is no CLASS constant specifieds ' \
+ 'in the view file \'%s\'.' % item)
+ except Exception:
+ logging.error('Exception while loading extension:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+
+ return options
+
+ def __cancel_clicked_cb(self, widget):
+ self._section_view.undo()
+ self._options[self._current_option]['alerts'] = []
+ self._section_toolbar.accept_button.set_sensitive(True)
+ self._show_main_view()
+
+ def __accept_clicked_cb(self, widget):
+ if self._section_view.needs_restart:
+ self._section_toolbar.accept_button.set_sensitive(False)
+ self._section_toolbar.cancel_button.set_sensitive(False)
+ alert = Alert()
+ alert.props.title = _('Warning')
+ alert.props.msg = _('Changes require restart')
+
+ icon = Icon(icon_name='dialog-cancel')
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel changes'), icon)
+ icon.show()
+
+ if self._current_option != 'aboutme':
+ icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_ACCEPT, _('Later'), icon)
+ icon.show()
+
+ icon = Icon(icon_name='system-restart')
+ alert.add_button(gtk.RESPONSE_APPLY, _('Restart now'), icon)
+ icon.show()
+
+ self._vbox.pack_start(alert, False)
+ self._vbox.reorder_child(alert, 2)
+ alert.connect('response', self.__response_cb)
+ alert.show()
+ else:
+ self._show_main_view()
+
+ def __response_cb(self, alert, response_id):
+ self._vbox.remove(alert)
+ self._section_toolbar.accept_button.set_sensitive(True)
+ self._section_toolbar.cancel_button.set_sensitive(True)
+ if response_id is gtk.RESPONSE_CANCEL:
+ self._section_view.undo()
+ self._section_view.setup()
+ self._options[self._current_option]['alerts'] = []
+ elif response_id is gtk.RESPONSE_ACCEPT:
+ self._options[self._current_option]['alerts'] = \
+ self._section_view.restart_alerts
+ self._show_main_view()
+ elif response_id is gtk.RESPONSE_APPLY:
+ session_manager = get_session_manager()
+ session_manager.logout()
+
+ def __select_option_cb(self, button, event, option):
+ self.show_section_view(option)
+
+ def __search_changed_cb(self, maintoolbar, query):
+ self._update(query)
+
+ def __stop_clicked_cb(self, widget):
+ self.destroy()
+
+ def __close_request_cb(self, widget, event=None):
+ self.destroy()
+
+ def __valid_section_cb(self, section_view, pspec):
+ section_is_valid = section_view.props.is_valid
+ self._section_toolbar.accept_button.set_sensitive(section_is_valid)
+
+class ModelWrapper(object):
+ def __init__(self, module):
+ self._module = module
+ self._options = {}
+ self._setup()
+
+ def _setup(self):
+ methods = dir(self._module)
+ for method in methods:
+ if method.startswith('get_') and method[4:] != 'color':
+ try:
+ self._options[method[4:]] = getattr(self._module, method)()
+ except Exception:
+ self._options[method[4:]] = None
+
+ def __getattr__(self, name):
+ return getattr(self._module, name)
+
+ def undo(self):
+ for key in self._options.keys():
+ method = getattr(self._module, 'set_' + key, None)
+ if method and self._options[key] is not None:
+ try:
+ method(self._options[key])
+ except Exception, detail:
+ _logger.debug('Error undo option: %s', detail)
+
+class _SectionIcon(gtk.EventBox):
+ __gtype_name__ = "SugarSectionIcon"
+
+ __gproperties__ = {
+ 'icon-name' : (str, None, None, None,
+ gobject.PARAM_READWRITE),
+ 'pixel-size' : (object, None, None,
+ gobject.PARAM_READWRITE),
+ 'xo-color' : (object, None, None,
+ gobject.PARAM_READWRITE),
+ 'title' : (str, None, None, None,
+ gobject.PARAM_READWRITE)
+ }
+
+ def __init__(self, **kwargs):
+ self._icon_name = None
+ self._pixel_size = style.GRID_CELL_SIZE
+ self._xo_color = None
+ self._title = 'No Title'
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ self._vbox = gtk.VBox()
+ self._icon = Icon(icon_name=self._icon_name,
+ pixel_size=self._pixel_size,
+ xo_color=self._xo_color)
+ self._vbox.pack_start(self._icon, expand=False, fill=False)
+
+ self._label = gtk.Label(self._title)
+ self._label.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._vbox.pack_start(self._label, expand=False, fill=False)
+
+ self._vbox.set_spacing(style.DEFAULT_SPACING)
+ self.set_visible_window(False)
+ self.set_app_paintable(True)
+ self.set_events(gtk.gdk.BUTTON_PRESS_MASK)
+
+ self.add(self._vbox)
+ self._vbox.show()
+ self._label.show()
+ self._icon.show()
+
+ def get_icon(self):
+ return self._icon
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'icon-name':
+ if self._icon_name != value:
+ self._icon_name = value
+ elif pspec.name == 'pixel-size':
+ if self._pixel_size != value:
+ self._pixel_size = value
+ elif pspec.name == 'xo-color':
+ if self._xo_color != value:
+ self._xo_color = value
+ elif pspec.name == 'title':
+ if self._title != value:
+ self._title = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'icon-name':
+ return self._icon_name
+ elif pspec.name == 'pixel-size':
+ return self._pixel_size
+ elif pspec.name == 'xo-color':
+ return self._xo_color
+ elif pspec.name == 'title':
+ return self._title
diff --git a/shell/src/jarabe/controlpanel/inlinealert.py b/shell/src/jarabe/controlpanel/inlinealert.py
new file mode 100644
index 0000000..b1880da
--- /dev/null
+++ b/shell/src/jarabe/controlpanel/inlinealert.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2008, OLPC
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import gobject
+import pango
+
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
+
+class InlineAlert(gtk.HBox):
+ """UI interface for Inline alerts
+
+ Inline alerts are different from the other alerts beause they are
+ no dialogs, they only inform about a current event.
+
+ Properties:
+ 'msg': the message of the alert,
+ 'icon': the icon that appears at the far left
+ See __gproperties__
+ """
+
+ __gtype_name__ = 'SugarInlineAlert'
+
+ __gproperties__ = {
+ 'msg' : (str, None, None, None,
+ gobject.PARAM_READWRITE),
+ 'icon' : (object, None, None,
+ gobject.PARAM_WRITABLE)
+ }
+
+ def __init__(self, **kwargs):
+
+ self._msg = None
+ self._msg_color = None
+ self._icon = Icon(icon_name='emblem-warning',
+ fill_color=style.COLOR_SELECTION_GREY.get_svg(),
+ stroke_color=style.COLOR_WHITE.get_svg())
+
+ self._msg_label = gtk.Label()
+ self._msg_label.set_max_width_chars(50)
+ self._msg_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
+ self._msg_label.set_alignment(0, 0.5)
+ self._msg_label.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_SELECTION_GREY.get_gdk_color())
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ self.set_spacing(style.DEFAULT_SPACING)
+ self.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+
+ self.pack_start(self._icon, False)
+ self.pack_start(self._msg_label, False)
+ self._msg_label.show()
+ self._icon.show()
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'msg':
+ if self._msg != value:
+ self._msg = value
+ self._msg_label.set_markup(self._msg)
+ elif pspec.name == 'icon':
+ if self._icon != value:
+ self._icon = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'msg':
+ return self._msg
+
diff --git a/shell/src/jarabe/controlpanel/sectionview.py b/shell/src/jarabe/controlpanel/sectionview.py
new file mode 100644
index 0000000..4de27a2
--- /dev/null
+++ b/shell/src/jarabe/controlpanel/sectionview.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2008, OLPC
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gobject
+import gtk
+from gettext import gettext as _
+
+class SectionView(gtk.VBox):
+ __gtype_name__ = 'SugarSectionView'
+
+ __gsignals__ = {
+ 'request-close': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([]))
+ }
+
+ __gproperties__ = {
+ 'is_valid' : (bool, None, None, True,
+ gobject.PARAM_READWRITE)
+ }
+
+ _APPLY_TIMEOUT = 1000
+
+ def __init__(self):
+ gtk.VBox.__init__(self)
+ self._is_valid = True
+ self.auto_close = False
+ self.needs_restart = False
+ self.restart_alerts = []
+ self.restart_msg = _('Changes require restart')
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'is-valid':
+ if self._is_valid != value:
+ self._is_valid = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'is-valid':
+ return self._is_valid
+
+ def undo(self):
+ '''Undo here the changes that have been made in this section.'''
+ pass
diff --git a/shell/src/jarabe/controlpanel/toolbar.py b/shell/src/jarabe/controlpanel/toolbar.py
new file mode 100644
index 0000000..320a8eb
--- /dev/null
+++ b/shell/src/jarabe/controlpanel/toolbar.py
@@ -0,0 +1,157 @@
+# Copyright (C) 2007, 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import gettext
+import gobject
+
+_ = lambda msg: gettext.dgettext('sugar', msg)
+
+from sugar.graphics.icon import Icon
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics import iconentry
+from sugar.graphics import style
+
+class MainToolbar(gtk.Toolbar):
+ """ Main toolbar of the control panel
+ """
+ __gtype_name__ = 'MainToolbar'
+
+ __gsignals__ = {
+ 'stop-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([])),
+ 'search-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str]))
+ }
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._add_separator()
+
+ tool_item = gtk.ToolItem()
+ self.insert(tool_item, -1)
+ tool_item.show()
+ self._search_entry = iconentry.IconEntry()
+ self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self._search_entry.add_clear_button()
+ self._search_entry.set_width_chars(25)
+ self._search_entry.connect('changed', self.__search_entry_changed_cb)
+ tool_item.add(self._search_entry)
+ self._search_entry.show()
+
+ self._add_separator(True)
+
+ self.stop = ToolButton(icon_name='dialog-cancel')
+ self.stop.set_tooltip(_('Done'))
+ self.stop.connect('clicked', self.__stop_clicked_cb)
+ self.stop.show()
+ self.insert(self.stop, -1)
+ self.stop.show()
+
+ def get_entry(self):
+ return self._search_entry
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.DEFAULT_SPACING, -1)
+ self.insert(separator, -1)
+ separator.show()
+
+ def __search_entry_changed_cb(self, search_entry):
+ self.emit('search-changed', search_entry.props.text)
+
+ def __stop_clicked_cb(self, button):
+ self.emit('stop-clicked')
+
+class SectionToolbar(gtk.Toolbar):
+ """ Toolbar of the sections of the control panel
+ """
+ __gtype_name__ = 'SectionToolbar'
+
+ __gsignals__ = {
+ 'cancel-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([])),
+ 'accept-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([]))
+ }
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._add_separator()
+
+ self._icon = Icon()
+ self._add_widget(self._icon)
+
+ self._add_separator()
+
+ self._title = gtk.Label()
+ self._add_widget(self._title)
+
+ self._add_separator(True)
+
+ self.cancel_button = ToolButton('dialog-cancel')
+ self.cancel_button.set_tooltip(_('Cancel'))
+ self.cancel_button.connect('clicked', self.__cancel_button_clicked_cb)
+ self.insert(self.cancel_button, -1)
+ self.cancel_button.show()
+
+ self.accept_button = ToolButton('dialog-ok')
+ self.accept_button.set_tooltip(_('Ok'))
+ self.accept_button.connect('clicked', self.__accept_button_clicked_cb)
+ self.insert(self.accept_button, -1)
+ self.accept_button.show()
+
+ def get_icon(self):
+ return self._icon
+
+ def get_title(self):
+ return self._title
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.DEFAULT_SPACING, -1)
+ self.insert(separator, -1)
+ separator.show()
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ def __cancel_button_clicked_cb(self, widget, data=None):
+ self.emit('cancel-clicked')
+
+ def __accept_button_clicked_cb(self, widget, data=None):
+ self.emit('accept-clicked')
+
diff --git a/shell/src/jarabe/desktop/Makefile.am b/shell/src/jarabe/desktop/Makefile.am
new file mode 100644
index 0000000..25fb0b4
--- /dev/null
+++ b/shell/src/jarabe/desktop/Makefile.am
@@ -0,0 +1,18 @@
+sugardir = $(pythondir)/jarabe/desktop
+sugar_PYTHON = \
+ __init__.py \
+ activitieslist.py \
+ favoritesview.py \
+ favoriteslayout.py \
+ friendview.py \
+ grid.py \
+ groupbox.py \
+ homebox.py \
+ homewindow.py \
+ keydialog.py \
+ meshbox.py \
+ networkviews.py \
+ schoolserver.py \
+ snowflakelayout.py \
+ spreadlayout.py \
+ transitionbox.py
diff --git a/shell/src/jarabe/desktop/__init__.py b/shell/src/jarabe/desktop/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/shell/src/jarabe/desktop/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2006-2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
diff --git a/shell/src/jarabe/desktop/activitieslist.py b/shell/src/jarabe/desktop/activitieslist.py
new file mode 100644
index 0000000..e14d0f7
--- /dev/null
+++ b/shell/src/jarabe/desktop/activitieslist.py
@@ -0,0 +1,451 @@
+# Copyright (C) 2008 One Laptop Per Child
+# Copyright (C) 2009 Tomeu Vizoso
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+from gettext import gettext as _
+
+import gobject
+import pango
+import gconf
+import gtk
+
+from sugar import util
+from sugar.graphics import style
+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]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._query = ''
+
+ self.modify_base(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color())
+ self.set_headers_visible(False)
+ selection = self.get_selection()
+ selection.set_mode(gtk.SELECTION_NONE)
+
+ model = ListModel()
+ model.set_visible_func(self.__model_visible_cb)
+ self.set_model(model)
+
+ cell_favorite = CellRendererFavorite(self)
+ cell_favorite.connect('clicked', self.__favorite_clicked_cb)
+
+ column = gtk.TreeViewColumn()
+ column.pack_start(cell_favorite)
+ column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb)
+ self.append_column(column)
+
+ cell_icon = CellRendererActivityIcon(self)
+ cell_icon.connect('erase-activated', self.__erase_activated_cb)
+ cell_icon.connect('clicked', self.__icon_clicked_cb)
+
+ column = gtk.TreeViewColumn()
+ column.pack_start(cell_icon)
+ column.add_attribute(cell_icon, 'file-name', ListModel.COLUMN_ICON)
+ self.append_column(column)
+
+ cell_text = gtk.CellRendererText()
+ cell_text.props.ellipsize = pango.ELLIPSIZE_MIDDLE
+ cell_text.props.ellipsize_set = True
+
+ column = gtk.TreeViewColumn()
+ column.props.sizing = gtk.TREE_VIEW_COLUMN_GROW_ONLY
+ column.props.expand = True
+ column.set_sort_column_id(ListModel.COLUMN_TITLE)
+ column.pack_start(cell_text)
+ column.add_attribute(cell_text, 'markup', ListModel.COLUMN_TITLE)
+ self.append_column(column)
+
+ cell_text = gtk.CellRendererText()
+ cell_text.props.xalign = 1
+
+ column = gtk.TreeViewColumn()
+ column.set_alignment(1)
+ column.props.sizing = gtk.TREE_VIEW_COLUMN_GROW_ONLY
+ column.props.resizable = True
+ column.props.reorderable = True
+ column.props.expand = True
+ column.set_sort_column_id(ListModel.COLUMN_VERSION)
+ column.pack_start(cell_text)
+ column.add_attribute(cell_text, 'text', ListModel.COLUMN_VERSION_TEXT)
+ self.append_column(column)
+
+ cell_text = gtk.CellRendererText()
+ cell_text.props.xalign = 1
+
+ column = gtk.TreeViewColumn()
+ column.set_alignment(1)
+ column.props.sizing = gtk.TREE_VIEW_COLUMN_GROW_ONLY
+ column.props.resizable = True
+ column.props.reorderable = True
+ column.props.expand = True
+ column.set_sort_column_id(ListModel.COLUMN_DATE)
+ column.pack_start(cell_text)
+ column.add_attribute(cell_text, 'text', ListModel.COLUMN_DATE_TEXT)
+ self.append_column(column)
+
+ self.set_search_column(ListModel.COLUMN_TITLE)
+
+ def __erase_activated_cb(self, cell_renderer, bundle_id):
+ self.emit('erase-activated', bundle_id)
+
+ def __favorite_set_data_cb(self, column, cell, model, tree_iter):
+ favorite = model[tree_iter][ListModel.COLUMN_FAVORITE]
+ if favorite:
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ cell.props.xo_color = color
+ else:
+ cell.props.xo_color = None
+
+ def __favorite_clicked_cb(self, cell, path):
+ row = self.get_model()[path]
+ registry = bundleregistry.get_registry()
+ registry.set_bundle_favorite(row[ListModel.COLUMN_BUNDLE_ID],
+ row[ListModel.COLUMN_VERSION],
+ not row[ListModel.COLUMN_FAVORITE])
+
+ def __icon_clicked_cb(self, cell, path):
+ row = self.get_model()[path]
+
+ registry = bundleregistry.get_registry()
+ bundle = registry.get_bundle(row[ListModel.COLUMN_BUNDLE_ID])
+
+ misc.launch(bundle)
+
+ def set_filter(self, query):
+ self._query = query.lower()
+ self.get_model().refilter()
+
+ def __model_visible_cb(self, model, tree_iter):
+ 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'
+
+ COLUMN_BUNDLE_ID = 0
+ COLUMN_FAVORITE = 1
+ COLUMN_ICON = 2
+ COLUMN_TITLE = 3
+ COLUMN_VERSION = 4
+ COLUMN_VERSION_TEXT = 5
+ COLUMN_DATE = 6
+ COLUMN_DATE_TEXT = 7
+
+ def __init__(self):
+ self._model = gtk.ListStore(str, bool, str, str, int, str, int, str)
+ self._model_filter = self._model.filter_new()
+ gtk.TreeModelSort.__init__(self, self._model_filter)
+
+ gobject.idle_add(self.__connect_to_bundle_registry_cb)
+
+ def __connect_to_bundle_registry_cb(self):
+ registry = bundleregistry.get_registry()
+ for info in registry:
+ self._add_activity(info)
+ registry.connect('bundle-added', self.__activity_added_cb)
+ registry.connect('bundle-changed', self.__activity_changed_cb)
+ registry.connect('bundle-removed', self.__activity_removed_cb)
+
+ def __activity_added_cb(self, activity_registry, activity_info):
+ self._add_activity(activity_info)
+
+ def __activity_changed_cb(self, activity_registry, activity_info):
+ bundle_id = activity_info.get_bundle_id()
+ version = activity_info.get_activity_version()
+ favorite = activity_registry.is_bundle_favorite(bundle_id, version)
+ for row in self._model:
+ if row[ListModel.COLUMN_BUNDLE_ID] == bundle_id and \
+ row[ListModel.COLUMN_VERSION] == version:
+ row[ListModel.COLUMN_FAVORITE] = favorite
+ return
+
+ def __activity_removed_cb(self, activity_registry, activity_info):
+ bundle_id = activity_info.get_bundle_id()
+ version = activity_info.get_activity_version()
+ for row in self._model:
+ if row[ListModel.COLUMN_BUNDLE_ID] == bundle_id and \
+ row[ListModel.COLUMN_VERSION] == version:
+ self._model.remove(row.iter)
+ return
+
+ def _add_activity(self, activity_info):
+ if activity_info.get_bundle_id() == 'org.laptop.JournalActivity':
+ return
+
+ timestamp = activity_info.get_installation_time()
+ version = activity_info.get_activity_version()
+
+ registry = bundleregistry.get_registry()
+ favorite = registry.is_bundle_favorite(activity_info.get_bundle_id(),
+ version)
+
+ tag_list = activity_info.get_tags()
+ if tag_list is None or not tag_list:
+ title = '<b>%s</b>' % activity_info.get_name()
+ else:
+ tags = ', '.join(tag_list)
+ title = '<b>%s</b>\n' \
+ '<span style="italic" weight="light">%s</span>' % \
+ (activity_info.get_name(), tags)
+
+ self._model.append([activity_info.get_bundle_id(),
+ favorite,
+ activity_info.get_icon(),
+ title,
+ version,
+ _('Version %s') % version,
+ timestamp,
+ util.timestamp_to_elapsed_string(timestamp)])
+
+ def set_visible_func(self, func):
+ self._model_filter.set_visible_func(func)
+
+ def refilter(self):
+ self._model_filter.refilter()
+
+class CellRendererFavorite(CellRendererIcon):
+ __gtype_name__ = 'SugarCellRendererFavorite'
+
+ def __init__(self, tree_view):
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.SMALL_ICON_SIZE
+ self.props.icon_name = 'emblem-favorite'
+ self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE
+ 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]))
+ }
+
+ def __init__(self, tree_view):
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.STANDARD_ICON_SIZE
+ self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ self.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+ self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE
+
+ client = gconf.client_get_default()
+ prelit_color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ self.props.prelit_stroke_color = prelit_color.get_stroke_color()
+ self.props.prelit_fill_color = prelit_color.get_fill_color()
+
+ self._tree_view = tree_view
+
+ def create_palette(self):
+ model = self._tree_view.get_model()
+ row = model[self.props.palette_invoker.path]
+ bundle_id = row[ListModel.COLUMN_BUNDLE_ID]
+
+ registry = bundleregistry.get_registry()
+ palette = ActivityListPalette(registry.get_bundle(bundle_id))
+ palette.connect('erase-activated', self.__erase_activated_cb)
+ return palette
+
+ def __erase_activated_cb(self, palette, bundle_id):
+ self.emit('erase-activated', bundle_id)
+
+class ActivitiesList(gtk.VBox):
+ __gtype_name__ = 'SugarActivitiesList'
+
+ def __init__(self):
+ logging.debug('STARTUP: Loading the activities list')
+
+ gobject.GObject.__init__(self)
+
+ scrolled_window = gtk.ScrolledWindow()
+ scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ scrolled_window.set_shadow_type(gtk.SHADOW_NONE)
+ scrolled_window.connect('key-press-event', self.__key_press_event_cb)
+ self.pack_start(scrolled_window)
+ scrolled_window.show()
+
+ self._tree_view = ActivitiesTreeView()
+ self._tree_view.connect('erase-activated', self.__erase_activated_cb)
+ scrolled_window.add(self._tree_view)
+ self._tree_view.show()
+
+ self._alert = None
+
+ def set_filter(self, query):
+ self._tree_view.set_filter(query)
+
+ def __key_press_event_cb(self, scrolled_window, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+
+ vadjustment = scrolled_window.props.vadjustment
+ if keyname == 'Up':
+ if vadjustment.props.value > vadjustment.props.lower:
+ vadjustment.props.value -= vadjustment.props.step_increment
+ elif keyname == 'Down':
+ max_value = vadjustment.props.upper - vadjustment.props.page_size
+ if vadjustment.props.value < max_value:
+ vadjustment.props.value = min(
+ vadjustment.props.value + vadjustment.props.step_increment,
+ max_value)
+ else:
+ return False
+
+ return True
+
+ def add_alert(self, alert):
+ if self._alert is not None:
+ self.remove_alert()
+ self._alert = alert
+ self.pack_start(alert, False)
+ self.reorder_child(alert, 0)
+
+ def remove_alert(self):
+ self.remove(self._alert)
+ self._alert = None
+
+ def __erase_activated_cb(self, tree_view, bundle_id):
+ registry = bundleregistry.get_registry()
+ activity_info = registry.get_bundle(bundle_id)
+
+ alert = Alert()
+ alert.props.title = _('Confirm erase')
+ alert.props.msg = \
+ _('Confirm erase: Do you want to permanently erase %s?') \
+ % activity_info.get_name()
+
+ cancel_icon = Icon(icon_name='dialog-cancel')
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Keep'), cancel_icon)
+
+ erase_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Erase'), erase_icon)
+
+ alert.connect('response', self.__erase_confirmation_dialog_response_cb,
+ bundle_id)
+
+ self.add_alert(alert)
+
+ def __erase_confirmation_dialog_response_cb(self, alert, response_id,
+ bundle_id):
+ self.remove_alert()
+ if response_id == gtk.RESPONSE_OK:
+ registry = bundleregistry.get_registry()
+ bundle = registry.get_bundle(bundle_id)
+ registry.uninstall(bundle)
+
+class ActivityListPalette(ActivityPalette):
+ __gtype_name__ = 'SugarActivityListPalette'
+
+ __gsignals__ = {
+ 'erase-activated' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str]))
+ }
+
+ def __init__(self, activity_info):
+ ActivityPalette.__init__(self, activity_info)
+
+ self._bundle_id = activity_info.get_bundle_id()
+ self._version = activity_info.get_activity_version()
+
+ registry = bundleregistry.get_registry()
+ self._favorite = registry.is_bundle_favorite(self._bundle_id,
+ self._version)
+
+ self._favorite_item = MenuItem('')
+ self._favorite_icon = Icon(icon_name='emblem-favorite',
+ icon_size=gtk.ICON_SIZE_MENU)
+ self._favorite_item.set_image(self._favorite_icon)
+ self._favorite_item.connect('activate',
+ self.__change_favorite_activate_cb)
+ self.menu.append(self._favorite_item)
+ self._favorite_item.show()
+
+ 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
+
+ registry = bundleregistry.get_registry()
+ self._activity_changed_sid = registry.connect('bundle_changed',
+ self.__activity_changed_cb)
+ self._update_favorite_item()
+
+ self.connect('destroy', self.__destroy_cb)
+
+ def __destroy_cb(self, palette):
+ self.disconnect(self._activity_changed_sid)
+
+ def _update_favorite_item(self):
+ label = self._favorite_item.child
+ if self._favorite:
+ label.set_text(_('Remove favorite'))
+ xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ else:
+ label.set_text(_('Make favorite'))
+ client = gconf.client_get_default()
+ xo_color = XoColor(client.get_string("/desktop/sugar/user/color"))
+
+ self._favorite_icon.props.xo_color = xo_color
+
+ def __change_favorite_activate_cb(self, menu_item):
+ registry = bundleregistry.get_registry()
+ registry.set_bundle_favorite(self._bundle_id,
+ self._version,
+ not self._favorite)
+
+ def __activity_changed_cb(self, activity_registry, activity_info):
+ if activity_info.get_bundle_id() == self._bundle_id and \
+ activity_info.get_activity_version() == self._version:
+ registry = bundleregistry.get_registry()
+ self._favorite = registry.is_bundle_favorite(self._bundle_id,
+ self._version)
+ self._update_favorite_item()
+
+ def __erase_activate_cb(self, menu_item):
+ self.emit('erase-activated', self._bundle_id)
+
diff --git a/shell/src/jarabe/desktop/favoriteslayout.py b/shell/src/jarabe/desktop/favoriteslayout.py
new file mode 100644
index 0000000..85e1b59
--- /dev/null
+++ b/shell/src/jarabe/desktop/favoriteslayout.py
@@ -0,0 +1,488 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import math
+import hashlib
+from gettext import gettext as _
+
+import gobject
+import gtk
+import hippo
+
+from sugar.graphics import style
+
+from jarabe.model import bundleregistry
+from jarabe.desktop.grid import Grid
+
+_logger = logging.getLogger('FavoritesLayout')
+
+_CELL_SIZE = 4
+_BASE_SCALE = 1000
+
+class FavoritesLayout(gobject.GObject, hippo.CanvasLayout):
+ """Base class of the different layout types."""
+
+ __gtype_name__ = 'FavoritesLayout'
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self.box = None
+ self.fixed_positions = {}
+
+ def do_set_box(self, box):
+ self.box = box
+
+ def do_get_height_request(self, for_width):
+ return 0, gtk.gdk.screen_height() - style.GRID_CELL_SIZE
+
+ def do_get_width_request(self):
+ return 0, gtk.gdk.screen_width()
+
+ def compare_activities(self, icon_a, icon_b):
+ return 0
+
+ def append(self, icon, locked=False):
+ if not hasattr(type(icon), 'fixed_position'):
+ logging.debug('Icon without fixed_position: %r', icon)
+ return
+
+ icon.props.size = max(icon.props.size, style.STANDARD_ICON_SIZE)
+
+ relative_x, relative_y = icon.fixed_position
+ if relative_x < 0 or relative_y < 0:
+ logging.debug('Icon out of bounds: %r', icon)
+ return
+
+ min_width_, width = self.box.get_width_request()
+ min_height_, height = self.box.get_height_request(width)
+ self.fixed_positions[icon] = \
+ (int(relative_x * _BASE_SCALE / float(width)),
+ int(relative_y * _BASE_SCALE / float(height)))
+
+ def remove(self, icon):
+ if icon in self.fixed_positions:
+ del self.fixed_positions[icon]
+
+ def move_icon(self, icon, x, y, locked=False):
+ 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')):
+ logging.debug('Not an activity icon %r', icon)
+ return
+
+ min_width_, width = self.box.get_width_request()
+ min_height_, height = self.box.get_height_request(width)
+ registry = bundleregistry.get_registry()
+ registry.set_bundle_position(
+ icon.get_bundle_id(), icon.get_version(),
+ x * width / float(_BASE_SCALE),
+ y * height / float(_BASE_SCALE))
+ self.fixed_positions[icon] = (x, y)
+
+ def do_allocate(self, x, y, width, height, req_width, req_height,
+ origin_changed):
+ raise NotImplementedError()
+
+ def allow_dnd(self):
+ return False
+
+class RandomLayout(FavoritesLayout):
+ """Lay out icons randomly; try to nudge them around to resolve overlaps."""
+
+ __gtype_name__ = 'RandomLayout'
+
+ icon_name = 'view-freeform'
+ """Name of icon used in home view dropdown palette."""
+
+ key = 'random-layout'
+ """String used in profile to represent this view."""
+
+ # TRANS: label for the freeform layout in the favorites view
+ palette_name = _('Freeform')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ FavoritesLayout.__init__(self)
+
+ min_width_, width = self.do_get_width_request()
+ min_height_, height = self.do_get_height_request(width)
+
+ self._grid = Grid(width / _CELL_SIZE, height / _CELL_SIZE)
+ self._grid.connect('child-changed', self.__grid_child_changed_cb)
+
+ def __grid_child_changed_cb(self, grid, child):
+ child.emit_request_changed()
+
+ def append(self, icon, locked=False):
+ FavoritesLayout.append(self, icon, locked)
+
+ min_width_, child_width = icon.get_width_request()
+ min_height_, child_height = icon.get_height_request(child_width)
+ min_width_, width = self.box.get_width_request()
+ min_height_, height = self.box.get_height_request(width)
+
+ if icon in self.fixed_positions:
+ x, y = self.fixed_positions[icon]
+ x = min(x, width - child_width)
+ y = min(y, height - child_height)
+ elif hasattr(icon, 'get_bundle_id'):
+ name_hash = hashlib.md5(icon.get_bundle_id())
+ x = int(name_hash.hexdigest()[:5], 16) % (width - child_width)
+ y = int(name_hash.hexdigest()[-5:], 16) % (height - child_height)
+ else:
+ x = None
+ y = None
+
+ if x is None or y is None:
+ self._grid.add(icon,
+ child_width / _CELL_SIZE, child_height / _CELL_SIZE)
+ else:
+ self._grid.add(icon,
+ child_width / _CELL_SIZE, child_height / _CELL_SIZE,
+ x / _CELL_SIZE, y / _CELL_SIZE)
+
+ def remove(self, icon):
+ self._grid.remove(icon)
+ FavoritesLayout.remove(self, icon)
+
+ def move_icon(self, icon, x, y, locked=False):
+ self._grid.move(icon, x / _CELL_SIZE, y / _CELL_SIZE, locked)
+ FavoritesLayout.move_icon(self, icon, x, y, locked)
+
+ def do_allocate(self, x, y, width, height, req_width, req_height,
+ origin_changed):
+ for child in self.box.get_layout_children():
+ # We need to always get requests to not confuse hippo
+ min_w_, child_width = child.get_width_request()
+ min_h_, child_height = child.get_height_request(child_width)
+
+ rect = self._grid.get_child_rect(child.item)
+ child.allocate(rect.x * _CELL_SIZE,
+ rect.y * _CELL_SIZE,
+ child_width,
+ child_height,
+ origin_changed)
+
+ def allow_dnd(self):
+ return True
+
+_MINIMUM_RADIUS = style.XLARGE_ICON_SIZE / 2 + style.DEFAULT_SPACING + \
+ style.STANDARD_ICON_SIZE * 2
+_MAXIMUM_RADIUS = (gtk.gdk.screen_height() - style.GRID_CELL_SIZE) / 2 - \
+ style.STANDARD_ICON_SIZE - style.DEFAULT_SPACING
+
+class RingLayout(FavoritesLayout):
+ """Lay out icons in a ring around the XO man."""
+
+ __gtype_name__ = 'RingLayout'
+ icon_name = 'view-radial'
+ """Name of icon used in home view dropdown palette."""
+ key = 'ring-layout'
+ """String used in profile to represent this view."""
+ # TRANS: label for the ring layout in the favorites view
+ palette_name = _('Ring')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ FavoritesLayout.__init__(self)
+ self._locked_children = {}
+
+ def append(self, icon, locked=False):
+ FavoritesLayout.append(self, icon, locked)
+ if locked:
+ child = self.box.find_box_child(icon)
+ self._locked_children[child] = (0, 0)
+
+ def remove(self, icon):
+ child = self.box.find_box_child(icon)
+ if child in self._locked_children:
+ del self._locked_children[child]
+ FavoritesLayout.remove(self, icon)
+
+ def move_icon(self, icon, x, y, locked=False):
+ FavoritesLayout.move_icon(self, icon, x, y, locked)
+ if locked:
+ child = self.box.find_box_child(icon)
+ self._locked_children[child] = (x, y)
+
+ def _calculate_radius_and_icon_size(self, children_count):
+ # what's the radius required without downscaling?
+ distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING
+ icon_size = style.STANDARD_ICON_SIZE
+ # circumference is 2*pi*r; we want this to be at least
+ # 'children_count * distance'
+ radius = children_count * distance / (2 * math.pi)
+ # limit computed radius to reasonable bounds.
+ radius = max(radius, _MINIMUM_RADIUS)
+ radius = min(radius, _MAXIMUM_RADIUS)
+ # recompute icon size from limited radius
+ if children_count > 0:
+ icon_size = (2 * math.pi * radius / children_count) \
+ - style.DEFAULT_SPACING
+ # limit adjusted icon size.
+ icon_size = max(icon_size, style.SMALL_ICON_SIZE)
+ icon_size = min(icon_size, style.MEDIUM_ICON_SIZE)
+ return radius, icon_size
+
+ def _calculate_position(self, radius, icon_size, index, children_count,
+ sin=math.sin, cos=math.cos):
+ width, height = self.box.get_allocation()
+ angle = index * (2 * math.pi / children_count) - math.pi / 2
+ x = radius * cos(angle) + (width - icon_size) / 2
+ y = radius * sin(angle) + (height - icon_size -
+ (style.GRID_CELL_SIZE/2) ) / 2
+ return x, y
+
+ def _get_children_in_ring(self):
+ children_in_ring = [child for child in self.box.get_layout_children() \
+ if child not in self._locked_children]
+ return children_in_ring
+
+ def do_allocate(self, x, y, width, height, req_width, req_height,
+ origin_changed):
+ children_in_ring = self._get_children_in_ring()
+ if children_in_ring:
+ radius, icon_size = \
+ self._calculate_radius_and_icon_size(len(children_in_ring))
+
+ for n in range(len(children_in_ring)):
+ child = children_in_ring[n]
+
+ x, y = self._calculate_position(radius, icon_size, n,
+ len(children_in_ring))
+
+ # We need to always get requests to not confuse hippo
+ min_w_, child_width = child.get_width_request()
+ min_h_, child_height = child.get_height_request(child_width)
+
+ child.allocate(int(x), int(y), child_width, child_height,
+ origin_changed)
+ child.item.props.size = icon_size
+
+ for child in self._locked_children.keys():
+ x, y = self._locked_children[child]
+
+ # We need to always get requests to not confuse hippo
+ min_w_, child_width = child.get_width_request()
+ min_h_, child_height = child.get_height_request(child_width)
+
+ if child_width <= 0 or child_height <= 0:
+ return
+
+ child.allocate(int(x), int(y), child_width, child_height,
+ origin_changed)
+
+ def compare_activities(self, icon_a, icon_b):
+ if hasattr(icon_a, 'installation_time') and \
+ hasattr(icon_b, 'installation_time'):
+ return icon_b.installation_time - icon_a.installation_time
+ else:
+ return 0
+
+_SUNFLOWER_CONSTANT = style.STANDARD_ICON_SIZE * .75
+"""Chose a constant such that STANDARD_ICON_SIZE icons are nicely spaced."""
+
+_SUNFLOWER_OFFSET = \
+ math.pow((style.XLARGE_ICON_SIZE / 2 + style.STANDARD_ICON_SIZE) /
+ _SUNFLOWER_CONSTANT, 2)
+"""
+Compute a starting index for the `SunflowerLayout` which leaves space for
+the XO man in the center. Since r = _SUNFLOWER_CONSTANT * sqrt(n),
+solve for n when r is (XLARGE_ICON_SIZE + STANDARD_ICON_SIZE)/2.
+"""
+
+_GOLDEN_RATIO = 1.6180339887498949
+"""
+Golden ratio: http://en.wikipedia.org/wiki/Golden_ratio
+Calculation: (math.sqrt(5) + 1) / 2
+"""
+
+_SUNFLOWER_ANGLE = 2.3999632297286531
+"""
+The sunflower angle is approximately 137.5 degrees.
+This is the golden angle: http://en.wikipedia.org/wiki/Golden_angle
+Calculation: math.radians(360) / ( _GOLDEN_RATIO * _GOLDEN_RATIO )
+"""
+
+class SunflowerLayout(RingLayout):
+ """Spiral layout based on Fibonacci ratio in phyllotaxis.
+
+ See http://algorithmicbotany.org/papers/abop/abop-ch4.pdf
+ for details of Vogel's model of florets in a sunflower head."""
+
+ __gtype_name__ = 'SunflowerLayout'
+
+ icon_name = 'view-spiral'
+ """Name of icon used in home view dropdown palette."""
+
+ key = 'spiral-layout'
+ """String used in profile to represent this view."""
+
+ # TRANS: label for the spiral layout in the favorites view
+ palette_name = _('Spiral')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ RingLayout.__init__(self)
+ self.skipped_indices = []
+
+ def _calculate_radius_and_icon_size(self, children_count):
+ """Stub out this method; not used in `SunflowerLayout`."""
+ return None, style.STANDARD_ICON_SIZE
+
+ def adjust_index(self, i):
+ """Skip floret indices which end up outside the desired bounding box."""
+ for idx in self.skipped_indices:
+ if i < idx:
+ break
+ i += 1
+ return i
+
+ def _calculate_position(self, radius, icon_size, oindex, children_count,
+ sin=math.sin, cos=math.cos):
+ """Calculate the position of sunflower floret number 'oindex'.
+ If the result is outside the bounding box, use the next index which
+ is inside the bounding box."""
+
+ width, height = self.box.get_allocation()
+
+ while True:
+
+ index = self.adjust_index(oindex)
+
+ # tweak phi to get a nice gap lined up where the "active activity"
+ # icon is, below the central XO man.
+ phi = index * _SUNFLOWER_ANGLE + math.radians(-130)
+
+ # we offset index when computing r to make space for the XO man.
+ r = _SUNFLOWER_CONSTANT * math.sqrt(index + _SUNFLOWER_OFFSET)
+
+ # x,y are the top-left corner of the icon, so remove icon_size
+ # from width/height to compensate. y has an extra GRID_CELL_SIZE/2
+ # removed to make room for the "active activity" icon.
+ x = r * cos(phi) + (width - icon_size) / 2
+ y = r * sin(phi) + (height - icon_size - \
+ (style.GRID_CELL_SIZE / 2) ) / 2
+
+ # skip allocations outside the allocation box.
+ # give up once we can't fit
+ if r < math.hypot(width / 2, height / 2):
+ if y < 0 or y > (height - icon_size) or \
+ x < 0 or x > (width - icon_size):
+ self.skipped_indices.append(index)
+ continue # try again
+
+ return x, y
+
+class BoxLayout(RingLayout):
+ """Lay out icons in a square around the XO man."""
+
+ __gtype_name__ = 'BoxLayout'
+
+ icon_name = 'view-box'
+ """Name of icon used in home view dropdown palette."""
+
+ key = 'box-layout'
+ """String used in profile to represent this view."""
+
+ # TRANS: label for the box layout in the favorites view
+ palette_name = _('Box')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ RingLayout.__init__(self)
+
+ def _calculate_position(self, radius, icon_size, index, children_count,
+ sin=None, cos=None):
+
+ # use "orthogonal" versions of cos and sin in order to square the
+ # circle and turn the 'ring view' into a 'box view'
+ def cos_d(d):
+ while d < 0:
+ d += 360
+ if d < 45:
+ return 1
+ if d < 135:
+ return (90 - d) / 45.
+ if d < 225:
+ return -1
+ return cos_d(360 - d) # mirror around 180
+
+ cos = lambda r: cos_d(math.degrees(r))
+ sin = lambda r: cos_d(math.degrees(r) - 90)
+
+ return RingLayout._calculate_position\
+ (self, radius, icon_size, index, children_count,
+ sin=sin, cos=cos)
+
+class TriangleLayout(RingLayout):
+ """Lay out icons in a triangle around the XO man."""
+
+ __gtype_name__ = 'TriangleLayout'
+
+ icon_name = 'view-triangle'
+ """Name of icon used in home view dropdown palette."""
+
+ key = 'triangle-layout'
+ """String used in profile to represent this view."""
+
+ # TRANS: label for the box layout in the favorites view
+ palette_name = _('Triangle')
+ """String used to identify this layout in home view dropdown palette."""
+
+ def __init__(self):
+ RingLayout.__init__(self)
+
+ def _calculate_radius_and_icon_size(self, children_count):
+ # use slightly larger minimum radius than parent, because sides
+ # of triangle come awful close to the center.
+ radius, icon_size = \
+ RingLayout._calculate_radius_and_icon_size(self, children_count)
+ return max(radius, _MINIMUM_RADIUS + style.MEDIUM_ICON_SIZE), icon_size
+
+ def _calculate_position(self, radius, icon_size, index, children_count,
+ sin=math.sin, cos=math.cos):
+ # tweak cos and sin in order to make the 'ring' into an equilateral
+ # triangle.
+
+ def cos_d(d):
+ while d < -90:
+ d += 360
+ if d <= 30:
+ return (d + 90) / 120.
+ if d <= 90:
+ return (90 - d) / 60.
+ return -cos_d(180 - d) # mirror around 90
+
+ sqrt_3 = math.sqrt(3)
+
+ def sin_d(d):
+ while d < -90:
+ d += 360
+ if d <= 30:
+ return ((d + 90) / 120.) * sqrt_3 - 1
+ if d <= 90:
+ return sqrt_3 - 1
+ return sin_d(180 - d) # mirror around 90
+
+ cos = lambda r: cos_d(math.degrees(r))
+ sin = lambda r: sin_d(math.degrees(r))
+
+ return RingLayout._calculate_position\
+ (self, radius, icon_size, index, children_count,
+ sin=sin, cos=cos)
diff --git a/shell/src/jarabe/desktop/favoritesview.py b/shell/src/jarabe/desktop/favoritesview.py
new file mode 100644
index 0000000..bb85024
--- /dev/null
+++ b/shell/src/jarabe/desktop/favoritesview.py
@@ -0,0 +1,670 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+import math
+
+import gobject
+import gconf
+import gtk
+import hippo
+
+from sugar.graphics import style
+from sugar.graphics.icon import Icon, CanvasIcon
+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, 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 import favoriteslayout
+
+_logger = logging.getLogger('FavoritesView')
+
+_ICON_DND_TARGET = ('activity-icon', gtk.TARGET_SAME_WIDGET, 0)
+
+LAYOUT_MAP = {favoriteslayout.RingLayout.key: favoriteslayout.RingLayout,
+ #favoriteslayout.BoxLayout.key: favoriteslayout.BoxLayout,
+ #favoriteslayout.TriangleLayout.key: favoriteslayout.TriangleLayout,
+ #favoriteslayout.SunflowerLayout.key: favoriteslayout.SunflowerLayout,
+ favoriteslayout.RandomLayout.key: favoriteslayout.RandomLayout}
+"""Map numeric layout identifiers to uninstantiated subclasses of
+`FavoritesLayout` which implement the layouts. Additional information
+about the layout can be accessed with fields of the class."""
+
+class FavoritesView(hippo.Canvas):
+ __gtype_name__ = 'SugarFavoritesView'
+
+ def __init__(self, **kwargs):
+ logging.debug('STARTUP: Loading the favorites view')
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ # DND stuff
+ self._pressed_button = None
+ self._press_start_x = None
+ self._press_start_y = None
+ self._hot_x = None
+ self._hot_y = None
+ self._last_clicked_icon = None
+
+ self._box = hippo.CanvasBox()
+ self._box.props.background_color = style.COLOR_WHITE.get_int()
+ self.set_root(self._box)
+
+ self._my_icon = OwnerIcon(style.XLARGE_ICON_SIZE)
+ self._my_icon.connect('register-activate', self.__register_activate_cb)
+ self._box.append(self._my_icon)
+
+ self._current_activity = CurrentActivityIcon()
+ self._box.append(self._current_activity)
+
+ self._layout = None
+ self._alert = None
+ self._resume_mode = True
+
+ # More DND stuff
+ self.add_events(gtk.gdk.BUTTON_PRESS_MASK |
+ gtk.gdk.POINTER_MOTION_HINT_MASK)
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+ self.connect('button-press-event', self.__button_press_event_cb)
+ self.connect('drag-begin', self.__drag_begin_cb)
+ self.connect('drag-motion', self.__drag_motion_cb)
+ self.connect('drag-drop', self.__drag_drop_cb)
+ self.connect('drag-data-received', self.__drag_data_received_cb)
+
+ gobject.idle_add(self.__connect_to_bundle_registry_cb)
+
+ favorites_settings = get_settings()
+ favorites_settings.changed.connect(self.__settings_changed_cb)
+ self._set_layout(favorites_settings.layout)
+
+ def __settings_changed_cb(self, **kwargs):
+ favorites_settings = get_settings()
+ self._set_layout(favorites_settings.layout)
+
+ def __connect_to_bundle_registry_cb(self):
+ registry = bundleregistry.get_registry()
+
+ for info in registry:
+ if registry.is_bundle_favorite(info.get_bundle_id(),
+ info.get_activity_version()):
+ self._add_activity(info)
+
+ registry.connect('bundle-added', self.__activity_added_cb)
+ registry.connect('bundle-removed', self.__activity_removed_cb)
+ registry.connect('bundle-changed', self.__activity_changed_cb)
+
+ def _add_activity(self, activity_info):
+ if activity_info.get_bundle_id() == 'org.laptop.JournalActivity':
+ return
+ icon = ActivityIcon(activity_info)
+ icon.props.size = style.STANDARD_ICON_SIZE
+ icon.set_resume_mode(self._resume_mode)
+ self._box.insert_sorted(icon, 0, self._layout.compare_activities)
+ self._layout.append(icon)
+
+ def __activity_added_cb(self, activity_registry, activity_info):
+ registry = bundleregistry.get_registry()
+ if registry.is_bundle_favorite(activity_info.get_bundle_id(),
+ activity_info.get_activity_version()):
+ self._add_activity(activity_info)
+
+ def _find_activity_icon(self, bundle_id, version):
+ for icon in self._box.get_children():
+ if isinstance(icon, ActivityIcon) and \
+ icon.bundle_id == bundle_id and icon.version == version:
+ return icon
+ return None
+
+ def __activity_removed_cb(self, activity_registry, activity_info):
+ icon = self._find_activity_icon(activity_info.get_bundle_id(),
+ activity_info.get_activity_version())
+ if icon is not None:
+ self._layout.remove(icon)
+ self._box.remove(icon)
+
+ def __activity_changed_cb(self, activity_registry, activity_info):
+ if activity_info.get_bundle_id() == 'org.laptop.JournalActivity':
+ return
+ icon = self._find_activity_icon(activity_info.get_bundle_id(),
+ activity_info.get_activity_version())
+ if icon is not None:
+ self._box.remove(icon)
+
+ registry = bundleregistry.get_registry()
+ if registry.is_bundle_favorite(activity_info.get_bundle_id(),
+ activity_info.get_activity_version()):
+ self._add_activity(activity_info)
+
+ def do_size_allocate(self, allocation):
+ width = allocation.width
+ height = allocation.height
+
+ min_w_, my_icon_width = self._my_icon.get_width_request()
+ min_h_, my_icon_height = self._my_icon.get_height_request(my_icon_width)
+ x = (width - my_icon_width) / 2
+ y = (height - my_icon_height - style.GRID_CELL_SIZE) / 2
+ self._layout.move_icon(self._my_icon, x, y, locked=True)
+
+ min_w_, icon_width = self._current_activity.get_width_request()
+ min_h_, icon_height = \
+ self._current_activity.get_height_request(icon_width)
+ x = (width - icon_width) / 2
+ y = (height - my_icon_height - style.GRID_CELL_SIZE) / 2 + \
+ my_icon_height + style.DEFAULT_PADDING
+ self._layout.move_icon(self._current_activity, x, y, locked=True)
+
+ hippo.Canvas.do_size_allocate(self, allocation)
+
+ # TODO: Dnd methods. This should be merged somehow inside hippo-canvas.
+ def __button_press_event_cb(self, widget, event):
+ if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS:
+ self._last_clicked_icon = self._get_icon_at_coords(event.x, event.y)
+ if self._last_clicked_icon is not None:
+ self._pressed_button = event.button
+ self._press_start_x = event.x
+ self._press_start_y = event.y
+
+ return False
+
+ def _get_icon_at_coords(self, x, y):
+ for icon in self._box.get_children():
+ icon_x, icon_y = icon.get_context().translate_to_widget(icon)
+ icon_width, icon_height = icon.get_allocation()
+
+ if (x >= icon_x ) and (x <= icon_x + icon_width) and \
+ (y >= icon_y ) and (y <= icon_y + icon_height) and \
+ isinstance(icon, ActivityIcon):
+ return icon
+ return None
+
+ def __motion_notify_event_cb(self, widget, event):
+ if not self._pressed_button:
+ return False
+
+ # if the mouse button is not pressed, no drag should occurr
+ if not event.state & gtk.gdk.BUTTON1_MASK:
+ self._pressed_button = None
+ return False
+
+ if event.is_hint:
+ x, y, state_ = event.window.get_pointer()
+ else:
+ x = event.x
+ y = event.y
+
+ if widget.drag_check_threshold(int(self._press_start_x),
+ int(self._press_start_y),
+ int(x),
+ int(y)):
+ context_ = widget.drag_begin([_ICON_DND_TARGET],
+ gtk.gdk.ACTION_MOVE,
+ 1,
+ event)
+ return False
+
+ def __drag_begin_cb(self, widget, context):
+ icon_file_name = self._last_clicked_icon.props.file_name
+ # TODO: we should get the pixbuf from the widget, so it has colors, etc
+ pixbuf = gtk.gdk.pixbuf_new_from_file(icon_file_name)
+
+ self._hot_x = pixbuf.props.width / 2
+ self._hot_y = pixbuf.props.height / 2
+ context.set_icon_pixbuf(pixbuf, self._hot_x, self._hot_y)
+
+ def __drag_motion_cb(self, widget, context, x, y, time):
+ if self._last_clicked_icon is not None:
+ context.drag_status(context.suggested_action, time)
+ return True
+ else:
+ return False
+
+ def __drag_drop_cb(self, widget, context, x, y, time):
+ if self._last_clicked_icon is not None:
+ self.drag_get_data(context, _ICON_DND_TARGET[0])
+
+ self._layout.move_icon(self._last_clicked_icon,
+ x - self._hot_x, y - self._hot_y)
+
+ self._pressed_button = None
+ self._press_start_x = None
+ self._press_start_y = None
+ self._hot_x = None
+ self._hot_y = None
+ self._last_clicked_icon = None
+
+ return True
+ else:
+ return False
+
+ def __drag_data_received_cb(self, widget, context, x, y, selection_data,
+ info, time):
+ context.drop_finish(success=True, time=time)
+
+ def _set_layout(self, layout):
+ if layout not in LAYOUT_MAP:
+ logging.warn('Unknown favorites layout: %r' % layout)
+ layout = favoriteslayout.RingLayout.key
+ assert layout in LAYOUT_MAP
+
+ if type(self._layout) == LAYOUT_MAP[layout]:
+ return
+
+ self._layout = LAYOUT_MAP[layout]()
+ self._box.set_layout(self._layout)
+
+ #TODO: compatibility hack while sort() gets added to the hippo python
+ # bindings
+ if hasattr(self._box, 'sort'):
+ self._box.sort(self._layout.compare_activities)
+
+ for icon in self._box.get_children():
+ if icon not in [self._my_icon, self._current_activity]:
+ self._layout.append(icon)
+
+ self._layout.append(self._my_icon, locked=True)
+ self._layout.append(self._current_activity, locked=True)
+
+ if self._layout.allow_dnd():
+ self.drag_source_set(0, [], 0)
+ self.drag_dest_set(0, [], 0)
+ else:
+ self.drag_source_unset()
+ self.drag_dest_unset()
+
+ layout = property(None, _set_layout)
+
+ def add_alert(self, alert):
+ if self._alert is not None:
+ self.remove_alert()
+ alert.set_size_request(gtk.gdk.screen_width(), -1)
+ self._alert = hippo.CanvasWidget(widget=alert)
+ self._box.append(self._alert, hippo.PACK_FIXED)
+
+ def remove_alert(self):
+ self._box.remove(self._alert)
+ self._alert = None
+
+ def __register_activate_cb(self, icon):
+ alert = Alert()
+ try:
+ schoolserver.register_laptop()
+ except RegisterError, e:
+ alert.props.title = _('Registration Failed')
+ alert.props.msg = _('%s') % e
+ else:
+ alert.props.title = _('Registration Successful')
+ alert.props.msg = _('You are now registered ' \
+ 'with your school server.')
+ self._my_icon.remove_register_menu()
+
+ ok_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Ok'), ok_icon)
+
+ self.add_alert(alert)
+ alert.connect('response', self.__register_alert_response_cb)
+
+ def __register_alert_response_cb(self, alert, response_id):
+ self.remove_alert()
+
+ def set_resume_mode(self, resume_mode):
+ self._resume_mode = resume_mode
+ for icon in self._box.get_children():
+ if hasattr(icon, 'set_resume_mode'):
+ icon.set_resume_mode(self._resume_mode)
+
+
+class ActivityIcon(CanvasIcon):
+ __gtype_name__ = 'SugarFavoriteActivityIcon'
+
+ _BORDER_WIDTH = style.zoom(3)
+ _MAX_RESUME_ENTRIES = 5
+
+ def __init__(self, activity_info):
+ CanvasIcon.__init__(self, cache=True,
+ file_name=activity_info.get_icon())
+
+ self._activity_info = activity_info
+ self._journal_entries = []
+ self._hovering = False
+ self._resume_mode = True
+
+ self.connect('hovering-changed', self.__hovering_changed_event_cb)
+ self.connect('button-release-event', self.__button_release_event_cb)
+
+ datastore.updated.connect(self.__datastore_listener_updated_cb)
+ datastore.deleted.connect(self.__datastore_listener_deleted_cb)
+
+ self._refresh()
+ self._update()
+
+ def _refresh(self):
+ bundle_id = self._activity_info.get_bundle_id()
+ properties = ['uid', 'title', 'icon-color', 'activity', 'activity_id',
+ 'mime_type', 'mountpoint']
+ self._get_last_activity_async(bundle_id, properties)
+
+ def __datastore_listener_updated_cb(self, **kwargs):
+ bundle_id = self._activity_info.get_bundle_id()
+ if kwargs['metadata'].get('activity', '') == bundle_id:
+ self._refresh()
+
+ def __datastore_listener_deleted_cb(self, **kwargs):
+ for entry in self._journal_entries:
+ if entry['uid'] == kwargs['object_id']:
+ self._refresh()
+ break
+
+ def _get_last_activity_async(self, bundle_id, properties):
+ query = {'activity': bundle_id}
+ datastore.find(query, sorting=['+timestamp'],
+ limit=self._MAX_RESUME_ENTRIES,
+ properties=properties,
+ reply_handler=self.__get_last_activity_reply_handler_cb,
+ 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.
+ checked_entries = []
+ for entry in entries:
+ if entry['activity'] == self.bundle_id:
+ checked_entries.append(entry)
+
+ self._journal_entries = checked_entries
+ self._update()
+
+ def __get_last_activity_error_handler_cb(self, error):
+ logging.error('Error retrieving most recent activities: %r', error)
+
+ def _update(self):
+ self.palette = None
+ if not self._resume_mode or not self._journal_entries:
+ xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ else:
+ xo_color = misc.get_icon_color(self._journal_entries[0])
+ self.props.xo_color = xo_color
+
+ def create_palette(self):
+ palette = FavoritePalette(self._activity_info, self._journal_entries)
+ palette.connect('activate', self.__palette_activate_cb)
+ palette.connect('entry-activate', self.__palette_entry_activate_cb)
+ return palette
+
+ def __palette_activate_cb(self, palette):
+ self._activate()
+
+ def __palette_entry_activate_cb(self, palette, metadata):
+ self._resume(metadata)
+
+ def __hovering_changed_event_cb(self, icon, hovering):
+ self._hovering = hovering
+ self.emit_paint_needed(0, 0, -1, -1)
+
+ def do_paint_above_children(self, cr, damaged_box):
+ if not self._hovering:
+ return
+
+ width, height = self.get_allocation()
+
+ x = ActivityIcon._BORDER_WIDTH / 2.0
+ y = ActivityIcon._BORDER_WIDTH / 2.0
+ width -= ActivityIcon._BORDER_WIDTH
+ height -= ActivityIcon._BORDER_WIDTH
+ radius = width / 10.0
+
+ cr.move_to(x + radius, y)
+ cr.arc(x + width - radius, y + radius, radius, math.pi * 1.5,
+ math.pi * 2.0)
+ cr.arc(x + width - radius, x + height - radius, radius, 0,
+ math.pi * 0.5)
+ cr.arc(x + radius, y + height - radius, radius, math.pi * 0.5, math.pi)
+ cr.arc(x + radius, y + radius, radius, math.pi, math.pi * 1.5)
+
+ color = style.COLOR_SELECTION_GREY.get_int()
+ hippo.cairo_set_source_rgba32(cr, color)
+ cr.set_line_width(ActivityIcon._BORDER_WIDTH)
+ cr.stroke()
+
+ def do_get_content_height_request(self, for_width):
+ height, height = CanvasIcon.do_get_content_height_request(self,
+ for_width)
+ height += ActivityIcon._BORDER_WIDTH * 2
+ return height, height
+
+ def do_get_content_width_request(self):
+ width, width = CanvasIcon.do_get_content_width_request(self)
+ width += ActivityIcon._BORDER_WIDTH * 2
+ return width, width
+
+ def __button_release_event_cb(self, icon, event):
+ self._activate()
+
+ def _resume(self, journal_entry):
+ if not journal_entry['activity_id']:
+ journal_entry['activity_id'] = activityfactory.create_activity_id()
+ misc.resume(journal_entry, self._activity_info.get_bundle_id())
+
+ def _activate(self):
+ if self.palette is not None:
+ self.palette.popdown(immediate=True)
+
+ if self._resume_mode and self._journal_entries:
+ self._resume(self._journal_entries[0])
+ else:
+ misc.launch(self._activity_info)
+
+ def get_bundle_id(self):
+ return self._activity_info.get_bundle_id()
+ bundle_id = property(get_bundle_id, None)
+
+ def get_version(self):
+ return self._activity_info.get_activity_version()
+ version = property(get_version, None)
+
+ def _get_installation_time(self):
+ return self._activity_info.get_installation_time()
+ installation_time = property(_get_installation_time, None)
+
+ def _get_fixed_position(self):
+ registry = bundleregistry.get_registry()
+ return registry.get_bundle_position(self.bundle_id, self.version)
+ fixed_position = property(_get_fixed_position, None)
+
+ def set_resume_mode(self, resume_mode):
+ self._resume_mode = resume_mode
+ self._update()
+
+class FavoritePalette(ActivityPalette):
+ __gtype_name__ = 'SugarFavoritePalette'
+
+ __gsignals__ = {
+ 'entry-activate': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([object])),
+ }
+
+ def __init__(self, activity_info, journal_entries):
+ ActivityPalette.__init__(self, activity_info)
+
+ if not journal_entries:
+ xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ else:
+ xo_color = misc.get_icon_color(journal_entries[0])
+
+ self.props.icon = Icon(file=activity_info.get_icon(),
+ xo_color=xo_color,
+ icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR)
+
+ if journal_entries:
+ self.props.secondary_text = journal_entries[0]['title']
+
+ menu_items = []
+ for entry in journal_entries:
+ icon_file_name = misc.get_icon_name(entry)
+ color = misc.get_icon_color(entry)
+
+ menu_item = MenuItem(text_label=entry['title'],
+ file_name=icon_file_name,
+ xo_color=color)
+ menu_item.connect('activate', self.__resume_entry_cb, entry)
+ menu_items.append(menu_item)
+ menu_item.show()
+
+ if journal_entries:
+ separator = gtk.SeparatorMenuItem()
+ menu_items.append(separator)
+ separator.show()
+
+ for i in range(0, len(menu_items)):
+ self.menu.insert(menu_items[i], i)
+
+ def __resume_entry_cb(self, menu_item, entry):
+ if entry is not None:
+ self.emit('entry-activate', entry)
+
+class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem):
+ def __init__(self):
+ CanvasIcon.__init__(self, cache=True)
+ self._home_model = shell.get_model()
+ self._home_activity = self._home_model.get_active_activity()
+
+ if self._home_activity is not None:
+ self._update()
+
+ self._home_model.connect('active-activity-changed',
+ self.__active_activity_changed_cb)
+
+ self.connect('button-release-event', self.__button_release_event_cb)
+
+ def __button_release_event_cb(self, icon, event):
+ window = self._home_model.get_active_activity().get_window()
+ window.activate(gtk.get_current_event_time())
+
+ def _update(self):
+ self.props.file_name = self._home_activity.get_icon_path()
+ self.props.xo_color = self._home_activity.get_icon_color()
+ self.props.size = style.STANDARD_ICON_SIZE
+
+ if self.palette is not None:
+ self.palette.destroy()
+ self.palette = None
+
+ def create_palette(self):
+ if self._home_activity.is_journal():
+ palette = JournalPalette(self._home_activity)
+ else:
+ palette = CurrentActivityPalette(self._home_activity)
+ return palette
+
+ def __active_activity_changed_cb(self, home_model, home_activity):
+ self._home_activity = home_activity
+ self._update()
+
+class OwnerIcon(BuddyIcon):
+ __gtype_name__ = 'SugarFavoritesOwnerIcon'
+
+ __gsignals__ = {
+ 'register-activate' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([]))
+ }
+ def __init__(self, size):
+ BuddyIcon.__init__(self, buddy=get_owner_instance(), size=size)
+
+ self._palette_enabled = False
+ self._register_menu = None
+
+ def create_palette(self):
+ if not self._palette_enabled:
+ self._palette_enabled = True
+ return
+
+ presence_service = presenceservice.get_instance()
+ palette = BuddyMenu(self.buddy)
+
+ 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()
+
+ return palette
+
+ def get_toplevel(self):
+ return hippo.get_canvas_for_item(self).get_toplevel()
+
+ def __register_activate_cb(self, menuitem):
+ self.emit('register-activate')
+
+ def remove_register_menu(self):
+ self.palette.menu.remove(self._register_menu)
+
+class FavoritesSetting(object):
+
+ _FAVORITES_KEY = "/desktop/sugar/desktop/favorites_layout"
+
+ def __init__(self):
+ client = gconf.client_get_default()
+ self._layout = client.get_string(self._FAVORITES_KEY)
+ logging.debug('FavoritesSetting layout %r', self._layout)
+
+ self._mode = None
+
+ self.changed = dispatch.Signal()
+
+ def get_layout(self):
+ return self._layout
+
+ def set_layout(self, layout):
+ logging.debug('set_layout %r %r', layout, self._layout)
+ if layout != self._layout:
+ self._layout = layout
+
+ client = gconf.client_get_default()
+ client.set_string(self._FAVORITES_KEY, layout)
+
+ self.changed.send(self)
+
+ layout = property(get_layout, set_layout)
+
+_favorites_settings = None
+
+def get_settings():
+ global _favorites_settings
+ if _favorites_settings is None:
+ _favorites_settings = FavoritesSetting()
+ return _favorites_settings
diff --git a/shell/src/jarabe/desktop/friendview.py b/shell/src/jarabe/desktop/friendview.py
new file mode 100644
index 0000000..c3faef8
--- /dev/null
+++ b/shell/src/jarabe/desktop/friendview.py
@@ -0,0 +1,87 @@
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import hippo
+
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics import style
+from sugar.presence import presenceservice
+
+from 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
+ self.append(self._buddy_icon)
+
+ self._activity_icon = CanvasIcon(size=style.LARGE_ICON_SIZE)
+ self._activity_icon_visible = False
+
+ self._update_activity()
+
+ 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()
+ activity_info = registry.get_bundle(ps_activity.props.type)
+ if activity_info:
+ return activity_info.get_icon()
+ return None
+
+ def _remove_activity_icon(self):
+ if self._activity_icon_visible:
+ self.remove(self._activity_icon)
+ self._activity_icon_visible = False
+
+ def __buddy_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(self._buddy.current_activity)
+ if name:
+ self._activity_icon.props.file_name = name
+ 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_notify_present_cb(self, buddy, pspec):
+ self._update_activity()
+
+ def __buddy_notify_color_cb(self, buddy, pspec):
+ # TODO: shouldn't this change self._buddy_icon instead?
+ self._activity_icon.props.xo_color = buddy.props.color
+
diff --git a/shell/src/jarabe/desktop/grid.py b/shell/src/jarabe/desktop/grid.py
new file mode 100644
index 0000000..f3412c9
--- /dev/null
+++ b/shell/src/jarabe/desktop/grid.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2007 Red Hat, Inc.
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import random
+
+import gobject
+import gtk
+
+from sugar import _sugarext
+
+_PLACE_TRIALS = 20
+_MAX_WEIGHT = 255
+_REFRESH_RATE = 200
+_MAX_COLLISIONS_PER_REFRESH = 20
+
+class Grid(_sugarext.Grid):
+ __gsignals__ = {
+ 'child-changed' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+ def __init__(self, width, height):
+ gobject.GObject.__init__(self)
+
+ self.width = width
+ self.height = height
+ self._children = []
+ self._child_rects = {}
+ self._locked_children = set()
+ self._collisions = []
+ self._collisions_sid = 0
+
+ self.setup(self.width, self.height)
+
+ def add(self, child, width, height, x=None, y=None, locked=False):
+ if x is not None and y is not None:
+ rect = gtk.gdk.Rectangle(x, y, width, height)
+ weight = self.compute_weight(rect)
+ else:
+ trials = _PLACE_TRIALS
+ weight = _MAX_WEIGHT
+ while trials > 0 and weight:
+ x = int(random.random() * (self.width - width))
+ y = int(random.random() * (self.height - height))
+
+ rect = gtk.gdk.Rectangle(x, y, width, height)
+ new_weight = self.compute_weight(rect)
+ if weight > new_weight:
+ weight = new_weight
+
+ trials -= 1
+
+ self._child_rects[child] = rect
+ self._children.append(child)
+ self.add_weight(self._child_rects[child])
+ if locked:
+ self._locked_children.add(child)
+
+ if weight > 0:
+ self._detect_collisions(child)
+
+ def remove(self, child):
+ self._children.remove(child)
+ self.remove_weight(self._child_rects[child])
+ self._locked_children.discard(child)
+ del self._child_rects[child]
+
+ if child in self._collisions:
+ self._collisions.remove(child)
+
+ def move(self, child, x, y, locked=False):
+ self.remove_weight(self._child_rects[child])
+
+ rect = self._child_rects[child]
+ rect.x = x
+ rect.y = y
+
+ weight = self.compute_weight(rect)
+ self.add_weight(self._child_rects[child])
+
+ if locked:
+ self._locked_children.add(child)
+ else:
+ self._locked_children.discard(child)
+
+ if weight > 0:
+ self._detect_collisions(child)
+
+ def _shift_child(self, child, weight):
+ rect = self._child_rects[child]
+
+ new_rects = []
+
+ # Get rects right, left, bottom and top
+ if (rect.x + rect.width < self.width - 1):
+ new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y,
+ rect.width, rect.height))
+
+ if (rect.x - 1 > 0):
+ new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y,
+ rect.width, rect.height))
+
+ if (rect.y + rect.height < self.height - 1):
+ new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y + 1,
+ rect.width, rect.height))
+
+ if (rect.y - 1 > 0):
+ new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y - 1,
+ rect.width, rect.height))
+
+ # Get diagonal rects
+ if rect.x + rect.width < self.width - 1 and \
+ rect.y + rect.height < self.height - 1:
+ new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y + 1,
+ rect.width, rect.height))
+
+ if rect.x - 1 > 0 and rect.y + rect.height < self.height - 1:
+ new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y + 1,
+ rect.width, rect.height))
+
+ if rect.x + rect.width < self.width - 1 and rect.y - 1 > 0:
+ new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y - 1,
+ rect.width, rect.height))
+
+ if rect.x - 1 > 0 and rect.y - 1 > 0:
+ new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y - 1,
+ rect.width, rect.height))
+
+ random.shuffle(new_rects)
+
+ best_rect = None
+ for new_rect in new_rects:
+ new_weight = self.compute_weight(new_rect)
+ if new_weight < weight:
+ best_rect = new_rect
+ weight = new_weight
+
+ if best_rect:
+ self._child_rects[child] = best_rect
+ weight = self._shift_child(child, weight)
+
+ return weight
+
+ def __solve_collisions_cb(self):
+ for i_ in range(_MAX_COLLISIONS_PER_REFRESH):
+ collision = self._collisions.pop(0)
+
+ old_rect = self._child_rects[collision]
+ self.remove_weight(old_rect)
+ weight = self.compute_weight(old_rect)
+ weight = self._shift_child(collision, weight)
+ self.add_weight(self._child_rects[collision])
+
+ # TODO: we shouldn't give up the first time we failed to find a
+ # better position.
+ if old_rect != self._child_rects[collision]:
+ self._detect_collisions(collision)
+ self.emit('child-changed', collision)
+ if weight > 0:
+ self._collisions.append(collision)
+
+ if not self._collisions:
+ self._collisions_sid = 0
+ return False
+
+ return True
+
+ def _detect_collisions(self, child):
+ collision_found = False
+ child_rect = self._child_rects[child]
+ for c in self._children:
+ intersection = child_rect.intersect(self._child_rects[c])
+ if c != child and intersection.width > 0:
+ if c not in self._locked_children and c not in self._collisions:
+ collision_found = True
+ self._collisions.append(c)
+
+ if collision_found:
+ if child not in self._collisions:
+ self._collisions.append(child)
+
+ if self._collisions and not self._collisions_sid:
+ self._collisions_sid = gobject.timeout_add(_REFRESH_RATE,
+ self.__solve_collisions_cb, priority=gobject.PRIORITY_LOW)
+
+ def get_child_rect(self, child):
+ return self._child_rects[child]
diff --git a/shell/src/jarabe/desktop/groupbox.py b/shell/src/jarabe/desktop/groupbox.py
new file mode 100644
index 0000000..89043fe
--- /dev/null
+++ b/shell/src/jarabe/desktop/groupbox.py
@@ -0,0 +1,92 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import gobject
+import hippo
+import gconf
+
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.view.buddymenu import BuddyMenu
+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")
+
+ gobject.GObject.__init__(self)
+
+ self._box = hippo.CanvasBox()
+ self._box.props.background_color = style.COLOR_WHITE.get_int()
+ self.set_root(self._box)
+
+ self._friends = {}
+
+ self._layout = SpreadLayout()
+ self._box.set_layout(self._layout)
+
+ client = gconf.client_get_default()
+ 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
+
+ self._owner_icon.set_palette(BuddyMenu(get_owner_instance()))
+ self._layout.add(self._owner_icon)
+
+ friends_model = friends.get_model()
+
+ for friend in friends_model:
+ self.add_friend(friend)
+
+ friends_model.connect('friend-added', self._friend_added_cb)
+ friends_model.connect('friend-removed', self._friend_removed_cb)
+
+ def add_friend(self, buddy_info):
+ icon = FriendView(buddy_info)
+ self._layout.add(icon)
+
+ self._friends[buddy_info.get_key()] = icon
+
+ def _friend_added_cb(self, data_model, buddy_info):
+ self.add_friend(buddy_info)
+
+ def _friend_removed_cb(self, data_model, key):
+ icon = self._friends[key]
+ self._layout.remove(icon)
+ del self._friends[key]
+ icon.destroy()
+
+ def do_size_allocate(self, allocation):
+ width = allocation.width
+ height = allocation.height
+
+ min_w_, icon_width = self._owner_icon.get_width_request()
+ min_h_, icon_height = self._owner_icon.get_height_request(icon_width)
+ x = (width - icon_width) / 2
+ y = (height - icon_height) / 2
+ self._layout.move(self._owner_icon, x, y)
+
+ hippo.Canvas.do_size_allocate(self, allocation)
diff --git a/shell/src/jarabe/desktop/homebox.py b/shell/src/jarabe/desktop/homebox.py
new file mode 100644
index 0000000..85279ff
--- /dev/null
+++ b/shell/src/jarabe/desktop/homebox.py
@@ -0,0 +1,298 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+import logging
+import os
+
+import gobject
+import gtk
+
+from sugar.graphics import style
+from sugar.graphics import iconentry
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.alert import Alert
+from sugar.graphics.icon import Icon
+
+from 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")
+
+ gobject.GObject.__init__(self)
+
+ self._favorites_view = favoritesview.FavoritesView()
+ self._list_view = ActivitiesList()
+
+ self._toolbar = HomeToolbar()
+ self._toolbar.connect('query-changed', self.__toolbar_query_changed_cb)
+ self._toolbar.connect('view-changed', self.__toolbar_view_changed_cb)
+ self.pack_start(self._toolbar, expand=False)
+ self._toolbar.show()
+
+ self._set_view(_FAVORITES_VIEW)
+
+ def show_software_updates_alert(self):
+ alert = Alert()
+ updater_icon = Icon(icon_name='module-updater',
+ pixel_size = style.STANDARD_ICON_SIZE)
+ alert.props.icon = updater_icon
+ updater_icon.show()
+ alert.props.title = _('Software Update')
+ alert.props.msg = _('Update your activities to ensure'
+ ' compatibility with your new software')
+
+ cancel_icon = Icon(icon_name='dialog-cancel')
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon)
+
+ alert.add_button(gtk.RESPONSE_REJECT, _('Later'))
+
+ erase_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Check now'), erase_icon)
+
+ if self._list_view in self.get_children():
+ self._list_view.add_alert(alert)
+ else:
+ self._favorites_view.add_alert(alert)
+ alert.connect('response', self.__software_update_response_cb)
+
+ def __software_update_response_cb(self, alert, response_id):
+ if self._list_view in self.get_children():
+ self._list_view.remove_alert()
+ else:
+ self._favorites_view.remove_alert()
+
+ if response_id != gtk.RESPONSE_REJECT:
+ update_trigger_file = os.path.expanduser('~/.sugar-update')
+ try:
+ os.unlink(update_trigger_file)
+ except OSError:
+ logging.error('Software-update: Can not remove file %s',
+ update_trigger_file)
+
+ if response_id == gtk.RESPONSE_OK:
+ from jarabe.controlpanel.gui import ControlPanel
+ panel = ControlPanel()
+ panel.set_transient_for(self.get_toplevel())
+ panel.show()
+ panel.show_section_view('updater')
+ panel.set_section_view_auto_close()
+
+ def __toolbar_query_changed_cb(self, toolbar, query):
+ query = query.lower()
+ self._list_view.set_filter(query)
+
+ def __toolbar_view_changed_cb(self, toolbar, view):
+ self._set_view(view)
+
+ def _set_view(self, view):
+ if view == _FAVORITES_VIEW:
+ if self._list_view in self.get_children():
+ self.remove(self._list_view)
+
+ if self._favorites_view not in self.get_children():
+ self.add(self._favorites_view)
+ self._favorites_view.show()
+ elif view == _LIST_VIEW:
+ if self._favorites_view in self.get_children():
+ self.remove(self._favorites_view)
+
+ if self._list_view not in self.get_children():
+ self.add(self._list_view)
+ self._list_view.show()
+ else:
+ raise ValueError('Invalid view: %r' % view)
+
+ _REDRAW_TIMEOUT = 5 * 60 * 1000 # 5 minutes
+
+ def resume(self):
+ pass
+
+ def suspend(self):
+ pass
+
+ def has_activities(self):
+ # TODO: Do we need this?
+ #return self._donut.has_activities()
+ return False
+
+ def focus_search_entry(self):
+ self._toolbar.search_entry.grab_focus()
+
+ 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,
+ ([str])),
+ 'view-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._query = None
+ self._autosearch_timer = None
+
+ self._add_separator()
+
+ tool_item = gtk.ToolItem()
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ self.search_entry = iconentry.IconEntry()
+ self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self.search_entry.add_clear_button()
+ self.search_entry.set_width_chars(25)
+ self.search_entry.connect('activate', self.__entry_activated_cb)
+ self.search_entry.connect('changed', self.__entry_changed_cb)
+ tool_item.add(self.search_entry)
+ self.search_entry.set_sensitive(False)
+ self.search_entry.show()
+
+ self._add_separator(expand=True)
+
+ favorites_button = FavoritesButton()
+ favorites_button.connect('toggled', self.__view_button_toggled_cb,
+ _FAVORITES_VIEW)
+ self.insert(favorites_button, -1)
+ favorites_button.show()
+
+ self._list_button = RadioToolButton(named_icon='view-list')
+ self._list_button.props.group = favorites_button
+ self._list_button.props.tooltip = _('List view')
+ self._list_button.props.accelerator = _('<Ctrl>2')
+ self._list_button.connect('toggled', self.__view_button_toggled_cb,
+ _LIST_VIEW)
+ self.insert(self._list_button, -1)
+ self._list_button.show()
+
+ self._add_separator()
+
+ def __view_button_toggled_cb(self, button, view):
+ if button.props.active:
+ if view == _FAVORITES_VIEW:
+ self.search_entry.set_text('')
+ self.search_entry.set_sensitive(False)
+ self.emit('view-changed', view)
+ else:
+ self.search_entry.set_sensitive(True)
+ self.search_entry.grab_focus()
+ self.emit('view-changed', view)
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.GRID_CELL_SIZE,
+ style.GRID_CELL_SIZE)
+ self.insert(separator, -1)
+ separator.show()
+
+ def __entry_activated_cb(self, entry):
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ new_query = entry.props.text
+ if self._query != new_query:
+ self._query = new_query
+
+ if self._query is not '':
+ self._list_button.props.active = True
+ self.emit('query-changed', self._query)
+
+ def __entry_changed_cb(self, entry):
+ if not entry.props.text:
+ entry.activate()
+ return
+
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT,
+ self.__autosearch_timer_cb)
+
+ def __autosearch_timer_cb(self):
+ self._autosearch_timer = None
+ self.search_entry.activate()
+ return False
+
+class FavoritesButton(RadioToolButton):
+ __gtype_name__ = 'SugarFavoritesButton'
+
+ def __init__(self):
+ RadioToolButton.__init__(self)
+
+ self.props.tooltip = _('Favorites view')
+ self.props.accelerator = _('<Ctrl>1')
+ self.props.group = None
+
+ favorites_settings = favoritesview.get_settings()
+ self._layout = favorites_settings.layout
+ self._update_icon()
+
+ # someday, this will be a gtk.Table()
+ layouts_grid = gtk.HBox()
+ layout_item = None
+ for layoutid, layoutclass in sorted(favoritesview.LAYOUT_MAP.items()):
+ layout_item = RadioToolButton(icon_name=layoutclass.icon_name,
+ group=layout_item, active=False)
+ if layoutid == self._layout:
+ layout_item.set_active(True)
+ layouts_grid.pack_start(layout_item, fill=False)
+ layout_item.connect('toggled', self.__layout_activate_cb,
+ layoutid)
+ layouts_grid.show_all()
+ self.props.palette.set_content(layouts_grid)
+
+ def __layout_activate_cb(self, menu_item, layout):
+ if not menu_item.get_active():
+ return
+ if self._layout == layout and self.props.active:
+ return
+
+ if self._layout != layout:
+ self._layout = layout
+ self._update_icon()
+
+ favorites_settings = favoritesview.get_settings()
+ favorites_settings.layout = layout
+
+ if not self.props.active:
+ self.props.active = True
+ else:
+ self.emit('toggled')
+
+ def _update_icon(self):
+ self.props.named_icon = favoritesview.LAYOUT_MAP[self._layout]\
+ .icon_name
+
diff --git a/shell/src/jarabe/desktop/homewindow.py b/shell/src/jarabe/desktop/homewindow.py
new file mode 100644
index 0000000..fec4289
--- /dev/null
+++ b/shell/src/jarabe/desktop/homewindow.py
@@ -0,0 +1,193 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import gtk
+
+from sugar.graphics import style
+from sugar.graphics import palettegroup
+
+from jarabe.desktop.meshbox import MeshBox
+from jarabe.desktop.homebox import HomeBox
+from jarabe.desktop.groupbox import GroupBox
+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
+_TRANSITION_PAGE = 3
+
+class HomeWindow(gtk.Window):
+ def __init__(self):
+ logging.debug('STARTUP: Loading the desktop window')
+ gtk.Window.__init__(self)
+
+ accel_group = gtk.AccelGroup()
+ self.set_data('sugar-accel-group', accel_group)
+ self.add_accel_group(accel_group)
+
+ self._active = False
+ self._fully_obscured = True
+
+ 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)
+
+ self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self.connect('visibility-notify-event',
+ self._visibility_notify_event_cb)
+ self.connect('map-event', self.__map_event_cb)
+ self.connect('key-press-event', self.__key_press_event_cb)
+ self.connect('key-release-event', self.__key_release_event_cb)
+
+ self._home_box = HomeBox()
+ self._group_box = GroupBox()
+ self._mesh_box = MeshBox()
+ self._transition_box = TransitionBox()
+
+ self.add(self._home_box)
+ self._home_box.show()
+
+ self._transition_box.connect('completed',
+ self._transition_completed_cb)
+
+ shell.get_model().zoom_level_changed.connect(
+ self.__zoom_level_changed_cb)
+
+ def _deactivate_view(self, level):
+ 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()
+ elif level == ShellModel.ZOOM_MESH:
+ self._mesh_box.resume()
+
+ def _visibility_notify_event_cb(self, window, event):
+ fully_obscured = (event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED)
+ if self._fully_obscured == fully_obscured:
+ return
+ self._fully_obscured = fully_obscured
+
+ if fully_obscured:
+ self._deactivate_view(shell.get_model().zoom_level)
+ else:
+ display = gtk.gdk.display_get_default()
+ screen_, x_, y_, modmask = display.get_pointer()
+ if modmask & gtk.gdk.MOD1_MASK:
+ self._home_box.set_resume_mode(False)
+ else:
+ self._home_box.set_resume_mode(True)
+
+ self._activate_view(shell.get_model().zoom_level)
+
+ def __key_press_event_cb(self, window, event):
+ if event.keyval in [gtk.keysyms.Alt_L, gtk.keysyms.Alt_R]:
+ self._home_box.set_resume_mode(False)
+ return False
+
+ def __key_release_event_cb(self, window, event):
+ if event.keyval in [gtk.keysyms.Alt_L, gtk.keysyms.Alt_R]:
+ self._home_box.set_resume_mode(True)
+ return False
+
+ def __map_event_cb(self, window, event):
+ # have to make the desktop window active
+ # since metacity doesn't make it on startup
+ timestamp = event.get_time()
+ if not timestamp:
+ timestamp = gtk.gdk.x11_get_server_time(self.window)
+ self.window.focus(timestamp)
+
+ def __zoom_level_changed_cb(self, **kwargs):
+ old_level = kwargs['old_level']
+ new_level = kwargs['new_level']
+
+ self._deactivate_view(old_level)
+ self._activate_view(new_level)
+
+ if old_level != ShellModel.ZOOM_ACTIVITY and \
+ new_level != ShellModel.ZOOM_ACTIVITY:
+ self.remove(self.get_child())
+ self.add(self._transition_box)
+ self._transition_box.show()
+
+ if new_level == ShellModel.ZOOM_HOME:
+ end_size = style.XLARGE_ICON_SIZE
+ elif new_level == ShellModel.ZOOM_GROUP:
+ end_size = style.LARGE_ICON_SIZE
+ elif new_level == ShellModel.ZOOM_MESH:
+ end_size = style.STANDARD_ICON_SIZE
+
+ if old_level == ShellModel.ZOOM_HOME:
+ start_size = style.XLARGE_ICON_SIZE
+ elif old_level == ShellModel.ZOOM_GROUP:
+ start_size = style.LARGE_ICON_SIZE
+ elif old_level == ShellModel.ZOOM_MESH:
+ start_size = style.STANDARD_ICON_SIZE
+
+ self._transition_box.start_transition(start_size, end_size)
+ else:
+ self._update_view(new_level)
+
+ def _transition_completed_cb(self, transition_box):
+ self._update_view(shell.get_model().zoom_level)
+
+ def _update_view(self, level):
+ if level == ShellModel.ZOOM_ACTIVITY:
+ return
+
+ current_child = self.get_child()
+ self.remove(current_child)
+
+ if level == ShellModel.ZOOM_HOME:
+ self.add(self._home_box)
+ self._home_box.show()
+ self._home_box.focus_search_entry()
+ elif level == ShellModel.ZOOM_GROUP:
+ self.add(self._group_box)
+ self._group_box.show()
+ elif level == ShellModel.ZOOM_MESH:
+ self.add(self._mesh_box)
+ self._mesh_box.show()
+ self._mesh_box.focus_search_entry()
+
+ def get_home_box(self):
+ return self._home_box
+
+_instance = None
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = HomeWindow()
+ return _instance
+
diff --git a/shell/src/jarabe/desktop/keydialog.py b/shell/src/jarabe/desktop/keydialog.py
new file mode 100644
index 0000000..1e6d17a
--- /dev/null
+++ b/shell/src/jarabe/desktop/keydialog.py
@@ -0,0 +1,317 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2009 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 hashlib
+from gettext import gettext as _
+
+import gtk
+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'
+
+def string_is_hex(key):
+ is_hex = True
+ for c in key:
+ if not 'a' <= c.lower() <= 'f' and not '0' <= c <= '9':
+ is_hex = False
+ return is_hex
+
+def string_is_ascii(string):
+ try:
+ string.encode('ascii')
+ return True
+ except UnicodeEncodeError:
+ return False
+
+def string_to_hex(passphrase):
+ key = ''
+ for c in passphrase:
+ key += '%02x' % ord(c)
+ return key
+
+def hash_passphrase(passphrase):
+ # passphrase must have a length of 64
+ if len(passphrase) > 64:
+ passphrase = passphrase[:64]
+ elif len(passphrase) < 64:
+ while len(passphrase) < 64:
+ passphrase += passphrase[:64 - len(passphrase)]
+ passphrase = 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._settings = settings
+ self._response = response
+ self._entry = None
+ self._ssid = ssid
+ self._flags = flags
+ self._wpa_flags = wpa_flags
+ self._rsn_flags = rsn_flags
+ self._dev_caps = dev_caps
+
+ self.set_has_separator(False)
+
+ label = gtk.Label("A wireless encryption key is required for\n" \
+ " the wireless network '%s'." % self._ssid)
+ self.vbox.pack_start(label)
+
+ self.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+ gtk.STOCK_OK, gtk.RESPONSE_OK)
+ self.set_default_response(gtk.RESPONSE_OK)
+ self.set_has_separator(True)
+
+ def add_key_entry(self):
+ self._entry = gtk.Entry()
+ self._entry.connect('changed', self._update_response_sensitivity)
+ self._entry.connect('activate', self._entry_activate_cb)
+ self.vbox.pack_start(self._entry)
+ self.vbox.set_spacing(6)
+ self.vbox.show_all()
+
+ self._update_response_sensitivity()
+ self._entry.grab_focus()
+
+ def _entry_activate_cb(self, entry):
+ self.response(gtk.RESPONSE_OK)
+
+ def create_security(self):
+ raise NotImplementedError
+
+ def get_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,
+ response):
+ KeyDialog.__init__(self, ssid, flags, wpa_flags, rsn_flags,
+ dev_caps, settings, response)
+
+ # WEP key type
+ self.key_store = gtk.ListStore(str, int)
+ self.key_store.append(["Passphrase (128-bit)", WEP_PASSPHRASE])
+ self.key_store.append(["Hex (40/128-bit)", WEP_HEX])
+ self.key_store.append(["ASCII (40/128-bit)", WEP_ASCII])
+
+ self.key_combo = gtk.ComboBox(self.key_store)
+ cell = gtk.CellRendererText()
+ self.key_combo.pack_start(cell, True)
+ self.key_combo.add_attribute(cell, 'text', 0)
+ self.key_combo.set_active(0)
+ self.key_combo.connect('changed', self._key_combo_changed_cb)
+
+ hbox = gtk.HBox()
+ hbox.pack_start(gtk.Label(_("Key Type:")))
+ hbox.pack_start(self.key_combo)
+ hbox.show_all()
+ self.vbox.pack_start(hbox)
+
+ # Key entry field
+ self.add_key_entry()
+
+ # WEP authentication mode
+ self.auth_store = gtk.ListStore(str, 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_combo = gtk.ComboBox(self.auth_store)
+ cell = gtk.CellRendererText()
+ self.auth_combo.pack_start(cell, True)
+ self.auth_combo.add_attribute(cell, 'text', 0)
+ self.auth_combo.set_active(0)
+
+ hbox = gtk.HBox()
+ hbox.pack_start(gtk.Label(_("Authentication Type:")))
+ hbox.pack_start(self.auth_combo)
+ hbox.show_all()
+
+ self.vbox.pack_start(hbox)
+
+ def _key_combo_changed_cb(self, widget):
+ self._update_response_sensitivity()
+
+ def _get_security(self):
+ key = self._entry.get_text()
+
+ it = self.key_combo.get_active_iter()
+ (key_type, ) = self.key_store.get(it, 1)
+
+ if key_type == WEP_PASSPHRASE:
+ key = hash_passphrase(key)
+ elif key_type == WEP_ASCII:
+ key = string_to_hex(key)
+
+ it = self.auth_combo.get_active_iter()
+ (auth_alg, ) = self.auth_store.get(it, 1)
+
+ return (key, auth_alg)
+
+ def print_security(self):
+ (key, auth_alg) = self._get_security()
+ print "Key: %s" % key
+ print "Auth: %d" % auth_alg
+
+ def create_security(self):
+ (key, auth_alg) = self._get_security()
+ secrets = Secrets(self._settings)
+ secrets.wep_key = key
+ secrets.auth_alg = auth_alg
+ return secrets
+
+ def _update_response_sensitivity(self, ignored=None):
+ key = self._entry.get_text()
+ it = self.key_combo.get_active_iter()
+ (key_type, ) = self.key_store.get(it, 1)
+
+ valid = False
+ if key_type == WEP_PASSPHRASE:
+ # As the md5 passphrase can be of any length and has no indicator,
+ # we cannot check for the validity of the input.
+ if len(key) > 0:
+ valid = True
+ elif key_type == WEP_ASCII:
+ if len(key) == 5 or len(key) == 13:
+ valid = string_is_ascii(key)
+ elif key_type == WEP_HEX:
+ if len(key) == 10 or len(key) == 26:
+ valid = string_is_hex(key)
+
+ self.set_response_sensitive(gtk.RESPONSE_OK, valid)
+
+class WPAKeyDialog(KeyDialog):
+ def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, settings,
+ response):
+ KeyDialog.__init__(self, ssid, flags, wpa_flags, rsn_flags,
+ dev_caps, settings, response)
+ self.add_key_entry()
+
+ self.store = gtk.ListStore(str)
+ self.store.append([_("WPA & WPA2 Personal")])
+
+ self.combo = gtk.ComboBox(self.store)
+ cell = gtk.CellRendererText()
+ self.combo.pack_start(cell, True)
+ self.combo.add_attribute(cell, 'text', 0)
+ self.combo.set_active(0)
+
+ self.hbox = gtk.HBox()
+ self.hbox.pack_start(gtk.Label(_("Wireless Security:")))
+ self.hbox.pack_start(self.combo)
+ self.hbox.show_all()
+
+ self.vbox.pack_start(self.hbox)
+
+ def _get_security(self):
+ ssid = self._ssid
+ key = self._entry.get_text()
+ is_hex = string_is_hex(key)
+
+ real_key = None
+ if len(key) == 64 and is_hex:
+ # Hex key
+ real_key = key
+ elif len(key) >= 8 and len(key) <= 63:
+ # passphrase
+ from subprocess import Popen, PIPE
+ p = Popen(['wpa_passphrase', ssid, key], stdout=PIPE)
+ for line in p.stdout:
+ if line.strip().startswith("psk="):
+ real_key = line.strip()[4:]
+ if p.wait() != 0:
+ raise RuntimeError("Error hashing passphrase")
+ if real_key and len(real_key) != 64:
+ real_key = None
+
+ if not real_key:
+ raise RuntimeError("Invalid key")
+
+ return real_key
+
+ def print_security(self):
+ key = self._get_security()
+ print "Key: %s" % key
+
+ def create_security(self):
+ secrets = Secrets(self._settings)
+ secrets.psk = self._get_security()
+ return secrets
+
+ def _update_response_sensitivity(self, ignored=None):
+ key = self._entry.get_text()
+ is_hex = string_is_hex(key)
+
+ valid = False
+ if len(key) == 64 and is_hex:
+ # hex key
+ valid = True
+ elif len(key) >= 8 and len(key) <= 63:
+ # passphrase
+ valid = True
+ self.set_response_sensitive(gtk.RESPONSE_OK, valid)
+ return False
+
+def 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:
+ key_dialog = WEPKeyDialog(ssid, flags, wpa_flags, rsn_flags,
+ dev_caps, settings, response)
+ else:
+ 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.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
+ if response_id == gtk.RESPONSE_OK:
+ secrets = key_dialog.create_security()
+
+ if response_id in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_NONE]:
+ # key dialog dialog was canceled; send the error back to NM
+ response.set_error(CanceledKeyRequestError())
+ elif response_id == gtk.RESPONSE_OK:
+ if not secrets:
+ raise RuntimeError("Invalid security arguments.")
+ response.set_secrets(secrets)
+ else:
+ raise RuntimeError("Unhandled key dialog response %d" % response_id)
+
+ key_dialog.destroy()
+
diff --git a/shell/src/jarabe/desktop/meshbox.py b/shell/src/jarabe/desktop/meshbox.py
new file mode 100644
index 0000000..cf72053
--- /dev/null
+++ b/shell/src/jarabe/desktop/meshbox.py
@@ -0,0 +1,670 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer
+# 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
+# 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 dbus
+import hippo
+import glib
+import gobject
+import gtk
+import gconf
+
+from sugar.graphics.icon import CanvasIcon, Icon
+from sugar.graphics import style
+from sugar.graphics import palette
+from sugar.graphics import iconentry
+from sugar.graphics.menuitem import MenuItem
+
+from jarabe.model import neighborhood
+from jarabe.model.buddy import get_owner_instance
+from jarabe.view.buddyicon import BuddyIcon
+from jarabe.desktop.snowflakelayout import SnowflakeLayout
+from jarabe.desktop.spreadlayout import SpreadLayout
+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.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'
+_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 ActivityView(hippo.CanvasBox):
+ def __init__(self, model):
+ 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
+
+ self._layout = SnowflakeLayout()
+ self.set_layout(self._layout)
+
+ self._icon = self._create_icon()
+ self._layout.add(self._icon, center=True)
+
+ self._palette = self._create_palette()
+ self._icon.set_palette(self._palette)
+
+ for buddy in self._model.props.current_buddies:
+ self._add_buddy(buddy)
+
+ def _create_icon(self):
+ 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_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=p_text,
+ icon=p_icon)
+
+ private = self._model.props.private
+ joined = get_owner_instance() in self._model.props.buddies
+
+ if joined:
+ item = MenuItem(_('Resume'), 'activity-start')
+ item.connect('activate', self._clicked_cb)
+ item.show()
+ p.menu.append(item)
+ elif not private:
+ item = MenuItem(_('Join'), 'activity-start')
+ item.connect('activate', self._clicked_cb)
+ item.show()
+ p.menu.append(item)
+
+ return p
+
+ def has_buddy_icon(self, key):
+ return self._icons.has_key(key)
+
+ def __buddy_added_cb(self, activity, buddy):
+ self._add_buddy(buddy)
+
+ def _add_buddy(self, buddy):
+ icon = BuddyIcon(buddy, style.STANDARD_ICON_SIZE)
+ self._icons[buddy.props.key] = icon
+ self._layout.add(icon)
+
+ 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):
+ 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.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()
+ else:
+ self._icon.props.xo_color = self._model.get_color()
+
+ for icon in self._icons.itervalues():
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(query)
+
+_AUTOSEARCH_TIMEOUT = 1000
+
+
+class MeshToolbar(gtk.Toolbar):
+ __gtype_name__ = 'MeshToolbar'
+
+ __gsignals__ = {
+ 'query-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._query = None
+ self._autosearch_timer = None
+
+ self._add_separator()
+
+ tool_item = gtk.ToolItem()
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ self.search_entry = iconentry.IconEntry()
+ self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self.search_entry.add_clear_button()
+ self.search_entry.set_width_chars(25)
+ self.search_entry.connect('activate', self._entry_activated_cb)
+ self.search_entry.connect('changed', self._entry_changed_cb)
+ tool_item.add(self.search_entry)
+ self.search_entry.show()
+
+ self._add_separator(expand=True)
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.GRID_CELL_SIZE,
+ style.GRID_CELL_SIZE)
+ self.insert(separator, -1)
+ separator.show()
+
+ def _entry_activated_cb(self, entry):
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ new_query = entry.props.text
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def _entry_changed_cb(self, entry):
+ if not entry.props.text:
+ entry.activate()
+ return
+
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT,
+ self._autosearch_timer_cb)
+
+ def _autosearch_timer_cb(self):
+ logging.debug('_autosearch_timer_cb')
+ self._autosearch_timer = None
+ self.search_entry.activate()
+ return False
+
+
+class 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
+
+ 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',
+ path=device.object_path,
+ dbus_interface=_NM_WIRELESS_IFACE)
+ self._bus.add_signal_receiver(self.__access_point_removed_cb,
+ signal_name='AccessPointRemoved',
+ path=device.object_path,
+ dbus_interface=_NM_WIRELESS_IFACE)
+
+ 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.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.emit('access-point-added', ap)
+
+ def __access_point_removed_cb(self, 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,
+ dbus_interface=_NM_WIRELESS_IFACE)
+ self._bus.remove_signal_receiver(self.__access_point_removed_cb,
+ signal_name='AccessPointRemoved',
+ 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:
+ self._bus = dbus.SystemBus()
+ obj = self._bus.get_object(_NM_SERVICE, _NM_PATH)
+ self._netmgr = dbus.Interface(obj, _NM_IFACE)
+ except dbus.DBusException:
+ logging.debug('%s service not available', _NM_SERVICE)
+ return
+
+ self._netmgr.GetDevices(reply_handler=self.__get_devices_reply_cb,
+ error_handler=self.__get_devices_error_cb)
+
+ self._bus.add_signal_receiver(self.__device_added_cb,
+ signal_name='DeviceAdded',
+ dbus_interface=_NM_IFACE)
+ 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:
+ settings.secrets_request.connect(self.__secrets_request_cb)
+
+ def __secrets_request_cb(self, **kwargs):
+ # 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')
+ 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')
+ 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')
+ found = False
+ if ap_o != '/':
+ for net in self._box.wireless_networks.values():
+ if net.find_ap(ap_o) is not None:
+ found = True
+ 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)
+
+ def __get_devices_reply_cb(self, devices_o):
+ for dev_o in devices_o:
+ self._check_device(dev_o)
+
+ def __get_devices_error_cb(self, err):
+ logging.error('Failed to get devices: %s', err)
+
+ def _check_device(self, device_o):
+ 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_WIRELESS:
+ 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):
+ logging.error('Failed to get device type: %s', err)
+
+ def __device_added_cb(self, device_o):
+ self._check_device(device_o)
+
+ def __device_removed_cb(self, device_o):
+ if device_o in self._devices:
+ observer = self._devices[device_o]
+ observer.disconnect()
+ del self._devices[device_o]
+ if self._have_adhoc_networks:
+ self._box.remove_adhoc_networks()
+ return
+
+ 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")
+
+ gobject.GObject.__init__(self)
+
+ self.wireless_networks = {}
+ self._adhoc_manager = None
+ self._adhoc_networks = []
+
+ self._model = neighborhood.get_model()
+ self._buddies = {}
+ self._activities = {}
+ self._mesh = []
+ self._buddy_to_activity = {}
+ self._suspended = True
+ self._query = ''
+ self._owner_icon = None
+
+ self._toolbar = MeshToolbar()
+ self._toolbar.connect('query-changed', self._toolbar_query_changed_cb)
+ self.pack_start(self._toolbar, expand=False)
+ self._toolbar.show()
+
+ canvas = hippo.Canvas()
+ self.add(canvas)
+ canvas.show()
+
+ self._layout_box = hippo.CanvasBox( \
+ background_color=style.COLOR_WHITE.get_int())
+ canvas.set_root(self._layout_box)
+
+ self._layout = SpreadLayout()
+ self._layout_box.set_layout(self._layout)
+
+ for buddy_model in self._model.get_buddies():
+ self._add_buddy(buddy_model)
+
+ self._model.connect('buddy-added', self._buddy_added_cb)
+ self._model.connect('buddy-removed', self._buddy_removed_cb)
+
+ for activity_model in self._model.get_activities():
+ self._add_activity(activity_model)
+
+ self._model.connect('activity-added', self._activity_added_cb)
+ self._model.connect('activity-removed', self._activity_removed_cb)
+
+ netmgr_observer = NetworkManagerObserver(self)
+ netmgr_observer.listen()
+
+ def do_size_allocate(self, allocation):
+ width = allocation.width
+ height = allocation.height
+
+ min_w_, icon_width = self._owner_icon.get_width_request()
+ min_h_, icon_height = self._owner_icon.get_height_request(icon_width)
+ x = (width - icon_width) / 2
+ y = (height - icon_height) / 2 - style.GRID_CELL_SIZE
+ self._layout.move(self._owner_icon, x, y)
+
+ gtk.VBox.do_size_allocate(self, allocation)
+
+ def _buddy_added_cb(self, model, buddy_model):
+ self._add_buddy(buddy_model)
+
+ def _buddy_removed_cb(self, model, buddy_model):
+ self._remove_buddy(buddy_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_buddy(self, buddy_model):
+ logging.debug('MeshBox._add_buddy %r', buddy_model.props.key)
+ 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
+ self._layout.add(icon)
+
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ self._buddies[buddy_model.props.key] = icon
+
+ 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.props.key]
+ icon.destroy()
+
+ 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)
+ self._layout.add(icon)
+
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ self._activities[activity_model.activity_id] = icon
+
+ def _remove_activity(self, activity_model):
+ icon = self._activities[activity_model.activity_id]
+ self._layout.remove(icon)
+ 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)
+ else:
+ # this is a new network
+ icon = WirelessNetworkView(ap)
+ self.wireless_networks[hash] = icon
+ self._layout.add(icon)
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ def _remove_net_if_empty(self, net, hash):
+ # 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]
+
+ def _ap_props_changed_cb(self, ap, old_hash):
+ # 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")
+ ap.disconnect()
+ return
+
+ 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 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 is None: # new AP finished initializing
+ self._add_ap_to_network(ap)
+ return
+
+ hash = ap.network_hash()
+ if old_hash == hash:
+ # no change in network identity, so just update signal strengths
+ self.wireless_networks[hash].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._add_ap_to_network(ap)
+
+ def add_access_point(self, device, ap_o):
+ ap = AccessPoint(device, ap_o)
+ ap.connect('props-changed', self._ap_props_changed_cb)
+ 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():
+ ap = net.find_ap(ap_o)
+ if not ap:
+ continue
+
+ ap.disconnect()
+ net.remove_ap(ap)
+ self._remove_net_if_empty(net, ap.network_hash())
+ return
+
+ # 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)
+
+ 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)
+ self._layout.add(icon)
+ self._mesh.append(icon)
+
+ def enable_olpc_mesh(self, mesh_device):
+ mesh_mgr = OlpcMeshManager(mesh_device)
+ self._add_olpc_mesh_icon(mesh_mgr, 1)
+ self._add_olpc_mesh_icon(mesh_mgr, 6)
+ self._add_olpc_mesh_icon(mesh_mgr, 11)
+
+ # 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():
+ if not net.is_olpc_mesh():
+ continue
+
+ logging.debug("removing OLPC mesh IBSS")
+ net.remove_all_aps()
+ net.disconnect()
+ self._layout.remove(net)
+ del self.wireless_networks[hash]
+
+ def disable_olpc_mesh(self, mesh_device):
+ for icon in self._mesh:
+ icon.disconnect()
+ self._layout.remove(icon)
+ self._mesh = []
+
+ def suspend(self):
+ if not self._suspended:
+ self._suspended = True
+ for net in self.wireless_networks.values() + self._mesh:
+ net.props.paused = True
+
+ def resume(self):
+ if self._suspended:
+ self._suspended = False
+ for net in self.wireless_networks.values() + self._mesh:
+ net.props.paused = False
+
+ def _toolbar_query_changed_cb(self, toolbar, query):
+ self._query = query.lower()
+ for icon in self._layout_box.get_children():
+ if hasattr(icon, 'set_filter'):
+ icon.set_filter(self._query)
+
+ def focus_search_entry(self):
+ self._toolbar.search_entry.grab_focus()
diff --git a/shell/src/jarabe/desktop/networkviews.py b/shell/src/jarabe/desktop/networkviews.py
new file mode 100644
index 0000000..121c817
--- /dev/null
+++ b/shell/src/jarabe/desktop/networkviews.py
@@ -0,0 +1,716 @@
+# 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._connection = 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
+
+ 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
+
+ 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 _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()
+
+ 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 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 state == network.DEVICE_STATE_ACTIVATED:
+ 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 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()
+ 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 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)
+
+ self.props.base_color = self._state_color
+ self._palette_icon.props.xo_color = self._state_color
+
+ 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.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()
+ self.props.base_color = self._state_color
+ self._palette_icon.props.xo_color = self._state_color
+ else:
+ color = '%s,%s' % (profile.get_color().get_stroke_color(),
+ style.COLOR_TRANSPARENT.get_svg())
+ self._state_color = XoColor(color)
+ 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._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):
+ 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/shell/src/jarabe/desktop/schoolserver.py b/shell/src/jarabe/desktop/schoolserver.py
new file mode 100644
index 0000000..a05f56c
--- /dev/null
+++ b/shell/src/jarabe/desktop/schoolserver.py
@@ -0,0 +1,127 @@
+# Copyright (C) 2007, 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+from xmlrpclib import ServerProxy, Error
+import socket
+import os
+from string import ascii_uppercase
+import random
+import time
+import uuid
+
+import gconf
+
+from sugar import env
+from sugar.profile import get_profile
+
+REGISTER_URL = 'http://schoolserver:8080/'
+
+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) :
+ serial_part1.append(random.choice(ascii_uppercase))
+
+ serial_part1 = ''.join(serial_part1)
+ serial_part2 = str(int(time.time()))[-8:]
+ serial = serial_part1 + serial_part2
+
+ return serial
+
+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. """
+
+ identifier_path = os.path.join(env.get_profile_path(), 'identifiers')
+ if not os.path.exists(identifier_path):
+ os.mkdir(identifier_path)
+
+ if os.path.exists(os.path.join(identifier_path, 'sn')):
+ os.remove(os.path.join(identifier_path, 'sn'))
+ serial_file = open(os.path.join(identifier_path, 'sn'), 'w')
+ serial_file.write(serial_number)
+ serial_file.close()
+
+ 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.close()
+
+ if os.path.exists(os.path.join(identifier_path, 'backup_url')):
+ os.remove(os.path.join(identifier_path, 'backup_url'))
+ backup_url_file = open(os.path.join(identifier_path, 'backup_url'), 'w')
+ backup_url_file.write(backup_url)
+ backup_url_file.close()
+
+class RegisterError(Exception):
+ pass
+
+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#')
+ sn = sn or 'SHF00000000'
+ uuid_ = uuid_ or '00000000-0000-0000-0000-000000000000'
+ else:
+ 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)
+ url = 'http://' + jabber_server + ':8080/'
+
+ nick = client.get_string('/desktop/sugar/user/nick')
+
+ server = ServerProxy(url)
+ try:
+ data = server.register(sn, nick, uuid_, profile.pubkey)
+ except (Error, TypeError, socket.error):
+ logging.exception('Registration: cannot connect to server')
+ raise RegisterError(_('Cannot connect to the server.'))
+
+ if data['success'] != 'OK':
+ logging.error('Registration: server could not complete request: %s',
+ data['error'])
+ raise RegisterError(_('The server could not complete the request.'))
+
+ client.set_string('/desktop/sugar/collaboration/jabber_server',
+ data['jabberserver'])
+ client.set_string('/desktop/sugar/backup_url', data['backupurl'])
+
+ return True
+
+def have_ofw_tree():
+ return os.path.exists('/ofw')
+
+def read_ofw(path):
+ path = os.path.join('/ofw', path)
+ if not os.path.exists(path):
+ return None
+ fh = open(path, 'r')
+ data = fh.read().rstrip('\0\n')
+ fh.close()
+ return data
diff --git a/shell/src/jarabe/desktop/snowflakelayout.py b/shell/src/jarabe/desktop/snowflakelayout.py
new file mode 100644
index 0000000..5782cff
--- /dev/null
+++ b/shell/src/jarabe/desktop/snowflakelayout.py
@@ -0,0 +1,108 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import math
+
+import gobject
+import hippo
+
+from sugar.graphics import style
+
+_BASE_DISTANCE = style.zoom(25)
+_CHILDREN_FACTOR = style.zoom(3)
+
+class SnowflakeLayout(gobject.GObject, hippo.CanvasLayout):
+ __gtype_name__ = 'SugarSnowflakeLayout'
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self._nflakes = 0
+ self._box = None
+
+ def add(self, child, center=False):
+ if not center:
+ self._nflakes += 1
+
+ self._box.append(child)
+
+ box_child = self._box.find_box_child(child)
+ box_child.is_center = center
+
+ def remove(self, child):
+ box_child = self._box.find_box_child(child)
+ if not box_child.is_center:
+ self._nflakes -= 1
+
+ self._box.remove(child)
+
+ def do_set_box(self, box):
+ self._box = box
+
+ def do_get_height_request(self, for_width):
+ size = self._calculate_size()
+ return (size, size)
+
+ def do_get_width_request(self):
+ size = self._calculate_size()
+ return (size, size)
+
+ def do_allocate(self, x, y, width, height,
+ req_width, req_height, origin_changed):
+ r = self._get_radius()
+ index = 0
+
+ for child in self._box.get_layout_children():
+ min_width, child_width = child.get_width_request()
+ min_height, child_height = child.get_height_request(child_width)
+
+ if child.is_center:
+ child.allocate(x + (width - child_width) / 2,
+ y + (height - child_height) / 2,
+ child_width, child_height, origin_changed)
+ else:
+ angle = 2 * math.pi * index / self._nflakes
+
+ if self._nflakes != 2:
+ angle -= math.pi / 2
+
+ dx = math.cos(angle) * r
+ dy = math.sin(angle) * r
+
+ child_x = int(x + (width - child_width) / 2 + dx)
+ child_y = int(y + (height - child_height) / 2 + dy)
+
+ child.allocate(child_x, child_y, child_width,
+ child_height, origin_changed)
+
+ index += 1
+
+ def _get_radius(self):
+ radius = int(_BASE_DISTANCE + _CHILDREN_FACTOR * self._nflakes)
+ for child in self._box.get_layout_children():
+ if child.is_center:
+ [min_w, child_w] = child.get_width_request()
+ [min_h, child_h] = child.get_height_request(child_w)
+ radius += max(child_w, child_h) / 2
+
+ return radius
+
+ def _calculate_size(self):
+ thickness = 0
+ for child in self._box.get_layout_children():
+ [min_width, child_width] = child.get_width_request()
+ [min_height, child_height] = child.get_height_request(child_width)
+ thickness = max(thickness, max(child_width, child_height))
+
+ return self._get_radius() * 2 + thickness
diff --git a/shell/src/jarabe/desktop/spreadlayout.py b/shell/src/jarabe/desktop/spreadlayout.py
new file mode 100644
index 0000000..ffc5bc7
--- /dev/null
+++ b/shell/src/jarabe/desktop/spreadlayout.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import hippo
+import gobject
+import gtk
+
+from sugar.graphics import style
+
+from jarabe.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
+
+ min_width, width = self.do_get_width_request()
+ min_height, height = self.do_get_height_request(width)
+
+ self._grid = Grid(int(width / _CELL_SIZE), int(height / _CELL_SIZE))
+ self._grid.connect('child-changed', self._grid_child_changed_cb)
+
+ def add(self, child):
+ self._box.append(child)
+
+ width, height = self._get_child_grid_size(child)
+ self._grid.add(child, width, height)
+
+ def remove(self, child):
+ self._grid.remove(child)
+ self._box.remove(child)
+
+ def move(self, child, x, y):
+ self._grid.move(child, x / _CELL_SIZE, y / _CELL_SIZE, locked=True)
+
+ def do_set_box(self, box):
+ self._box = box
+
+ def do_get_height_request(self, for_width):
+ return 0, gtk.gdk.screen_height() - style.GRID_CELL_SIZE
+
+ def do_get_width_request(self):
+ return 0, gtk.gdk.screen_width()
+
+ def do_allocate(self, x, y, width, height,
+ req_width, req_height, origin_changed):
+ for child in self._box.get_layout_children():
+ # We need to always get requests to not confuse hippo
+ min_w, child_width = child.get_width_request()
+ min_h, child_height = child.get_height_request(child_width)
+
+ rect = self._grid.get_child_rect(child.item)
+ child.allocate(int(round(rect.x * _CELL_SIZE)),
+ int(round(rect.y * _CELL_SIZE)),
+ child_width,
+ child_height,
+ origin_changed)
+
+ def _get_child_grid_size(self, child):
+ min_width, width = child.get_width_request()
+ min_height, height = child.get_height_request(width)
+
+ return int(width / _CELL_SIZE), int(height / _CELL_SIZE)
+
+ def _grid_child_changed_cb(self, grid, child):
+ child.emit_request_changed()
+
diff --git a/shell/src/jarabe/desktop/transitionbox.py b/shell/src/jarabe/desktop/transitionbox.py
new file mode 100644
index 0000000..cf9e0d6
--- /dev/null
+++ b/shell/src/jarabe/desktop/transitionbox.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import hippo
+import gobject
+
+from sugar.graphics import style
+from sugar.graphics import animator
+
+from jarabe.model.buddy import get_owner_instance
+from jarabe.view.buddyicon import BuddyIcon
+
+class _Animation(animator.Animation):
+ def __init__(self, icon, start_size, end_size):
+ animator.Animation.__init__(self, 0.0, 1.0)
+
+ self._icon = icon
+ self.start_size = start_size
+ self.end_size = end_size
+
+ def next_frame(self, current):
+ d = (self.end_size - self.start_size) * current
+ self._icon.props.size = self.start_size + d
+
+class _Layout(gobject.GObject, hippo.CanvasLayout):
+ __gtype_name__ = 'SugarTransitionBoxLayout'
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self._box = None
+
+ def do_set_box(self, box):
+ self._box = box
+
+ def do_get_height_request(self, for_width):
+ return 0, 0
+
+ def do_get_width_request(self):
+ return 0, 0
+
+ def do_allocate(self, x, y, width, height,
+ req_width, req_height, origin_changed):
+ for child in self._box.get_layout_children():
+ min_width, child_width = child.get_width_request()
+ min_height, child_height = child.get_height_request(child_width)
+
+ child.allocate(x + (width - child_width) / 2,
+ y + (height - child_height) / 2,
+ child_width, child_height, origin_changed)
+
+class TransitionBox(hippo.Canvas):
+ __gtype_name__ = 'SugarTransitionBox'
+
+ __gsignals__ = {
+ 'completed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._box = hippo.CanvasBox()
+ self._box.props.background_color = style.COLOR_WHITE.get_int()
+ self.set_root(self._box)
+
+ self._layout = _Layout()
+ self._box.set_layout(self._layout)
+
+ 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)
+ self._animator.connect('completed', self._animation_completed_cb)
+
+ def _animation_completed_cb(self, anim):
+ self.emit('completed')
+
+ def start_transition(self, start_size, end_size):
+ self._my_icon.props.size = start_size
+
+ self._animator.remove_all()
+ self._animator.add(_Animation(self._my_icon, start_size, end_size))
+ self._animator.start()
diff --git a/shell/src/jarabe/frame/Makefile.am b/shell/src/jarabe/frame/Makefile.am
new file mode 100644
index 0000000..e5c445f
--- /dev/null
+++ b/shell/src/jarabe/frame/Makefile.am
@@ -0,0 +1,18 @@
+sugardir = $(pythondir)/jarabe/frame
+sugar_PYTHON = \
+ __init__.py \
+ activitiestray.py \
+ clipboard.py \
+ clipboardicon.py \
+ clipboardmenu.py \
+ clipboardobject.py \
+ clipboardpanelwindow.py \
+ clipboardtray.py \
+ devicestray.py \
+ frameinvoker.py \
+ friendstray.py \
+ eventarea.py \
+ frame.py \
+ notification.py \
+ framewindow.py \
+ zoomtoolbar.py
diff --git a/shell/src/jarabe/frame/__init__.py b/shell/src/jarabe/frame/__init__.py
new file mode 100644
index 0000000..d7aec3d
--- /dev/null
+++ b/shell/src/jarabe/frame/__init__.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2006-2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from jarabe.frame.frame import Frame
+
+_view = None
+
+def get_view():
+ global _view
+ if not _view:
+ _view = Frame()
+ return _view
diff --git a/shell/src/jarabe/frame/activitiestray.py b/shell/src/jarabe/frame/activitiestray.py
new file mode 100644
index 0000000..9a1a9da
--- /dev/null
+++ b/shell/src/jarabe/frame/activitiestray.py
@@ -0,0 +1,745 @@
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+import tempfile
+import os
+
+import gobject
+import gconf
+import gio
+import gtk
+
+from sugar.graphics import style
+from sugar.graphics.tray import HTray
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.icon import Icon, get_icon_file_name
+from sugar.graphics.palette import Palette, WidgetInvoker
+from sugar.graphics.menuitem import MenuItem
+from sugar.datastore import datastore
+from sugar import mime
+from sugar import env
+
+from jarabe.model import shell
+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.frame.frameinvoker import FrameWidgetInvoker
+from jarabe.frame.notification import NotificationIcon
+import jarabe.frame
+
+
+class ActivityButton(RadioToolButton):
+ def __init__(self, home_activity, group):
+ RadioToolButton.__init__(self, group=group)
+
+ 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()
+ self._icon.props.pulse_color = \
+ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TOOLBAR_GREY.get_svg()))
+ if home_activity.get_icon_path():
+ self._icon.props.file = home_activity.get_icon_path()
+ else:
+ self._icon.props.icon_name = 'image-missing'
+ self.set_icon_widget(self._icon)
+ self._icon.show()
+
+ if home_activity.props.launch_status == shell.Activity.LAUNCHING:
+ self._icon.props.pulsing = True
+ 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():
+ palette = JournalPalette(self._home_activity)
+ else:
+ palette = CurrentActivityPalette(self._home_activity)
+ palette.set_group_id('frame')
+ self.set_palette(palette)
+
+ 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
+
+
+
+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()
+
+ 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')
+
+ def __clicked_cb(self, button):
+ if self._notif_icon is not None:
+ frame = jarabe.frame.get_view()
+ frame.remove_notification(self._notif_icon)
+ self._notif_icon = None
+ self._launch()
+
+ def __destroy_cb(self, button):
+ frame = jarabe.frame.get_view()
+ frame.remove_notification(self._notif_icon)
+
+ def _launch(self):
+ """Join the activity in the invite."""
+ self._invite.join()
+
+
+class InvitePalette(Palette):
+ """Palette for frame or notification icon for invites."""
+
+ def __init__(self, invite):
+ 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)
+ menu_item.show()
+
+ menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__decline_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ bundle_id = invite.get_bundle_id()
+
+ registry = bundleregistry.get_registry()
+ self._bundle = registry.get_bundle(bundle_id)
+ if self._bundle:
+ self.set_primary_text(self._bundle.get_name())
+ else:
+ self.set_primary_text(bundle_id)
+
+ def __join_activate_cb(self, menu_item):
+ self._invite.join()
+
+ def __decline_activate_cb(self, menu_item):
+ invites_model = invites.get_instance()
+ activity_id = self._activity_model.get_id()
+ invites_model.remove_activity(activity_id)
+
+
+class ActivitiesTray(HTray):
+ def __init__(self):
+ HTray.__init__(self)
+
+ self._buttons = {}
+ self._invite_to_item = {}
+ self._freeze_button_clicks = False
+
+ 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('active-activity-changed',
+ self.__activity_changed_cb)
+ self._home_model.connect('tabbing-activity-changed',
+ self.__tabbing_activity_changed_cb)
+
+ self._invites = invites.get_instance()
+ for invite in self._invites:
+ self._add_invite(invite)
+ self._invites.connect('invite-added', self.__invite_added_cb)
+ self._invites.connect('invite-removed', self.__invite_removed_cb)
+
+ filetransfer.new_file_transfer.connect(self.__new_file_transfer_cb)
+
+ def __activity_added_cb(self, home_model, home_activity):
+ logging.debug('__activity_added_cb: %r', home_activity)
+ if self.get_children():
+ group = self.get_children()[0]
+ else:
+ group = None
+
+ button = ActivityButton(home_activity, group)
+ self.add_item(button)
+ self._buttons[home_activity] = button
+ button.connect('clicked', self.__activity_clicked_cb, home_activity)
+ button.show()
+
+ def __activity_removed_cb(self, home_model, home_activity):
+ logging.debug('__activity_removed_cb: %r', home_activity)
+ button = self._buttons[home_activity]
+ self.remove_item(button)
+ del self._buttons[home_activity]
+
+ def _activate_activity(self, home_activity):
+ button = self._buttons[home_activity]
+ self._freeze_button_clicks = True
+ button.props.active = True
+ self._freeze_button_clicks = False
+
+ self.scroll_to_item(button)
+ # Redraw immediately.
+ # The widget may not be realized yet, and then there is no window.
+ if self.window:
+ self.window.process_updates(True)
+
+ def __activity_changed_cb(self, home_model, home_activity):
+ logging.debug('__activity_changed_cb: %r', home_activity)
+
+ # Only select the new activity, if there is no tabbing activity.
+ if home_model.get_tabbing_activity() is None:
+ self._activate_activity(home_activity)
+
+ def __tabbing_activity_changed_cb(self, home_model, home_activity):
+ logging.debug('__tabbing_activity_changed_cb: %r', home_activity)
+ # If the tabbing_activity is set to None just do nothing.
+ # The active activity will be updated a bit later (and it will
+ # be set to the activity that is currently selected).
+ if home_activity is None:
+ return
+
+ self._activate_activity(home_activity)
+
+ def __activity_clicked_cb(self, button, home_activity):
+ if not self._freeze_button_clicks and button.props.active:
+ logging.debug('ActivitiesTray.__activity_clicked_cb')
+ window = home_activity.get_window()
+ if window:
+ window.activate(gtk.get_current_event_time())
+
+ def __invite_clicked_cb(self, icon, invite):
+ self._invites.remove_invite(invite)
+
+ def __invite_added_cb(self, invites_model, invite):
+ self._add_invite(invite)
+
+ def __invite_removed_cb(self, invites_model, invite):
+ self._remove_invite(invite)
+
+ def _add_invite(self, invite):
+ """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])
+ self._invite_to_item[invite].destroy()
+ del self._invite_to_item[invite]
+
+ def __new_file_transfer_cb(self, **kwargs):
+ file_transfer = kwargs['file_transfer']
+ logging.debug('__new_file_transfer_cb %r', file_transfer)
+
+ if isinstance(file_transfer, filetransfer.IncomingFileTransfer):
+ button = IncomingTransferButton(file_transfer)
+ elif isinstance(file_transfer, filetransfer.OutgoingFileTransfer):
+ button = OutgoingTransferButton(file_transfer)
+
+ self.add_item(button)
+ button.show()
+
+class BaseTransferButton(ToolButton):
+ """Button with a notification attached
+ """
+ def __init__(self, file_transfer):
+ ToolButton.__init__(self)
+
+ self.file_transfer = file_transfer
+ file_transfer.connect('notify::state', self.__notify_state_cb)
+
+ icon = Icon()
+ self.props.icon_widget = icon
+ icon.show()
+
+ self.notif_icon = NotificationIcon()
+ self.notif_icon.connect('button-release-event',
+ self.__button_release_event_cb)
+
+ def __button_release_event_cb(self, icon, event):
+ if self.notif_icon is not None:
+ frame = jarabe.frame.get_view()
+ frame.remove_notification(self.notif_icon)
+ self.notif_icon = None
+
+ def remove(self):
+ frame = jarabe.frame.get_view()
+ frame.remove_notification(self.notif_icon)
+ self.props.parent.remove(self)
+
+ def __notify_state_cb(self, file_transfer, pspec):
+ logging.debug('_update state: %r %r', file_transfer.props.state,
+ file_transfer.reason_last_change)
+ if file_transfer.props.state == filetransfer.FT_STATE_CANCELLED:
+ if file_transfer.reason_last_change == \
+ filetransfer.FT_REASON_LOCAL_STOPPED:
+ self.remove()
+
+class IncomingTransferButton(BaseTransferButton):
+ """UI element representing an ongoing incoming file transfer
+ """
+ def __init__(self, file_transfer):
+ BaseTransferButton.__init__(self, file_transfer)
+
+ self._ds_object = datastore.create()
+
+ file_transfer.connect('notify::state', self.__notify_state_cb)
+ file_transfer.connect('notify::transferred-bytes',
+ self.__notify_transferred_bytes_cb)
+
+ icons = gio.content_type_get_icon(file_transfer.mime_type).props.names
+ icons.append('application-octet-stream')
+ for icon_name in icons:
+ icon_name = 'transfer-from-%s' % icon_name
+ file_name = get_icon_file_name(icon_name)
+ if file_name is not None:
+ self.props.icon_widget.props.icon_name = icon_name
+ self.notif_icon.props.icon_name = icon_name
+ break
+
+ icon_color = XoColor(file_transfer.buddy.props.color)
+ self.props.icon_widget.props.xo_color = icon_color
+ self.notif_icon.props.xo_color = icon_color
+
+ frame = jarabe.frame.get_view()
+ frame.add_notification(self.notif_icon,
+ gtk.CORNER_TOP_LEFT)
+
+ def create_palette(self):
+ palette = IncomingTransferPalette(self.file_transfer)
+ palette.connect('dismiss-clicked', self.__dismiss_clicked_cb)
+ palette.props.invoker = FrameWidgetInvoker(self)
+ palette.set_group_id('frame')
+ return palette
+
+ def __notify_state_cb(self, file_transfer, pspec):
+ if file_transfer.props.state == filetransfer.FT_STATE_OPEN:
+ logging.debug('__notify_state_cb OPEN')
+ self._ds_object.metadata['title'] = file_transfer.title
+ self._ds_object.metadata['description'] = file_transfer.description
+ self._ds_object.metadata['progress'] = '0'
+ self._ds_object.metadata['keep'] = '0'
+ self._ds_object.metadata['buddies'] = ''
+ self._ds_object.metadata['preview'] = ''
+ self._ds_object.metadata['icon-color'] = \
+ file_transfer.buddy.props.color
+ 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_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._ds_object.object_id
+ if object_id is not None:
+ self._ds_object.destroy()
+ datastore.delete(object_id)
+ self._ds_object = None
+
+ def __notify_transferred_bytes_cb(self, file_transfer, pspec):
+ progress = file_transfer.props.transferred_bytes / \
+ file_transfer.file_size
+ self._ds_object.metadata['progress'] = str(progress * 100)
+ datastore.write(self._ds_object, update_mtime=False)
+
+ def __reply_handler_cb(self):
+ 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._ds_object.object_id,
+ error)
+
+ def __dismiss_clicked_cb(self, palette):
+ self.remove()
+
+class OutgoingTransferButton(BaseTransferButton):
+ """UI element representing an ongoing outgoing file transfer
+ """
+ def __init__(self, file_transfer):
+ BaseTransferButton.__init__(self, file_transfer)
+
+ icons = gio.content_type_get_icon(file_transfer.mime_type).props.names
+ icons.append('application-octet-stream')
+ for icon_name in icons:
+ icon_name = 'transfer-to-%s' % icon_name
+ file_name = get_icon_file_name(icon_name)
+ if file_name is not None:
+ self.props.icon_widget.props.icon_name = icon_name
+ self.notif_icon.props.icon_name = icon_name
+ break
+
+ client = gconf.client_get_default()
+ 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
+
+ frame = jarabe.frame.get_view()
+ frame.add_notification(self.notif_icon,
+ gtk.CORNER_TOP_LEFT)
+
+ def create_palette(self):
+ palette = OutgoingTransferPalette(self.file_transfer)
+ palette.connect('dismiss-clicked', self.__dismiss_clicked_cb)
+ palette.props.invoker = FrameWidgetInvoker(self)
+ palette.set_group_id('frame')
+ return palette
+
+ 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"
+
+ __gsignals__ = {
+ 'dismiss-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([])),
+ }
+
+ def __init__(self, file_transfer):
+ Palette.__init__(self, file_transfer.title)
+
+ self.file_transfer = file_transfer
+
+ self.progress_bar = None
+ self.progress_label = None
+ self._notify_transferred_bytes_handler = None
+
+ self.connect('popup', self.__popup_cb)
+ self.connect('popdown', self.__popdown_cb)
+
+ def __popup_cb(self, palette):
+ self.update_progress()
+ self._notify_transferred_bytes_handler = \
+ self.file_transfer.connect('notify::transferred_bytes',
+ self.__notify_transferred_bytes_cb)
+
+ def __popdown_cb(self, palette):
+ if self._notify_transferred_bytes_handler is not None:
+ self.file_transfer.disconnect(
+ self._notify_transferred_bytes_handler)
+ self._notify_transferred_bytes_handler = None
+
+ def __notify_transferred_bytes_cb(self, file_transfer, pspec):
+ self.update_progress()
+
+ def _format_size(self, size):
+ if size < 1024:
+ return _('%dB') % size
+ elif size < 1048576:
+ return _('%dKB') % (size / 1024)
+ else:
+ return _('%dMB') % (size / 1048576)
+
+ def update_progress(self):
+ logging.debug('update_progress: %r',
+ self.file_transfer.props.transferred_bytes)
+
+ if self.progress_bar is None:
+ return
+
+ self.progress_bar.props.fraction = \
+ self.file_transfer.props.transferred_bytes / \
+ float(self.file_transfer.file_size)
+ logging.debug('update_progress: %r', self.progress_bar.props.fraction)
+
+ transferred = self._format_size(
+ self.file_transfer.props.transferred_bytes)
+ 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"
+ def __init__(self, file_transfer):
+ BaseTransferPalette.__init__(self, file_transfer)
+
+ self.file_transfer.connect('notify::state', self.__notify_state_cb)
+
+ nick = self.file_transfer.buddy.props.nick
+ self.props.secondary_text = _('Transfer from %r') % nick
+
+ self._update()
+
+ def __notify_state_cb(self, file_transfer, pspec):
+ self._update()
+
+ def _update(self):
+ logging.debug('_update state: %r', self.file_transfer.props.state)
+ if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING:
+ menu_item = MenuItem(_('Accept'), icon_name='dialog-ok')
+ menu_item.connect('activate', self.__accept_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__decline_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ if self.file_transfer.description:
+ label = gtk.Label(self.file_transfer.description)
+ vbox.add(label)
+ label.show()
+
+ mime_type = self.file_transfer.mime_type
+ type_description = mime.get_mime_description(mime_type)
+
+ size = self._format_size(self.file_transfer.file_size)
+ label = gtk.Label(_('%s (%s)') % (size, type_description))
+ vbox.add(label)
+ label.show()
+
+ elif self.file_transfer.props.state in \
+ [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]:
+
+ for item in self.menu.get_children():
+ self.menu.remove(item)
+
+ menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__cancel_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ self.progress_bar = gtk.ProgressBar()
+ vbox.add(self.progress_bar)
+ self.progress_bar.show()
+
+ self.progress_label = gtk.Label('')
+ vbox.add(self.progress_label)
+ self.progress_label.show()
+
+ self.update_progress()
+
+ elif self.file_transfer.props.state == filetransfer.FT_STATE_COMPLETED:
+
+ for item in self.menu.get_children():
+ self.menu.remove(item)
+
+ menu_item = MenuItem(_('Dismiss'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__dismiss_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ self.update_progress()
+ elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED:
+
+ for item in self.menu.get_children():
+ self.menu.remove(item)
+
+ menu_item = MenuItem(_('Resume'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__resume_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ self.update_progress()
+
+ def __accept_activate_cb(self, menu_item):
+ #TODO: figure out the best place to get rid of that temp file
+ extension = mime.get_primary_extension(self.file_transfer.mime_type)
+ if extension is None:
+ extension = '.bin'
+ fd, file_path = tempfile.mkstemp(suffix=extension,
+ prefix=self._sanitize(self.file_transfer.title),
+ dir=os.path.join(env.get_profile_path(), 'data'))
+ os.close(fd)
+ os.unlink(file_path)
+
+ self.file_transfer.accept(file_path)
+
+ def _sanitize(self, file_name):
+ file_name = file_name.replace('/', '_')
+ file_name = file_name.replace('.', '_')
+ file_name = file_name.replace('?', '_')
+ return file_name
+
+ def __decline_activate_cb(self, menu_item):
+ self.file_transfer.cancel()
+
+ def __cancel_activate_cb(self, menu_item):
+ self.file_transfer.cancel()
+
+ def __resume_activate_cb(self, menu_item):
+ self.file_transfer.resume()
+
+ 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"
+
+ def __init__(self, file_transfer):
+ BaseTransferPalette.__init__(self, file_transfer)
+
+ self.progress_bar = None
+ self.progress_label = None
+
+ self.file_transfer.connect('notify::state', self.__notify_state_cb)
+
+ nick = file_transfer.buddy.props.nick
+ self.props.secondary_text = _('Transfer to %r') % nick
+
+ self._update()
+
+ def __notify_state_cb(self, file_transfer, pspec):
+ self._update()
+
+ def _update(self):
+ new_state = self.file_transfer.props.state
+ logging.debug('_update state: %r', new_state)
+ if new_state == filetransfer.FT_STATE_PENDING:
+
+ menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__cancel_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ if self.file_transfer.description:
+ label = gtk.Label(self.file_transfer.description)
+ vbox.add(label)
+ label.show()
+
+ mime_type = self.file_transfer.mime_type
+ type_description = mime.get_mime_description(mime_type)
+
+ size = self._format_size(self.file_transfer.file_size)
+ label = gtk.Label(_('%s (%s)') % (size, type_description))
+ vbox.add(label)
+ label.show()
+
+ elif new_state in [filetransfer.FT_STATE_ACCEPTED,
+ filetransfer.FT_STATE_OPEN]:
+
+ for item in self.menu.get_children():
+ self.menu.remove(item)
+
+ menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__cancel_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ self.progress_bar = gtk.ProgressBar()
+ vbox.add(self.progress_bar)
+ self.progress_bar.show()
+
+ self.progress_label = gtk.Label('')
+ vbox.add(self.progress_label)
+ self.progress_label.show()
+
+ self.update_progress()
+
+ elif new_state in [filetransfer.FT_STATE_COMPLETED,
+ filetransfer.FT_STATE_CANCELLED]:
+
+ for item in self.menu.get_children():
+ self.menu.remove(item)
+
+ menu_item = MenuItem(_('Dismiss'), icon_name='dialog-cancel')
+ menu_item.connect('activate', self.__dismiss_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ self.update_progress()
+
+ def __cancel_activate_cb(self, menu_item):
+ self.file_transfer.cancel()
+
+ def __dismiss_activate_cb(self, menu_item):
+ self.emit('dismiss-clicked')
diff --git a/shell/src/jarabe/frame/clipboard.py b/shell/src/jarabe/frame/clipboard.py
new file mode 100644
index 0000000..3b9f745
--- /dev/null
+++ b/shell/src/jarabe/frame/clipboard.py
@@ -0,0 +1,149 @@
+# Copyright (C) 2006, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import os
+import shutil
+import urlparse
+import tempfile
+
+import gobject
+
+from sugar import mime
+
+from jarabe.frame.clipboardobject import ClipboardObject, Format
+
+class Clipboard(gobject.GObject):
+
+ __gsignals__ = {
+ 'object-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'object-deleted': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([int])),
+ 'object-state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._objects = {}
+ self._next_id = 0
+
+ def _get_next_object_id(self):
+ self._next_id += 1
+ return self._next_id
+
+ def add_object(self, name):
+ logging.debug('Clipboard.add_object')
+ object_id = self._get_next_object_id()
+ self._objects[object_id] = ClipboardObject(object_id, name)
+ self.emit('object-added', self._objects[object_id])
+ return object_id
+
+ def add_object_format(self, object_id, format_type, data, on_disk):
+ logging.debug('Clipboard.add_object_format')
+ cb_object = self._objects[object_id]
+
+ if format_type == 'XdndDirectSave0':
+ format_ = Format('text/uri-list', data + '\r\n', on_disk)
+ format_.owns_disk_data = True
+ cb_object.add_format(format_)
+ elif on_disk and cb_object.get_percent() == 100:
+ new_uri = self._copy_file(data)
+ cb_object.add_format(Format(format_type, new_uri, on_disk))
+ logging.debug('Added format of type ' + format_type
+ + ' with path at ' + new_uri)
+ else:
+ cb_object.add_format(Format(format_type, data, on_disk))
+ logging.debug('Added in-memory format of type ' + format_type + '.')
+
+ self.emit('object-state-changed', cb_object)
+
+ def delete_object(self, object_id):
+ cb_object = self._objects.pop(object_id)
+ cb_object.destroy()
+ self.emit('object-deleted', object_id)
+ logging.debug('Deleted object with object_id %r', object_id)
+
+ def set_object_percent(self, object_id, percent):
+ cb_object = self._objects[object_id]
+ if percent < 0 or percent > 100:
+ raise ValueError("invalid percentage")
+ if cb_object.get_percent() > percent:
+ raise ValueError("invalid percentage; less than current percent")
+ if cb_object.get_percent() == percent:
+ # ignore setting same percentage
+ return
+
+ cb_object.set_percent(percent)
+
+ if percent == 100:
+ self._process_object(cb_object)
+
+ self.emit('object-state-changed', cb_object)
+
+ def _process_object(self, cb_object):
+ formats = cb_object.get_formats()
+ for format_name, format_ in formats.iteritems():
+ if format_.is_on_disk() and not format_.owns_disk_data:
+ new_uri = self._copy_file(format_.get_data())
+ format_.set_data(new_uri)
+
+ # Add a text/plain format to objects that are text but lack it
+ if 'text/plain' not in formats.keys():
+ if 'UTF8_STRING' in formats.keys():
+ self.add_object_format(
+ cb_object.get_id(), 'text/plain',
+ data=formats['UTF8_STRING'].get_data(), on_disk=False)
+ elif 'text/unicode' in formats.keys():
+ self.add_object_format(
+ cb_object.get_id(), 'text/plain',
+ data=formats['UTF8_STRING'].get_data(), on_disk=False)
+
+ def get_object(self, object_id):
+ logging.debug('Clipboard.get_object')
+ return self._objects[object_id]
+
+ def get_object_data(self, object_id, format_type):
+ logging.debug('Clipboard.get_object_data')
+ cb_object = self._objects[object_id]
+ format_ = cb_object.get_formats()[format_type]
+ return format_
+
+ def _copy_file(self, original_uri):
+ uri = urlparse.urlparse(original_uri)
+ path_, file_name = os.path.split(uri.path)
+
+ root, ext = os.path.splitext(file_name)
+ if not ext or ext == '.':
+ mime_type = mime.get_for_file(uri.path)
+ ext = '.' + mime.get_primary_extension(mime_type)
+
+ f_, new_file_path = tempfile.mkstemp(ext, root)
+ del f_
+ shutil.copyfile(uri.path, new_file_path)
+ os.chmod(new_file_path, 0644)
+
+ return 'file://' + new_file_path
+
+_instance = None
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = Clipboard()
+ return _instance
diff --git a/shell/src/jarabe/frame/clipboardicon.py b/shell/src/jarabe/frame/clipboardicon.py
new file mode 100644
index 0000000..279db08
--- /dev/null
+++ b/shell/src/jarabe/frame/clipboardicon.py
@@ -0,0 +1,158 @@
+# Copyright (C) 2007, Red Hat, Inc.
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import gconf
+
+import gtk
+
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.icon import Icon
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics import style
+
+from jarabe.frame import clipboard
+from jarabe.frame.clipboardmenu import ClipboardMenu
+from jarabe.frame.frameinvoker import FrameWidgetInvoker
+from jarabe.frame.notification import NotificationIcon
+import jarabe.frame
+
+class ClipboardIcon(RadioToolButton):
+ __gtype_name__ = 'SugarClipboardIcon'
+
+ def __init__(self, cb_object, group):
+ RadioToolButton.__init__(self, group=group)
+
+ self.props.palette_invoker = FrameWidgetInvoker(self)
+
+ self._cb_object = cb_object
+ self.owns_clipboard = False
+ self.props.sensitive = False
+ self.props.active = False
+ self._notif_icon = None
+ self._current_percent = None
+
+ self._icon = Icon()
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ self._icon.props.xo_color = color
+ self.set_icon_widget(self._icon)
+ self._icon.show()
+
+ cb_service = clipboard.get_instance()
+ cb_service.connect('object-state-changed',
+ self._object_state_changed_cb)
+
+ child = self.get_child()
+ child.connect('drag_data_get', self._drag_data_get_cb)
+ self.connect('notify::active', self._notify_active_cb)
+
+ def create_palette(self):
+ palette = ClipboardMenu(self._cb_object)
+ palette.set_group_id('frame')
+ return palette
+
+ def get_object_id(self):
+ return self._cb_object.get_id()
+
+ def _drag_data_get_cb(self, widget, context, selection, target_type,
+ event_time):
+ logging.debug('_drag_data_get_cb: requested target ' + selection.target)
+ data = self._cb_object.get_formats()[selection.target].get_data()
+ selection.set(selection.target, 8, data)
+
+ def _put_in_clipboard(self):
+ logging.debug('ClipboardIcon._put_in_clipboard')
+
+ if self._cb_object.get_percent() < 100:
+ raise ValueError('Object is not complete,' \
+ ' cannot be put into the clipboard.')
+
+ targets = self._get_targets()
+ if targets:
+ x_clipboard = gtk.Clipboard()
+ if not x_clipboard.set_with_data(targets,
+ self._clipboard_data_get_cb,
+ self._clipboard_clear_cb,
+ targets):
+ logging.error('GtkClipboard.set_with_data failed!')
+ else:
+ self.owns_clipboard = True
+
+ def _clipboard_data_get_cb(self, x_clipboard, selection, info, targets):
+ if not selection.target in [target[0] for target in targets]:
+ logging.warning('ClipboardIcon._clipboard_data_get_cb: asked %s' \
+ ' but only have %r.', selection.target, targets)
+ return
+ data = self._cb_object.get_formats()[selection.target].get_data()
+ selection.set(selection.target, 8, data)
+
+ def _clipboard_clear_cb(self, x_clipboard, targets):
+ logging.debug('ClipboardIcon._clipboard_clear_cb')
+ self.owns_clipboard = False
+
+ def _object_state_changed_cb(self, cb_service, cb_object):
+ if cb_object != self._cb_object:
+ return
+
+ if cb_object.get_icon():
+ self._icon.props.icon_name = cb_object.get_icon()
+ else:
+ self._icon.props.icon_name = 'application-octet-stream'
+
+ child = self.get_child()
+ child.connect('drag-begin', self._drag_begin_cb)
+ child.drag_source_set(gtk.gdk.BUTTON1_MASK,
+ self._get_targets(),
+ gtk.gdk.ACTION_COPY)
+
+ if cb_object.get_percent() == 100:
+ self.props.sensitive = True
+
+ # Clipboard object became complete. Make it the active one.
+ if self._current_percent < 100 and cb_object.get_percent() == 100:
+ self.props.active = True
+
+ self._notif_icon = NotificationIcon()
+ self._notif_icon.props.icon_name = self._icon.props.icon_name
+ self._notif_icon.props.xo_color = \
+ XoColor('%s,%s' % (self._icon.props.stroke_color,
+ self._icon.props.fill_color))
+ frame = jarabe.frame.get_view()
+ frame.add_notification(self._notif_icon,
+ gtk.CORNER_BOTTOM_LEFT)
+ self._current_percent = cb_object.get_percent()
+
+ def _drag_begin_cb(self, widget, context):
+ # TODO: We should get the pixbuf from the icon, with colors, etc.
+ icon_theme = gtk.icon_theme_get_default()
+ pixbuf = icon_theme.load_icon(self._icon.props.icon_name,
+ style.STANDARD_ICON_SIZE, 0)
+ context.set_icon_pixbuf(pixbuf, hot_x=pixbuf.props.width / 2,
+ hot_y=pixbuf.props.height / 2)
+
+ def _notify_active_cb(self, widget, pspec):
+ if self.props.active:
+ self._put_in_clipboard()
+ else:
+ self.owns_clipboard = False
+
+ def _get_targets(self):
+ targets = []
+ for format_type in self._cb_object.get_formats().keys():
+ targets.append((format_type, 0, 0))
+ return targets
diff --git a/shell/src/jarabe/frame/clipboardmenu.py b/shell/src/jarabe/frame/clipboardmenu.py
new file mode 100644
index 0000000..b998110
--- /dev/null
+++ b/shell/src/jarabe/frame/clipboardmenu.py
@@ -0,0 +1,249 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+import tempfile
+import urlparse
+import os
+import logging
+import gconf
+
+import gtk
+
+from sugar.graphics.palette import Palette
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.graphics.xocolor import XoColor
+from sugar.datastore import datastore
+from sugar import mime
+from sugar import env
+
+from jarabe.frame import clipboard
+from jarabe.journal import misc
+from jarabe.model import bundleregistry
+
+class ClipboardMenu(Palette):
+
+ def __init__(self, cb_object):
+ Palette.__init__(self, text_maxlen=100)
+
+ self._cb_object = cb_object
+
+ self.set_group_id('frame')
+
+ cb_service = clipboard.get_instance()
+ cb_service.connect('object-state-changed',
+ self._object_state_changed_cb)
+
+ self._progress_bar = None
+
+ self._remove_item = MenuItem(_('Remove'), 'list-remove')
+ self._remove_item.connect('activate', self._remove_item_activate_cb)
+ self.menu.append(self._remove_item)
+ self._remove_item.show()
+
+ self._open_item = MenuItem(_('Open'), 'zoom-activity')
+ self._open_item.connect('activate', self._open_item_activate_cb)
+ self.menu.append(self._open_item)
+ self._open_item.show()
+
+ self._journal_item = MenuItem(_('Keep'))
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ icon = Icon(icon_name='document-save', icon_size=gtk.ICON_SIZE_MENU,
+ xo_color=color)
+ self._journal_item.set_image(icon)
+
+ self._journal_item.connect('activate', self._journal_item_activate_cb)
+ self.menu.append(self._journal_item)
+ self._journal_item.show()
+
+ self._update()
+
+ def _update_open_submenu(self):
+ activities = self._get_activities()
+ logging.debug('_update_open_submenu: %r', activities)
+ child = self._open_item.get_child()
+ if activities is None or len(activities) <= 1:
+ child.set_text(_('Open'))
+ if self._open_item.get_submenu() is not None:
+ self._open_item.remove_submenu()
+ return
+
+ child.set_text(_('Open with'))
+ submenu = self._open_item.get_submenu()
+ if submenu is None:
+ submenu = gtk.Menu()
+ self._open_item.set_submenu(submenu)
+ submenu.show()
+ else:
+ for item in submenu.get_children():
+ submenu.remove(item)
+
+ for service_name in activities:
+ registry = bundleregistry.get_registry()
+ activity_info = registry.get_bundle(service_name)
+
+ if not activity_info:
+ logging.warning('Activity %s is unknown.', service_name)
+
+ item = gtk.MenuItem(activity_info.get_name())
+ item.connect('activate', self._open_submenu_item_activate_cb,
+ service_name)
+ submenu.append(item)
+ item.show()
+
+ def _update_items_visibility(self):
+ activities = self._get_activities()
+ installable = self._cb_object.is_bundle()
+ percent = self._cb_object.get_percent()
+
+ if percent == 100 and (activities or installable):
+ self._remove_item.props.sensitive = True
+ self._open_item.props.sensitive = True
+ self._journal_item.props.sensitive = True
+ elif percent == 100 and (not activities and not installable):
+ self._remove_item.props.sensitive = True
+ self._open_item.props.sensitive = False
+ self._journal_item.props.sensitive = True
+ else:
+ self._remove_item.props.sensitive = True
+ self._open_item.props.sensitive = False
+ self._journal_item.props.sensitive = False
+
+ self._update_progress_bar()
+
+ def _get_activities(self):
+ mime_type = self._cb_object.get_mime_type()
+ if not mime_type:
+ return ''
+
+ registry = bundleregistry.get_registry()
+ activities = registry.get_activities_for_type(mime_type)
+ if activities:
+ return [info.get_bundle_id() for info in activities]
+ else:
+ return ''
+
+ def _update_progress_bar(self):
+ percent = self._cb_object.get_percent()
+ if percent == 100.0:
+ if self._progress_bar:
+ self._progress_bar = None
+ self.set_content(None)
+ else:
+ if self._progress_bar is None:
+ self._progress_bar = gtk.ProgressBar()
+ self._progress_bar.show()
+ self.set_content(self._progress_bar)
+
+ self._progress_bar.props.fraction = percent / 100.0
+ self._progress_bar.props.text = '%.2f %%' % percent
+
+ def _object_state_changed_cb(self, cb_service, cb_object):
+ if cb_object != self._cb_object:
+ return
+ self._update()
+
+ def _update(self):
+ self.props.primary_text = self._cb_object.get_name()
+ preview = self._cb_object.get_preview()
+ if preview:
+ self.props.secondary_text = preview
+ self._update_progress_bar()
+ self._update_items_visibility()
+ self._update_open_submenu()
+
+ def _open_item_activate_cb(self, menu_item):
+ logging.debug('_open_item_activate_cb')
+ percent = self._cb_object.get_percent()
+ if percent < 100 or menu_item.get_submenu() is not None:
+ return
+ jobject = self._copy_to_journal()
+ misc.resume(jobject.metadata, self._get_activities()[0])
+ jobject.destroy()
+
+ def _open_submenu_item_activate_cb(self, menu_item, service_name):
+ logging.debug('_open_submenu_item_activate_cb')
+ percent = self._cb_object.get_percent()
+ if percent < 100:
+ return
+ jobject = self._copy_to_journal()
+ misc.resume(jobject.metadata, service_name)
+ jobject.destroy()
+
+ def _remove_item_activate_cb(self, menu_item):
+ cb_service = clipboard.get_instance()
+ cb_service.delete_object(self._cb_object.get_id())
+
+ def _journal_item_activate_cb(self, menu_item):
+ logging.debug('_journal_item_activate_cb')
+ jobject = self._copy_to_journal()
+ jobject.destroy()
+
+ def _write_to_temp_file(self, data):
+ tmp_dir = os.path.join(env.get_profile_path(), 'data')
+ f, file_path = tempfile.mkstemp(dir=tmp_dir)
+ try:
+ os.write(f, data)
+ finally:
+ os.close(f)
+ return file_path
+
+ def _copy_to_journal(self):
+ formats = self._cb_object.get_formats().keys()
+ most_significant_mime_type = mime.choose_most_significant(formats)
+ format_ = self._cb_object.get_formats()[most_significant_mime_type]
+
+ transfer_ownership = False
+ if most_significant_mime_type == 'text/uri-list':
+ uris = mime.split_uri_list(format_.get_data())
+ if len(uris) == 1 and uris[0].startswith('file://'):
+ file_path = urlparse.urlparse(uris[0]).path
+ transfer_ownership = False
+ mime_type = mime.get_for_file(file_path)
+ else:
+ file_path = self._write_to_temp_file(format_.get_data())
+ transfer_ownership = True
+ mime_type = 'text/uri-list'
+ else:
+ if format_.is_on_disk():
+ file_path = urlparse.urlparse(format_.get_data()).path
+ transfer_ownership = False
+ mime_type = mime.get_for_file(file_path)
+ else:
+ file_path = self._write_to_temp_file(format_.get_data())
+ transfer_ownership = True
+ sniffed_mime_type = mime.get_for_file(file_path)
+ if sniffed_mime_type == 'application/octet-stream':
+ mime_type = most_significant_mime_type
+ else:
+ mime_type = sniffed_mime_type
+
+ jobject = datastore.create()
+ jobject.metadata['title'] = self._cb_object.get_name()
+ jobject.metadata['keep'] = '0'
+ jobject.metadata['buddies'] = ''
+ jobject.metadata['preview'] = ''
+ client = gconf.client_get_default()
+ color = client.get_string('/desktop/sugar/user/color')
+ jobject.metadata['icon-color'] = color
+ jobject.metadata['mime_type'] = mime_type
+ jobject.file_path = file_path
+
+ datastore.write(jobject, transfer_ownership=transfer_ownership)
+
+ return jobject
diff --git a/shell/src/jarabe/frame/clipboardobject.py b/shell/src/jarabe/frame/clipboardobject.py
new file mode 100644
index 0000000..e9403f9
--- /dev/null
+++ b/shell/src/jarabe/frame/clipboardobject.py
@@ -0,0 +1,142 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+import urlparse
+import gio
+import gtk
+
+from gettext import gettext as _
+from sugar import mime
+from sugar.bundle.activitybundle import ActivityBundle
+
+class ClipboardObject(object):
+
+ def __init__(self, object_path, name):
+ self._id = object_path
+ self._name = name
+ self._percent = 0
+ self._formats = {}
+
+ def destroy(self):
+ for format_ in self._formats.itervalues():
+ format_.destroy()
+
+ def get_id(self):
+ return self._id
+
+ def get_name(self):
+ name = self._name
+ if not name:
+ mime_type = mime.get_mime_description(self.get_mime_type())
+
+ if not mime_type:
+ mime_type = 'Data'
+ name = _('%s clipping') % mime_type
+
+ return name
+
+ def get_icon(self):
+ mime_type = self.get_mime_type()
+
+ generic_types = mime.get_all_generic_types()
+ for generic_type in generic_types:
+ if mime_type in generic_type.mime_types:
+ return generic_type.icon
+
+ icons = gio.content_type_get_icon(mime_type)
+ icon_name = None
+ if icons is not None:
+ icon_theme = gtk.icon_theme_get_default()
+ for icon_name in icons.props.names:
+ icon_info = icon_theme.lookup_icon(icon_name,
+ gtk.ICON_SIZE_LARGE_TOOLBAR, 0)
+ if icon_info is not None:
+ icon_info.free()
+ return icon_name
+
+ return 'application-octet-stream'
+
+ def get_preview(self):
+ for mime_type in ['text/plain']:
+ if mime_type in self._formats:
+ return self._formats[mime_type].get_data()
+ return ''
+
+ def is_bundle(self):
+ # A bundle will have only one format.
+ if not self._formats:
+ return False
+ else:
+ return self._formats.keys()[0] in [ActivityBundle.MIME_TYPE,
+ ActivityBundle.DEPRECATED_MIME_TYPE]
+
+ def get_percent(self):
+ return self._percent
+
+ def set_percent(self, percent):
+ self._percent = percent
+
+ def add_format(self, format_):
+ self._formats[format_.get_type()] = format_
+
+ def get_formats(self):
+ return self._formats
+
+ def get_mime_type(self):
+ if not self._formats:
+ return ''
+
+ format_ = mime.choose_most_significant(self._formats.keys())
+ if format_ == 'text/uri-list':
+ data = self._formats['text/uri-list'].get_data()
+ uri = urlparse.urlparse(mime.split_uri_list(data)[0], 'file')
+ if uri.scheme == 'file':
+ if os.path.exists(uri.path):
+ format_ = mime.get_for_file(uri.path)
+ else:
+ format_ = mime.get_from_file_name(uri.path)
+ logging.debug('Chose %r!', format_)
+
+ return format_
+
+class Format(object):
+
+ def __init__(self, mime_type, data, on_disk):
+ self.owns_disk_data = False
+
+ self._type = mime_type
+ self._data = data
+ self._on_disk = on_disk
+
+ def destroy(self):
+ if self._on_disk:
+ uri = urlparse.urlparse(self._data)
+ if os.path.isfile(uri.path):
+ os.remove(uri.path)
+
+ def get_type(self):
+ return self._type
+
+ def get_data(self):
+ return self._data
+
+ def set_data(self, data):
+ self._data = data
+
+ def is_on_disk(self):
+ return self._on_disk
diff --git a/shell/src/jarabe/frame/clipboardpanelwindow.py b/shell/src/jarabe/frame/clipboardpanelwindow.py
new file mode 100644
index 0000000..ac324f4
--- /dev/null
+++ b/shell/src/jarabe/frame/clipboardpanelwindow.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from urlparse import urlparse
+
+import gtk
+import hippo
+
+from jarabe.frame.framewindow import FrameWindow
+from jarabe.frame.clipboardtray import ClipboardTray
+
+from jarabe.frame import clipboard
+
+class ClipboardPanelWindow(FrameWindow):
+ def __init__(self, frame, orientation):
+ FrameWindow.__init__(self, orientation)
+
+ self._frame = frame
+
+ # Listening for new clipboard objects
+ # NOTE: we need to keep a reference to gtk.Clipboard in order to keep
+ # listening to it.
+ self._clipboard = gtk.Clipboard()
+ self._clipboard.connect("owner-change", self._owner_change_cb)
+
+ self._clipboard_tray = ClipboardTray()
+ canvas_widget = hippo.CanvasWidget(widget=self._clipboard_tray)
+ self.append(canvas_widget, hippo.PACK_EXPAND)
+
+ # Receiving dnd drops
+ self.drag_dest_set(0, [], 0)
+ self.connect("drag_motion", self._clipboard_tray.drag_motion_cb)
+ self.connect("drag_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")
+
+ if self._clipboard_tray.owns_clipboard():
+ return
+
+ cb_service = clipboard.get_instance()
+ key = cb_service.add_object(name="")
+ cb_service.set_object_percent(key, percent=0)
+
+ targets = x_clipboard.wait_for_targets()
+ for target in targets:
+ if target not in ('TIMESTAMP', 'TARGETS',
+ 'MULTIPLE', 'SAVE_TARGETS'):
+ logging.debug('Asking for target %s.', target)
+ selection = x_clipboard.wait_for_contents(target)
+ if not selection:
+ logging.warning('no data for selection target %s.', target)
+ continue
+ self._add_selection(key, selection)
+
+ cb_service.set_object_percent(key, percent=100)
+
+ def _add_selection(self, key, selection):
+ if not selection.data:
+ logging.warning('no data for selection target %s.', selection.type)
+ return
+
+ logging.debug('adding type ' + selection.type + '.')
+
+ cb_service = clipboard.get_instance()
+ if selection.type == 'text/uri-list':
+ uris = selection.get_uris()
+
+ if len(uris) > 1:
+ raise NotImplementedError('Multiple uris in text/uri-list' \
+ ' still not supported.')
+ uri = uris[0]
+ scheme, netloc_, path_, parameters_, query_, fragment_ = \
+ urlparse(uri)
+ on_disk = (scheme == 'file')
+
+ cb_service.add_object_format(key,
+ selection.type,
+ uri,
+ on_disk)
+ else:
+ cb_service.add_object_format(key,
+ selection.type,
+ selection.data,
+ on_disk=False)
+
diff --git a/shell/src/jarabe/frame/clipboardtray.py b/shell/src/jarabe/frame/clipboardtray.py
new file mode 100644
index 0000000..8beb6a8
--- /dev/null
+++ b/shell/src/jarabe/frame/clipboardtray.py
@@ -0,0 +1,216 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+import tempfile
+
+import gtk
+
+from sugar import util
+from sugar.graphics import tray
+from sugar.graphics import style
+
+from jarabe.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):
+ self._context_map = {}
+
+ def add_context(self, context, object_id, data_types):
+ """Establishes the mapping. data_types will serve us for reference-
+ counting this mapping.
+ """
+ self._context_map[context] = [object_id, data_types]
+
+ def get_object_id(self, context):
+ """Retrieves the object_id associated with context.
+ Will release the association when this function was called as many times
+ as the number of data_types that this clipboard object contains.
+ """
+ [object_id, data_types_left] = self._context_map[context]
+
+ data_types_left = data_types_left - 1
+ if data_types_left == 0:
+ del self._context_map[context]
+ else:
+ self._context_map[context] = [object_id, data_types_left]
+
+ return object_id
+
+ def has_context(self, context):
+ return context in self._context_map
+
+class ClipboardTray(tray.VTray):
+
+ MAX_ITEMS = gtk.gdk.screen_height() / style.GRID_CELL_SIZE - 2
+
+ def __init__(self):
+ tray.VTray.__init__(self, align=tray.ALIGN_TO_END)
+ self._icons = {}
+ self._context_map = _ContextMap()
+
+ cb_service = clipboard.get_instance()
+ cb_service.connect('object-added', self._object_added_cb)
+ cb_service.connect('object-deleted', self._object_deleted_cb)
+
+ def owns_clipboard(self):
+ for icon in self._icons.values():
+ if icon.owns_clipboard:
+ return True
+ return False
+
+ def _add_selection(self, object_id, selection):
+ if not selection.data:
+ return
+
+ logging.debug('ClipboardTray: adding type %r', selection.type)
+
+ cb_service = clipboard.get_instance()
+ if selection.type == 'text/uri-list':
+ uris = selection.data.split('\n')
+ if len(uris) > 1:
+ raise NotImplementedError('Multiple uris in text/uri-list' \
+ ' still not supported.')
+
+ cb_service.add_object_format(object_id,
+ selection.type,
+ uris[0],
+ on_disk=True)
+ else:
+ cb_service.add_object_format(object_id,
+ selection.type,
+ selection.data,
+ on_disk=False)
+
+ def _object_added_cb(self, cb_service, cb_object):
+ if self._icons:
+ group = self._icons.values()[0]
+ else:
+ group = None
+
+ icon = ClipboardIcon(cb_object, group)
+ self.add_item(icon)
+ icon.show()
+ self._icons[cb_object.get_id()] = icon
+
+ objects_to_delete = self.get_children()[:-self.MAX_ITEMS]
+ for icon in objects_to_delete:
+ logging.debug('ClipboardTray: deleting surplus object')
+ cb_service = clipboard.get_instance()
+ cb_service.delete_object(icon.get_object_id())
+
+ logging.debug('ClipboardTray: %r was added', cb_object.get_id())
+
+ def _object_deleted_cb(self, cb_service, object_id):
+ icon = self._icons[object_id]
+ self.remove_item(icon)
+ del self._icons[object_id]
+ logging.debug('ClipboardTray: %r was deleted', object_id)
+
+ def drag_motion_cb(self, widget, context, x, y, time):
+ logging.debug('ClipboardTray._drag_motion_cb')
+
+ if self._internal_drag(context):
+ context.drag_status(gtk.gdk.ACTION_MOVE, time)
+ else:
+ context.drag_status(gtk.gdk.ACTION_COPY, time)
+ self.props.drag_active = True
+
+ return True
+
+ def drag_leave_cb(self, widget, context, time):
+ self.props.drag_active = False
+
+ def drag_drop_cb(self, widget, context, x, y, time):
+ logging.debug('ClipboardTray._drag_drop_cb')
+
+ if self._internal_drag(context):
+ # TODO: We should move the object within the clipboard here
+ if not self._context_map.has_context(context):
+ context.drop_finish(False, gtk.get_current_event_time())
+ return False
+
+ cb_service = clipboard.get_instance()
+ object_id = cb_service.add_object(name="")
+
+ self._context_map.add_context(context, object_id, len(context.targets))
+
+ if 'XdndDirectSave0' in context.targets:
+ window = context.source_window
+ prop_type, format_, filename = \
+ window.property_get('XdndDirectSave0','text/plain')
+
+ # FIXME query the clipboard service for a filename?
+ base_dir = tempfile.gettempdir()
+ dest_filename = util.unique_id()
+
+ name_, dot, extension = filename.rpartition('.')
+ dest_filename += dot + extension
+
+ dest_uri = 'file://' + os.path.join(base_dir, dest_filename)
+
+ window.property_change('XdndDirectSave0', prop_type, format_,
+ gtk.gdk.PROP_MODE_REPLACE, dest_uri)
+
+ widget.drag_get_data(context, 'XdndDirectSave0', time)
+ else:
+ for target in context.targets:
+ if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'):
+ widget.drag_get_data(context, target, time)
+
+ cb_service.set_object_percent(object_id, percent=100)
+
+ return True
+
+ def drag_data_received_cb(self, widget, context, x, y, selection,
+ targetType, time):
+ logging.debug('ClipboardTray: got data for target %r',
+ selection.target)
+
+ object_id = self._context_map.get_object_id(context)
+ try:
+ if selection is None:
+ logging.warn('ClipboardTray: empty selection for target %s',
+ selection.target)
+ elif selection.target == 'XdndDirectSave0':
+ if selection.data == 'S':
+ window = context.source_window
+
+ prop_type, format_, dest = \
+ window.property_get('XdndDirectSave0', 'text/plain')
+
+ clipboardservice = clipboard.get_instance()
+ clipboardservice.add_object_format( \
+ object_id, 'XdndDirectSave0', dest, on_disk=True)
+ else:
+ self._add_selection(object_id, selection)
+
+ finally:
+ # If it's the last target to be processed, finish
+ # the dnd transaction
+ if not self._context_map.has_context(context):
+ context.drop_finish(True, gtk.get_current_event_time())
+
+ def _internal_drag(self, context):
+ view_ancestor = context.get_source_widget().get_ancestor(gtk.Viewport)
+ if view_ancestor is self._viewport:
+ return True
+ else:
+ return False
+
diff --git a/shell/src/jarabe/frame/devicestray.py b/shell/src/jarabe/frame/devicestray.py
new file mode 100644
index 0000000..72affe3
--- /dev/null
+++ b/shell/src/jarabe/frame/devicestray.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import 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)
+
+ for f in os.listdir(os.path.join(config.ext_path, 'deviceicon')):
+ if f.endswith('.py') and not f.startswith('__'):
+ module_name = f[:-3]
+ try:
+ mod = __import__('deviceicon.' + module_name, globals(),
+ locals(), [module_name])
+ mod.setup(self)
+ except Exception:
+ logging.error('Exception while loading extension:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+
+ def add_device(self, view):
+ index = 0
+ relative_index = getattr(view, "FRAME_POSITION_RELATIVE", -1)
+ for item in self.get_children():
+ current_relative_index = getattr(item, "FRAME_POSITION_RELATIVE", 0)
+ if current_relative_index >= relative_index:
+ index += 1
+ else:
+ break
+ self.add_item(view, index=index)
+ view.show()
+
+ def remove_device(self, view):
+ self.remove_item(view)
diff --git a/shell/src/jarabe/frame/eventarea.py b/shell/src/jarabe/frame/eventarea.py
new file mode 100644
index 0000000..166aaf5
--- /dev/null
+++ b/shell/src/jarabe/frame/eventarea.py
@@ -0,0 +1,151 @@
+# Copyright (C) 2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import gobject
+import wnck
+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, ([]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._windows = []
+ self._hover = False
+ 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')
+
+ right = gtk.gdk.screen_width() - 1
+ bottom = gtk.gdk.screen_height() -1
+ width = gtk.gdk.screen_width() - 2
+ height = gtk.gdk.screen_height() - 2
+
+ if self._edge_delay != _MAX_DELAY:
+ invisible = self._create_invisible(1, 0, width, 1,
+ self._edge_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(1, bottom, width, 1,
+ self._edge_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(0, 1, 1, height,
+ self._edge_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(right, 1, 1, height,
+ self._edge_delay)
+ self._windows.append(invisible)
+
+ if self._corner_delay != _MAX_DELAY:
+ invisible = self._create_invisible(0, 0, 1, 1,
+ self._corner_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(right, 0, 1, 1,
+ self._corner_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(0, bottom, 1, 1,
+ self._corner_delay)
+ self._windows.append(invisible)
+
+ invisible = self._create_invisible(right, bottom, 1, 1,
+ self._corner_delay)
+ self._windows.append(invisible)
+
+ screen = wnck.screen_get_default()
+ screen.connect('window-stacking-changed',
+ self._window_stacking_changed_cb)
+
+ def _create_invisible(self, x, y, width, height, delay):
+ invisible = gtk.Invisible()
+ if delay >= 0:
+ invisible.connect('enter-notify-event', self._enter_notify_cb,
+ delay)
+ invisible.connect('leave-notify-event', self._leave_notify_cb)
+
+ invisible.drag_dest_set(0, [], 0)
+ invisible.connect('drag_motion', self._drag_motion_cb)
+ invisible.connect('drag_leave', self._drag_leave_cb)
+
+ invisible.realize()
+ invisible.window.set_events(gtk.gdk.POINTER_MOTION_MASK |
+ gtk.gdk.ENTER_NOTIFY_MASK |
+ gtk.gdk.LEAVE_NOTIFY_MASK)
+ invisible.window.move_resize(x, y, width, height)
+
+ return invisible
+
+ def _notify_enter(self):
+ if not self._hover:
+ self._hover = True
+ self.emit('enter')
+
+ def _notify_leave(self):
+ if self._hover:
+ self._hover = False
+ self.emit('leave')
+
+ def _enter_notify_cb(self, widget, event, delay):
+ if widget in self._sids:
+ gobject.source_remove(self._sids[widget])
+ self._sids[widget] = gobject.timeout_add(delay,
+ self.__delay_cb,
+ widget)
+
+ def __delay_cb(self, widget):
+ del self._sids[widget]
+ self._notify_enter()
+ return False
+
+ def _leave_notify_cb(self, widget, event):
+ if widget in self._sids:
+ gobject.source_remove(self._sids[widget])
+ del self._sids[widget]
+ self._notify_leave()
+
+ def _drag_motion_cb(self, widget, drag_context, x, y, timestamp):
+ drag_context.drag_status(0, timestamp)
+ self._notify_enter()
+ return True
+
+ def _drag_leave_cb(self, widget, drag_context, timestamp):
+ self._notify_leave()
+ return True
+
+ def show(self):
+ for window in self._windows:
+ window.show()
+
+ def hide(self):
+ for window in self._windows:
+ window.hide()
+
+ def _window_stacking_changed_cb(self, screen):
+ for window in self._windows:
+ window.window.raise_()
diff --git a/shell/src/jarabe/frame/frame.py b/shell/src/jarabe/frame/frame.py
new file mode 100644
index 0000000..7dde55b
--- /dev/null
+++ b/shell/src/jarabe/frame/frame.py
@@ -0,0 +1,351 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import gtk
+import gobject
+import hippo
+
+from sugar.graphics import animator
+from sugar.graphics import style
+from sugar.graphics import palettegroup
+from sugar import profile
+
+from jarabe.frame.eventarea import EventArea
+from jarabe.frame.activitiestray import ActivitiesTray
+from jarabe.frame.zoomtoolbar import ZoomToolbar
+from jarabe.frame.friendstray import FriendsTray
+from jarabe.frame.devicestray import DevicesTray
+from jarabe.frame.framewindow import FrameWindow
+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
+BOTTOM_LEFT = 3
+
+_FRAME_HIDING_DELAY = 500
+_NOTIFICATION_DURATION = 5000
+
+class _Animation(animator.Animation):
+ def __init__(self, frame, end):
+ start = frame.current_position
+ animator.Animation.__init__(self, start, end)
+ self._frame = frame
+
+ def next_frame(self, current):
+ self._frame.move(current)
+
+class _MouseListener(object):
+ def __init__(self, frame):
+ self._frame = frame
+ self._hide_sid = 0
+
+ def mouse_enter(self):
+ self._show_frame()
+
+ def mouse_leave(self):
+ if self._frame.mode == Frame.MODE_MOUSE:
+ self._hide_frame()
+
+ def _show_frame(self):
+ if self._hide_sid != 0:
+ gobject.source_remove(self._hide_sid)
+ self._frame.show(Frame.MODE_MOUSE)
+
+ def _hide_frame_timeout_cb(self):
+ self._frame.hide()
+ return False
+
+ def _hide_frame(self):
+ if self._hide_sid != 0:
+ gobject.source_remove(self._hide_sid)
+ self._hide_sid = gobject.timeout_add(
+ _FRAME_HIDING_DELAY, self._hide_frame_timeout_cb)
+
+class _KeyListener(object):
+ def __init__(self, frame):
+ self._frame = frame
+
+ def key_press(self):
+ if self._frame.visible:
+ if self._frame.mode == Frame.MODE_KEYBOARD:
+ self._frame.hide()
+ else:
+ self._frame.show(Frame.MODE_KEYBOARD)
+
+class Frame(object):
+ MODE_MOUSE = 0
+ MODE_KEYBOARD = 1
+ MODE_NON_INTERACTIVE = 2
+
+ def __init__(self):
+ logging.debug("STARTUP: Loading the frame")
+ self.mode = None
+
+ self._palette_group = palettegroup.get_group('frame')
+ self._palette_group.connect('popdown', self._palette_group_popdown_cb)
+
+ self._left_panel = None
+ self._right_panel = None
+ self._top_panel = None
+ self._bottom_panel = None
+
+ self.current_position = 0.0
+ self._animator = None
+
+ self._event_area = EventArea()
+ self._event_area.connect('enter', self._enter_corner_cb)
+ self._event_area.show()
+
+ self._top_panel = self._create_top_panel()
+ self._bottom_panel = self._create_bottom_panel()
+ self._left_panel = self._create_left_panel()
+ self._right_panel = self._create_right_panel()
+
+ screen = gtk.gdk.screen_get_default()
+ screen.connect('size-changed', self._size_changed_cb)
+
+ self._key_listener = _KeyListener(self)
+ self._mouse_listener = _MouseListener(self)
+
+ self._notif_by_icon = {}
+
+ notification_service = notifications.get_service()
+ notification_service.notification_received.connect(
+ self.__notification_received_cb)
+ notification_service.notification_cancelled.connect(
+ self.__notification_cancelled_cb)
+
+ def is_visible(self):
+ return self.current_position != 0.0
+
+ visible = property(is_visible, None)
+
+ def hide(self):
+ if self._animator:
+ self._animator.stop()
+
+ self._animator = animator.Animator(0.5)
+ self._animator.add(_Animation(self, 0.0))
+ self._animator.start()
+
+ self.mode = None
+
+ def show(self, mode):
+ if self.visible:
+ return
+ if self._animator:
+ self._animator.stop()
+
+ self.mode = mode
+
+ self._animator = animator.Animator(0.5)
+ self._animator.add(_Animation(self, 1.0))
+ self._animator.start()
+
+ def move(self, pos):
+ self.current_position = pos
+ self._update_position()
+
+ def _is_hover(self):
+ return (self._top_panel.hover or \
+ self._bottom_panel.hover or \
+ self._left_panel.hover or \
+ self._right_panel.hover)
+
+ def _create_top_panel(self):
+ panel = self._create_panel(gtk.POS_TOP)
+
+ # TODO: setting box_width and hippo.PACK_EXPAND looks like a hack to me.
+ # Why hippo isn't respecting the request size of these controls?
+
+ zoom_toolbar = ZoomToolbar()
+ panel.append(hippo.CanvasWidget(widget=zoom_toolbar,
+ box_width=4*style.GRID_CELL_SIZE))
+ zoom_toolbar.show()
+
+ activities_tray = ActivitiesTray()
+ panel.append(hippo.CanvasWidget(widget=activities_tray),
+ hippo.PACK_EXPAND)
+ activities_tray.show()
+
+ return panel
+
+ def _create_bottom_panel(self):
+ panel = self._create_panel(gtk.POS_BOTTOM)
+
+ # TODO: same issue as in _create_top_panel()
+ devices_tray = DevicesTray()
+ panel.append(hippo.CanvasWidget(widget=devices_tray), hippo.PACK_EXPAND)
+ devices_tray.show()
+
+ return panel
+
+ def _create_right_panel(self):
+ panel = self._create_panel(gtk.POS_RIGHT)
+
+ tray = FriendsTray()
+ panel.append(hippo.CanvasWidget(widget=tray), hippo.PACK_EXPAND)
+ tray.show()
+
+ return panel
+
+ def _create_left_panel(self):
+ panel = ClipboardPanelWindow(self, gtk.POS_LEFT)
+
+ self._connect_to_panel(panel)
+ panel.connect('drag-motion', self._drag_motion_cb)
+ panel.connect('drag-leave', self._drag_leave_cb)
+
+ return panel
+
+ def _create_panel(self, orientation):
+ panel = FrameWindow(orientation)
+ self._connect_to_panel(panel)
+
+ return panel
+
+ def _move_panel(self, panel, pos, x1, y1, x2, y2):
+ x = (x2 - x1) * pos + x1
+ y = (y2 - y1) * pos + y1
+
+ panel.move(int(x), int(y))
+
+ # FIXME we should hide and show as necessary to free memory
+ if not panel.props.visible:
+ panel.show()
+
+ def _connect_to_panel(self, panel):
+ panel.connect('enter-notify-event', self._enter_notify_cb)
+ panel.connect('leave-notify-event', self._leave_notify_cb)
+
+ def _update_position(self):
+ screen_h = gtk.gdk.screen_height()
+ screen_w = gtk.gdk.screen_width()
+
+ self._move_panel(self._top_panel, self.current_position,
+ 0, - self._top_panel.size, 0, 0)
+
+ self._move_panel(self._bottom_panel, self.current_position,
+ 0, screen_h, 0, screen_h - self._bottom_panel.size)
+
+ self._move_panel(self._left_panel, self.current_position,
+ - self._left_panel.size, 0, 0, 0)
+
+ self._move_panel(self._right_panel, self.current_position,
+ screen_w, 0, screen_w - self._right_panel.size, 0)
+
+ def _size_changed_cb(self, screen):
+ self._update_position()
+
+ def _enter_notify_cb(self, window, event):
+ if event.detail != gtk.gdk.NOTIFY_INFERIOR:
+ self._mouse_listener.mouse_enter()
+
+ def _leave_notify_cb(self, window, event):
+ if event.detail == gtk.gdk.NOTIFY_INFERIOR:
+ return
+
+ if not self._is_hover() and not self._palette_group.is_up():
+ self._mouse_listener.mouse_leave()
+
+ def _palette_group_popdown_cb(self, group):
+ if not self._is_hover():
+ self._mouse_listener.mouse_leave()
+
+ def _drag_motion_cb(self, window, context, x, y, time):
+ self._mouse_listener.mouse_enter()
+
+ def _drag_leave_cb(self, window, drag_context, timestamp):
+ self._mouse_listener.mouse_leave()
+
+ def _enter_corner_cb(self, event_area):
+ self._mouse_listener.mouse_enter()
+
+ def notify_key_press(self):
+ self._key_listener.key_press()
+
+ def add_notification(self, icon, corner=gtk.CORNER_TOP_LEFT,
+ duration=_NOTIFICATION_DURATION):
+
+ if not isinstance(icon, NotificationIcon):
+ raise TypeError('icon must be a NotificationIcon.')
+
+ window = NotificationWindow()
+
+ screen = gtk.gdk.screen_get_default()
+ if corner == gtk.CORNER_TOP_LEFT:
+ window.move(0, 0)
+ elif corner == gtk.CORNER_TOP_RIGHT:
+ window.move(screen.get_width() - style.GRID_CELL_SIZE, 0)
+ elif corner == gtk.CORNER_BOTTOM_LEFT:
+ window.move(0, screen.get_height() - style.GRID_CELL_SIZE)
+ elif corner == gtk.CORNER_BOTTOM_RIGHT:
+ window.move(screen.get_width() - style.GRID_CELL_SIZE,
+ screen.get_height() - style.GRID_CELL_SIZE)
+ else:
+ raise ValueError('Inalid corner: %r' % corner)
+
+ window.add(icon)
+ icon.show()
+ window.show()
+
+ self._notif_by_icon[icon] = window
+
+ gobject.timeout_add(duration,
+ lambda: self.remove_notification(icon))
+
+ def remove_notification(self, icon):
+ if icon not in self._notif_by_icon:
+ logging.debug('icon %r not in list of notifications.', icon)
+ return
+
+ window = self._notif_by_icon[icon]
+ window.destroy()
+ del self._notif_by_icon[icon]
+
+ def __notification_received_cb(self, **kwargs):
+ logging.debug('__notification_received_cb')
+ icon = NotificationIcon()
+
+ hints = kwargs['hints']
+
+ icon_file_name = hints.get('x-sugar-icon-file-name', '')
+ if icon_file_name:
+ icon.props.icon_filename = icon_file_name
+ else:
+ icon.props.icon_name = 'application-octet-stream'
+
+ icon_colors = hints.get('x-sugar-icon-colors', '')
+ if not icon_colors:
+ icon_colors = profile.get_color()
+ icon.props.xo_color = icon_colors
+
+ duration = kwargs.get('expire_timeout', -1)
+ if duration == -1:
+ duration = _NOTIFICATION_DURATION
+
+ self.add_notification(icon, gtk.CORNER_TOP_RIGHT, duration)
+
+ def __notification_cancelled_cb(self, **kwargs):
+ # Do nothing for now. Our notification UI is so simple, there's no
+ # point yet.
+ pass
+
diff --git a/shell/src/jarabe/frame/frameinvoker.py b/shell/src/jarabe/frame/frameinvoker.py
new file mode 100644
index 0000000..e4a13e1
--- /dev/null
+++ b/shell/src/jarabe/frame/frameinvoker.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2007, Eduardo Silva <edsiper@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+
+from sugar.graphics import style
+from sugar.graphics.palette import WidgetInvoker
+
+def _get_screen_area():
+ frame_thickness = style.GRID_CELL_SIZE
+
+ x = y = frame_thickness
+ width = gtk.gdk.screen_width() - frame_thickness
+ height = gtk.gdk.screen_height() - frame_thickness
+
+ return gtk.gdk.Rectangle(x, y, width, height)
+
+class FrameWidgetInvoker(WidgetInvoker):
+ def __init__(self, widget):
+ WidgetInvoker.__init__(self, widget, widget.child)
+
+ self._position_hint = self.ANCHORED
+ self._screen_area = _get_screen_area()
diff --git a/shell/src/jarabe/frame/framewindow.py b/shell/src/jarabe/frame/framewindow.py
new file mode 100644
index 0000000..a7d8fe7
--- /dev/null
+++ b/shell/src/jarabe/frame/framewindow.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import hippo
+
+from sugar.graphics import style
+
+class FrameWindow(gtk.Window):
+ __gtype_name__ = 'SugarFrameWindow'
+
+ def __init__(self, position):
+ gtk.Window.__init__(self)
+ self.hover = False
+ self.size = style.GRID_CELL_SIZE + style.LINE_WIDTH
+
+ accel_group = gtk.AccelGroup()
+ self.set_data('sugar-accel-group', accel_group)
+ self.add_accel_group(accel_group)
+
+ self._position = position
+
+ self.set_decorated(False)
+ self.connect('realize', self._realize_cb)
+ self.connect('enter-notify-event', self._enter_notify_cb)
+ self.connect('leave-notify-event', self._leave_notify_cb)
+
+ self._canvas = hippo.Canvas()
+ self.add(self._canvas)
+ self._canvas.show()
+
+ box = hippo.CanvasBox()
+ self._canvas.set_root(box)
+
+ bg_box = hippo.CanvasBox(
+ border_color=style.COLOR_BUTTON_GREY.get_int())
+ box.append(bg_box, hippo.PACK_EXPAND)
+
+ self._bg = hippo.CanvasBox()
+ bg_box.append(self._bg, hippo.PACK_EXPAND)
+
+ padding = style.GRID_CELL_SIZE
+ border = style.LINE_WIDTH
+
+ if position == gtk.POS_TOP or position == gtk.POS_BOTTOM:
+ box.props.orientation = hippo.ORIENTATION_HORIZONTAL
+ box.props.padding_left = padding
+ box.props.padding_right = padding
+ box.props.padding_top = 0
+ box.props.padding_bottom = 0
+ self._bg.props.orientation = hippo.ORIENTATION_HORIZONTAL
+ self._bg.props.padding_left = border * 2
+ self._bg.props.padding_right = border * 2
+ else:
+ box.props.orientation = hippo.ORIENTATION_VERTICAL
+ box.props.padding_left = 0
+ box.props.padding_right = 0
+ box.props.padding_top = padding
+ box.props.padding_bottom = padding
+ self._bg.props.orientation = hippo.ORIENTATION_VERTICAL
+ self._bg.props.padding_top = border * 2
+ self._bg.props.padding_bottom = border * 2
+
+ if position == gtk.POS_TOP:
+ bg_box.props.orientation = hippo.ORIENTATION_HORIZONTAL
+ bg_box.props.border_bottom = border
+ elif position == gtk.POS_BOTTOM:
+ bg_box.props.orientation = hippo.ORIENTATION_HORIZONTAL
+ bg_box.props.border_top = border
+ elif position == gtk.POS_LEFT:
+ bg_box.props.orientation = hippo.ORIENTATION_VERTICAL
+ bg_box.props.border_right = border
+ elif position == gtk.POS_RIGHT:
+ bg_box.props.orientation = hippo.ORIENTATION_VERTICAL
+ bg_box.props.border_left = border
+
+ self._update_size()
+
+ screen = gtk.gdk.screen_get_default()
+ screen.connect('size-changed', self._size_changed_cb)
+
+ def append(self, child, flags=0):
+ self._bg.append(child, flags)
+
+ def _update_size(self):
+ if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM:
+ self.resize(gtk.gdk.screen_width(), self.size)
+ else:
+ self.resize(self.size, gtk.gdk.screen_height())
+
+ def _realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DOCK)
+ self.window.set_accept_focus(False)
+
+ def _enter_notify_cb(self, window, event):
+ if event.detail != gtk.gdk.NOTIFY_INFERIOR:
+ self.hover = True
+
+ def _leave_notify_cb(self, window, event):
+ if event.detail != gtk.gdk.NOTIFY_INFERIOR:
+ self.hover = False
+
+ def _size_changed_cb(self, screen):
+ self._update_size()
diff --git a/shell/src/jarabe/frame/friendstray.py b/shell/src/jarabe/frame/friendstray.py
new file mode 100644
index 0000000..141505b
--- /dev/null
+++ b/shell/src/jarabe/frame/friendstray.py
@@ -0,0 +1,118 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+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 get_owner_instance
+from jarabe.model import neighborhood
+
+class FriendIcon(TrayIcon):
+ def __init__(self, buddy):
+ TrayIcon.__init__(self, icon_name='computer-xo',
+ xo_color=buddy.get_color())
+
+ self.set_palette_invoker(FrameWidgetInvoker(self))
+ self.palette = BuddyMenu(buddy)
+ self.palette.props.icon_visible = False
+ self.palette.set_group_id('frame')
+
+class FriendsTray(VTray):
+ def __init__(self):
+ VTray.__init__(self)
+
+ self._shared_activity = None
+ self._buddies = {}
+
+ shell.get_model().connect('active-activity-changed',
+ self.__active_activity_changed_cb)
+
+ neighborhood.get_model().connect('activity-added',
+ self.__neighborhood_activity_added_cb)
+
+ def add_buddy(self, buddy):
+ if self._buddies.has_key(buddy.props.key):
+ return
+
+ 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):
+ return
+
+ self.remove_item(self._buddies[buddy.props.key])
+ del self._buddies[buddy.props.key]
+
+ def clear(self):
+ for item in self.get_children():
+ self.remove_item(item)
+ item.destroy()
+ self._buddies = {}
+
+ def __neighborhood_activity_added_cb(self, neighborhood_model,
+ shared_activity):
+ logging.debug('FriendsTray.__neighborhood_activity_added_cb')
+ self.clear()
+
+ # always display ourselves
+ self.add_buddy(get_owner_instance())
+
+ 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(get_owner_instance())
+
+ if home_activity is None:
+ return
+
+ activity_id = home_activity.get_activity_id()
+ if activity_id is None:
+ return
+
+ 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_removed_cb(self, activity, buddy):
+ logging.debug('FriendsTray.__buddy_removed_cb')
+ self.remove_buddy(buddy)
diff --git a/shell/src/jarabe/frame/notification.py b/shell/src/jarabe/frame/notification.py
new file mode 100644
index 0000000..83dc27e
--- /dev/null
+++ b/shell/src/jarabe/frame/notification.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gobject
+import gtk
+
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.view.pulsingicon import PulsingIcon
+
+class NotificationIcon(gtk.EventBox):
+ __gtype_name__ = 'SugarNotificationIcon'
+
+ __gproperties__ = {
+ 'xo-color' : (object, None, None, gobject.PARAM_READWRITE),
+ 'icon-name' : (str, None, None, None, gobject.PARAM_READWRITE),
+ 'icon-filename' : (str, None, None, None, gobject.PARAM_READWRITE)
+ }
+
+ _PULSE_TIMEOUT = 3
+
+ def __init__(self, **kwargs):
+ self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE)
+ gobject.GObject.__init__(self, **kwargs)
+ self.props.visible_window = False
+
+ self._icon.props.pulse_color = \
+ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ self._icon.props.pulsing = True
+ self.add(self._icon)
+ self._icon.show()
+
+ gobject.timeout_add_seconds(self._PULSE_TIMEOUT, self.__stop_pulsing_cb)
+
+ self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)
+
+ def __stop_pulsing_cb(self):
+ self._icon.props.pulsing = False
+ return False
+
+ def do_set_property(self, pspec, value):
+ if pspec.name == 'xo-color':
+ if self._icon.props.base_color != value:
+ self._icon.props.base_color = value
+ elif pspec.name == 'icon-name':
+ if self._icon.props.icon_name != value:
+ self._icon.props.icon_name = value
+ elif pspec.name == 'icon-filename':
+ if self._icon.props.file != value:
+ self._icon.props.file = value
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'xo-color':
+ return self._icon.props.base_color
+ elif pspec.name == 'icon-name':
+ return self._icon.props.icon_name
+ elif pspec.name == 'icon-filename':
+ return self._icon.props.file
+
+ def _set_palette(self, palette):
+ self._icon.palette = palette
+
+ def _get_palette(self):
+ return self._icon.palette
+
+ palette = property(_get_palette, _set_palette)
+
+class NotificationWindow(gtk.Window):
+ __gtype_name__ = 'SugarNotificationWindow'
+
+ def __init__(self, **kwargs):
+
+ gtk.Window.__init__(self, **kwargs)
+
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.connect('realize', self._realize_cb)
+
+ def _realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(False)
+
+ color = gtk.gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html())
+ self.modify_bg(gtk.STATE_NORMAL, color)
+
diff --git a/shell/src/jarabe/frame/zoomtoolbar.py b/shell/src/jarabe/frame/zoomtoolbar.py
new file mode 100644
index 0000000..2ed3c54
--- /dev/null
+++ b/shell/src/jarabe/frame/zoomtoolbar.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2009 Simon Schampijer
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+import logging
+
+import gtk
+
+from sugar.graphics.palette import Palette
+from sugar.graphics.radiotoolbutton import RadioToolButton
+
+from jarabe.frame.frameinvoker import FrameWidgetInvoker
+from jarabe.model import shell
+
+class ZoomToolbar(gtk.Toolbar):
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ # we shouldn't be mirrored in RTL locales
+ self.set_direction(gtk.TEXT_DIR_LTR)
+
+ self._mesh_button = self._add_button('zoom-neighborhood',
+ _('Neighborhood'), _('F1'), shell.ShellModel.ZOOM_MESH)
+ self._groups_button = self._add_button('zoom-groups',
+ _('Group'), _('F2'), shell.ShellModel.ZOOM_GROUP)
+ self._home_button = self._add_button('zoom-home',
+ _('Home'), _('F3'), shell.ShellModel.ZOOM_HOME)
+ self._activity_button = self._add_button('zoom-activity',
+ _('Activity'), _('F4'), shell.ShellModel.ZOOM_ACTIVITY)
+
+ shell_model = shell.get_model()
+ self._set_zoom_level(shell_model.zoom_level)
+ shell_model.zoom_level_changed.connect(self.__zoom_level_changed_cb)
+
+ def _add_button(self, icon_name, label, accelerator, zoom_level):
+ if self.get_children():
+ group = self.get_children()[0]
+ else:
+ group = None
+
+ button = RadioToolButton(named_icon=icon_name, group=group,
+ accelerator=accelerator)
+ button.connect('clicked', self.__level_clicked_cb, zoom_level)
+ self.add(button)
+ button.show()
+
+ palette = Palette(label)
+ palette.props.invoker = FrameWidgetInvoker(button)
+ palette.set_group_id('frame')
+ button.set_palette(palette)
+
+ return button
+
+ def __level_clicked_cb(self, button, level):
+ if not button.get_active():
+ return
+
+ shell.get_model().set_zoom_level(level)
+
+ def __zoom_level_changed_cb(self, **kwargs):
+ self._set_zoom_level(kwargs['new_level'])
+
+ def _set_zoom_level(self, new_level):
+ logging.debug('new zoom level: %r', new_level)
+ if new_level == shell.ShellModel.ZOOM_MESH:
+ self._mesh_button.props.active = True
+ elif new_level == shell.ShellModel.ZOOM_GROUP:
+ self._groups_button.props.active = True
+ elif new_level == shell.ShellModel.ZOOM_HOME:
+ self._home_button.props.active = True
+ elif new_level == shell.ShellModel.ZOOM_ACTIVITY:
+ self._activity_button.props.active = True
+ else:
+ raise ValueError('Invalid zoom level: %r' % (new_level))
+
diff --git a/shell/src/jarabe/intro/Makefile.am b/shell/src/jarabe/intro/Makefile.am
new file mode 100644
index 0000000..a9fb96b
--- /dev/null
+++ b/shell/src/jarabe/intro/Makefile.am
@@ -0,0 +1,9 @@
+imagedir = $(pythondir)/jarabe/intro
+image_DATA = default-picture.png
+
+EXTRA_DIST = $(conf_DATA) $(image_DATA)
+sugardir = $(pythondir)/jarabe/intro
+sugar_PYTHON = \
+ __init__.py \
+ colorpicker.py \
+ window.py
diff --git a/shell/src/jarabe/intro/__init__.py b/shell/src/jarabe/intro/__init__.py
new file mode 100644
index 0000000..ca4f64d
--- /dev/null
+++ b/shell/src/jarabe/intro/__init__.py
@@ -0,0 +1,25 @@
+import os
+
+import gtk
+
+from sugar import env
+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()
+
+ path = os.path.join(os.path.expanduser('~/.sugar'), 'debug')
+ if not os.path.exists(path):
+ profile.create_debug_file()
+
+ path = os.path.join(env.get_profile_path(), 'config')
+ if os.path.exists(path):
+ profile.convert_profile()
+
+ if not profile.is_valid():
+ win = IntroWindow()
+ win.show_all()
+ gtk.main()
diff --git a/shell/src/jarabe/intro/colorpicker.py b/shell/src/jarabe/intro/colorpicker.py
new file mode 100644
index 0000000..a939857
--- /dev/null
+++ b/shell/src/jarabe/intro/colorpicker.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import hippo
+
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor
+
+class ColorPicker(hippo.CanvasBox, hippo.CanvasItem):
+ def __init__(self, **kwargs):
+ hippo.CanvasBox.__init__(self, **kwargs)
+ self.props.orientation = hippo.ORIENTATION_HORIZONTAL
+ self._xo_color = None
+
+ self._xo = CanvasIcon(size=style.XLARGE_ICON_SIZE,
+ icon_name='computer-xo')
+ self._set_random_colors()
+ self._xo.connect('activated', self._xo_activated_cb)
+ self.append(self._xo)
+
+ def _xo_activated_cb(self, item):
+ self._set_random_colors()
+
+ def get_color(self):
+ return self._xo_color
+
+ def _set_random_colors(self):
+ self._xo_color = XoColor()
+ self._xo.props.xo_color = self._xo_color
diff --git a/shell/src/jarabe/intro/default-picture.png b/shell/src/jarabe/intro/default-picture.png
new file mode 100644
index 0000000..e26b9b0
--- /dev/null
+++ b/shell/src/jarabe/intro/default-picture.png
Binary files differ
diff --git a/shell/src/jarabe/intro/window.py b/shell/src/jarabe/intro/window.py
new file mode 100644
index 0000000..35c0cda
--- /dev/null
+++ b/shell/src/jarabe/intro/window.py
@@ -0,0 +1,298 @@
+# Copyright (C) 2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+from gettext import gettext as _
+import gconf
+import pwd
+
+import gtk
+import gobject
+import hippo
+
+from sugar import env
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
+from sugar.graphics.entry import CanvasEntry
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.intro import colorpicker
+
+_BACKGROUND_COLOR = style.COLOR_WHITE
+
+def create_profile(name, color=None, pixbuf=None):
+ if not pixbuf:
+ path = os.path.join(os.path.dirname(__file__), 'default-picture.png')
+ pixbuf = gtk.gdk.pixbuf_new_from_file(path)
+
+ if not color:
+ color = XoColor()
+
+ icon_path = os.path.join(env.get_profile_path(), "buddy-icon.jpg")
+ pixbuf.save(icon_path, "jpeg", {"quality":"85"})
+
+ client = gconf.client_get_default()
+ client.set_string("/desktop/sugar/user/nick", name)
+ client.set_string("/desktop/sugar/user/color", color.to_string())
+
+ # Generate keypair
+ import commands
+ keypath = os.path.join(env.get_profile_path(), "owner.key")
+ if not os.path.isfile(keypath):
+ cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % keypath
+ (s, o) = commands.getstatusoutput(cmd)
+ if s != 0:
+ logging.error("Could not generate key pair: %d %s", s, o)
+ else:
+ logging.error("Keypair exists, skip generation.")
+
+class _Page(hippo.CanvasBox):
+ __gproperties__ = {
+ 'valid' : (bool, None, None, False,
+ gobject.PARAM_READABLE)
+ }
+
+ def __init__(self, **kwargs):
+ hippo.CanvasBox.__init__(self, **kwargs)
+ self.valid = False
+
+ def set_valid(self, valid):
+ self.valid = valid
+ self.notify('valid')
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'valid':
+ return self.valid
+
+ def activate(self):
+ pass
+
+class _NamePage(_Page):
+ def __init__(self, intro):
+ _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER,
+ background_color=_BACKGROUND_COLOR.get_int(),
+ spacing=style.DEFAULT_SPACING,
+ orientation=hippo.ORIENTATION_HORIZONTAL,)
+
+ self._intro = intro
+
+ label = hippo.CanvasText(text=_("Name:"))
+ self.append(label)
+
+ self._entry = CanvasEntry(box_width=style.zoom(300))
+ self._entry.set_background(_BACKGROUND_COLOR.get_html())
+ self._entry.connect('notify::text', self._text_changed_cb)
+
+ widget = self._entry.props.widget
+ widget.set_max_length(45)
+
+ self.append(self._entry)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ self.reverse()
+
+ def _text_changed_cb(self, entry, pspec):
+ valid = len(entry.props.text.strip()) > 0
+ self.set_valid(valid)
+
+ def get_name(self):
+ return self._entry.props.text
+
+ def set_name(self, new_name):
+ self._entry.props.text = new_name
+
+ def activate(self):
+ self._entry.props.widget.grab_focus()
+
+class _ColorPage(_Page):
+ def __init__(self, **kwargs):
+ _Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER,
+ background_color=_BACKGROUND_COLOR.get_int(),
+ spacing=style.DEFAULT_SPACING,
+ yalign=hippo.ALIGNMENT_CENTER, **kwargs)
+
+ self._label = hippo.CanvasText(text=_("Click to change color:"),
+ xalign=hippo.ALIGNMENT_CENTER)
+ self.append(self._label)
+
+ self._cp = colorpicker.ColorPicker(xalign=hippo.ALIGNMENT_CENTER)
+ self.append(self._cp)
+
+ self._color = self._cp.get_color()
+ self.set_valid(True)
+
+ def get_color(self):
+ return self._cp.get_color()
+
+class _IntroBox(hippo.CanvasBox):
+ __gsignals__ = {
+ 'done': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT]))
+ }
+
+ PAGE_NAME = 0
+ PAGE_COLOR = 1
+
+ PAGE_FIRST = PAGE_NAME
+ PAGE_LAST = PAGE_COLOR
+
+ def __init__(self):
+ hippo.CanvasBox.__init__(self, padding=style.zoom(30),
+ background_color=_BACKGROUND_COLOR.get_int())
+
+ self._page = self.PAGE_NAME
+ self._name_page = _NamePage(self)
+ self._color_page = _ColorPage()
+ self._current_page = None
+ self._next_button = None
+
+ client = gconf.client_get_default()
+ default_nick = client.get_string('/desktop/sugar/user/default_nick')
+ if default_nick != 'disabled':
+ 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)
+
+ self._setup_page()
+
+ def _setup_page(self):
+ self.remove_all()
+
+ if self._page == self.PAGE_NAME:
+ self._current_page = self._name_page
+ elif self._page == self.PAGE_COLOR:
+ self._current_page = self._color_page
+
+ self.append(self._current_page, hippo.PACK_EXPAND)
+
+ button_box = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL)
+
+ if self._page != self.PAGE_FIRST:
+ back_button = hippo.CanvasButton(text=_('Back'))
+ image = Icon(icon_name='go-left')
+ back_button.props.widget.set_image(image)
+ back_button.connect('activated', self._back_activated_cb)
+ button_box.append(back_button)
+
+ spacer = hippo.CanvasBox()
+ button_box.append(spacer, hippo.PACK_EXPAND)
+
+ self._next_button = hippo.CanvasButton()
+ image = Icon(icon_name='go-right')
+ self._next_button.props.widget.set_image(image)
+
+ if self._page == self.PAGE_LAST:
+ self._next_button.props.text = _('Done')
+ self._next_button.connect('activated', self._done_activated_cb)
+ else:
+ self._next_button.props.text = _('Next')
+ self._next_button.connect('activated', self._next_activated_cb)
+
+ self._current_page.activate()
+
+ self._update_next_button()
+ button_box.append(self._next_button)
+
+ self._current_page.connect('notify::valid',
+ self._page_valid_changed_cb)
+ self.append(button_box)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ button_box.reverse()
+
+ def _update_next_button(self):
+ widget = self._next_button.props.widget
+ widget.props.sensitive = self._current_page.props.valid
+
+ def _page_valid_changed_cb(self, page, pspec):
+ self._update_next_button()
+
+ def _back_activated_cb(self, item):
+ self.back()
+
+ def back(self):
+ if self._page != self.PAGE_FIRST:
+ self._page -= 1
+ self._setup_page()
+
+ def _next_activated_cb(self, item):
+ self.next()
+
+ def next(self):
+ if self._page == self.PAGE_LAST:
+ self.done()
+ if self._current_page.props.valid:
+ self._page += 1
+ self._setup_page()
+
+ def _done_activated_cb(self, item):
+ self.done()
+
+ def done(self):
+ name = self._name_page.get_name()
+ color = self._color_page.get_color()
+
+ self.emit('done', name, color)
+
+class IntroWindow(gtk.Window):
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ self.props.decorated = False
+ self.maximize()
+
+ self._canvas = hippo.Canvas()
+ self._intro_box = _IntroBox()
+ self._intro_box.connect('done', self._done_cb)
+ self._canvas.set_root(self._intro_box)
+
+ self.add(self._canvas)
+ self._canvas.show()
+ self.connect('key-press-event', self.__key_press_cb)
+
+ def _done_cb(self, box, name, color):
+ self.hide()
+ gobject.idle_add(self._create_profile_cb, name, color)
+
+ def _create_profile_cb(self, name, color):
+ create_profile(name, color)
+ gtk.main_quit()
+
+ return False
+
+ def __key_press_cb(self, widget, event):
+ if gtk.gdk.keyval_name(event.keyval) == "Return":
+ self._intro_box.next()
+ return True
+ elif gtk.gdk.keyval_name(event.keyval) == "Escape":
+ self._intro_box.back()
+ return True
+ return False
+
+
+if __name__ == "__main__":
+ w = IntroWindow()
+ w.show()
+ w.connect('destroy', gtk.main_quit)
+ gtk.main()
diff --git a/shell/src/jarabe/journal/Makefile.am b/shell/src/jarabe/journal/Makefile.am
new file mode 100644
index 0000000..f4bf273
--- /dev/null
+++ b/shell/src/jarabe/journal/Makefile.am
@@ -0,0 +1,17 @@
+sugardir = $(pythondir)/jarabe/journal
+sugar_PYTHON = \
+ __init__.py \
+ detailview.py \
+ expandedentry.py \
+ journalactivity.py \
+ journalentrybundle.py \
+ journaltoolbox.py \
+ keepicon.py \
+ listmodel.py \
+ listview.py \
+ misc.py \
+ modalalert.py \
+ model.py \
+ objectchooser.py \
+ palettes.py \
+ volumestoolbar.py
diff --git a/shell/src/jarabe/journal/__init__.py b/shell/src/jarabe/journal/__init__.py
new file mode 100644
index 0000000..6373228
--- /dev/null
+++ b/shell/src/jarabe/journal/__init__.py
@@ -0,0 +1,15 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
diff --git a/shell/src/jarabe/journal/detailview.py b/shell/src/jarabe/journal/detailview.py
new file mode 100644
index 0000000..b4a2339
--- /dev/null
+++ b/shell/src/jarabe/journal/detailview.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+
+import gobject
+import gtk
+import hippo
+
+from sugar.graphics import style
+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, ([]))
+ }
+
+ def __init__(self, **kwargs):
+ self._metadata = None
+ self._expanded_entry = None
+
+ canvas = hippo.Canvas()
+
+ self._root = hippo.CanvasBox()
+ self._root.props.background_color = style.COLOR_PANEL_GREY.get_int()
+ canvas.set_root(self._root)
+
+ back_bar = BackBar()
+ back_bar.connect('button-release-event',
+ self.__back_bar_release_event_cb)
+ self._root.append(back_bar)
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ self.pack_start(canvas)
+ canvas.show()
+
+ def _fav_icon_activated_cb(self, fav_icon):
+ keep = not self._expanded_entry.get_keep()
+ self._expanded_entry.set_keep(keep)
+ fav_icon.props.keep = keep
+
+ def __back_bar_release_event_cb(self, back_bar, event):
+ self.emit('go-back-clicked')
+ return False
+
+ def _update_view(self):
+ if self._expanded_entry is None:
+ self._expanded_entry = ExpandedEntry()
+ self._root.append(self._expanded_entry, hippo.PACK_EXPAND)
+ self._expanded_entry.set_metadata(self._metadata)
+
+ def refresh(self):
+ logging.debug('DetailView.refresh')
+ self._metadata = model.get(self._metadata['uid'])
+ self._update_view()
+
+ def get_metadata(self):
+ return self._metadata
+
+ def set_metadata(self, metadata):
+ self._metadata = metadata
+ self._update_view()
+
+ metadata = gobject.property(
+ type=object, getter=get_metadata, setter=set_metadata)
+
+class BackBar(hippo.CanvasBox):
+ def __init__(self):
+ hippo.CanvasBox.__init__(self,
+ orientation=hippo.ORIENTATION_HORIZONTAL,
+ border=style.LINE_WIDTH,
+ background_color=style.COLOR_PANEL_GREY.get_int(),
+ border_color=style.COLOR_SELECTION_GREY.get_int(),
+ padding=style.DEFAULT_PADDING,
+ padding_left=style.DEFAULT_SPACING,
+ spacing=style.DEFAULT_SPACING)
+
+ icon = CanvasIcon(icon_name='go-previous',
+ size=style.SMALL_ICON_SIZE,
+ fill_color=style.COLOR_TOOLBAR_GREY.get_svg())
+ self.append(icon)
+
+ label = hippo.CanvasText(text=_('Back'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ self.append(label)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ self.reverse()
+
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ def __motion_notify_event_cb(self, box, event):
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ box.props.background_color = style.COLOR_SELECTION_GREY.get_int()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ box.props.background_color = style.COLOR_PANEL_GREY.get_int()
+ return False
diff --git a/shell/src/jarabe/journal/expandedentry.py b/shell/src/jarabe/journal/expandedentry.py
new file mode 100644
index 0000000..c8e40c1
--- /dev/null
+++ b/shell/src/jarabe/journal/expandedentry.py
@@ -0,0 +1,429 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+import StringIO
+import time
+
+import hippo
+import cairo
+import gobject
+import gtk
+import simplejson
+
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.canvastextview import CanvasTextView
+from sugar.util import format_size
+
+from jarabe.journal.keepicon import KeepIcon
+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,
+ background_color=style.COLOR_PANEL_GREY.get_int())
+
+ if orientation == hippo.ORIENTATION_VERTICAL:
+ self.props.box_width = style.LINE_WIDTH
+ else:
+ self.props.box_height = style.LINE_WIDTH
+
+class BuddyList(hippo.CanvasBox):
+ def __init__(self, buddies):
+ hippo.CanvasBox.__init__(self, xalign=hippo.ALIGNMENT_START,
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+
+ for buddy in buddies:
+ nick_, color = buddy
+ hbox = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL)
+ icon = CanvasIcon(icon_name='computer-xo',
+ xo_color=XoColor(color),
+ size=style.STANDARD_ICON_SIZE)
+ icon.set_palette(BuddyPalette(buddy))
+ hbox.append(icon)
+ self.append(hbox)
+
+class ExpandedEntry(hippo.CanvasBox):
+ def __init__(self):
+ hippo.CanvasBox.__init__(self)
+ self.props.orientation = hippo.ORIENTATION_VERTICAL
+ self.props.background_color = style.COLOR_WHITE.get_int()
+ self.props.padding_top = style.DEFAULT_SPACING * 3
+
+ self._metadata = None
+ self._update_title_sid = None
+
+ # Create header
+ header = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL,
+ padding=style.DEFAULT_PADDING,
+ padding_right=style.GRID_CELL_SIZE,
+ spacing=style.DEFAULT_SPACING)
+ self.append(header)
+
+ # Create two column body
+
+ body = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL,
+ spacing=style.DEFAULT_SPACING * 3,
+ padding_left=style.GRID_CELL_SIZE,
+ padding_right=style.GRID_CELL_SIZE,
+ padding_top=style.DEFAULT_SPACING * 3)
+
+ self.append(body, hippo.PACK_EXPAND)
+
+ first_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ spacing=style.DEFAULT_SPACING)
+ body.append(first_column)
+
+ second_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ spacing=style.DEFAULT_SPACING)
+ body.append(second_column, hippo.PACK_EXPAND)
+
+ # Header
+
+ self._keep_icon = self._create_keep_icon()
+ header.append(self._keep_icon)
+
+ self._icon = None
+ self._icon_box = hippo.CanvasBox()
+ header.append(self._icon_box)
+
+ self._title = self._create_title()
+ header.append(self._title, hippo.PACK_EXPAND)
+
+ # TODO: create a version list popup instead of a date label
+ self._date = self._create_date()
+ header.append(self._date)
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ header.reverse()
+
+ # First column
+
+ self._preview_box = hippo.CanvasBox()
+ first_column.append(self._preview_box)
+
+ self._technical_box = hippo.CanvasBox()
+ first_column.append(self._technical_box)
+
+ # Second column
+
+ description_box, self._description = self._create_description()
+ second_column.append(description_box)
+
+ tags_box, self._tags = self._create_tags()
+ second_column.append(tags_box)
+
+ self._buddy_list = hippo.CanvasBox()
+ second_column.append(self._buddy_list)
+
+ def set_metadata(self, metadata):
+ if self._metadata == metadata:
+ return
+ self._metadata = metadata
+
+ self._keep_icon.keep = (int(metadata.get('keep', 0)) == 1)
+
+ self._icon = self._create_icon()
+ self._icon_box.clear()
+ self._icon_box.append(self._icon)
+
+ self._date.props.text = misc.get_date(metadata)
+
+ title = self._title.props.widget
+ title.props.text = metadata.get('title', _('Untitled'))
+ title.props.editable = model.is_editable(metadata)
+
+ self._preview_box.clear()
+ self._preview_box.append(self._create_preview())
+
+ self._technical_box.clear()
+ self._technical_box.append(self._create_technical())
+
+ self._buddy_list.clear()
+ self._buddy_list.append(self._create_buddy_list())
+
+ description = self._description.text_view_widget
+ description.props.buffer.props.text = metadata.get('description', '')
+ description.props.editable = model.is_editable(metadata)
+
+ tags = self._tags.text_view_widget
+ tags.props.buffer.props.text = metadata.get('tags', '')
+ tags.props.editable = model.is_editable(metadata)
+
+ def _create_keep_icon(self):
+ keep_icon = KeepIcon(False)
+ keep_icon.connect('activated', self._keep_icon_activated_cb)
+ return keep_icon
+
+ def _create_icon(self):
+ icon = CanvasIcon(file_name=misc.get_icon_name(self._metadata))
+ icon.connect_after('button-release-event',
+ self._icon_button_release_event_cb)
+
+ if misc.is_activity_bundle(self._metadata):
+ xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ else:
+ xo_color = misc.get_icon_color(self._metadata)
+ icon.props.xo_color = xo_color
+
+ icon.set_palette(ObjectPalette(self._metadata))
+
+ return icon
+
+ def _create_title(self):
+ entry = gtk.Entry()
+ entry.connect('focus-out-event', self._title_focus_out_event_cb)
+
+ bg_color = style.COLOR_WHITE.get_gdk_color()
+ entry.modify_bg(gtk.STATE_INSENSITIVE, bg_color)
+ entry.modify_base(gtk.STATE_INSENSITIVE, bg_color)
+
+ return hippo.CanvasWidget(widget=entry)
+
+ def _create_date(self):
+ date = hippo.CanvasText(xalign=hippo.ALIGNMENT_START,
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ return date
+
+ def _create_preview(self):
+ width = style.zoom(320)
+ height = style.zoom(240)
+ box = hippo.CanvasBox()
+
+ if self._metadata.has_key('preview') and \
+ len(self._metadata['preview']) > 4:
+
+ if self._metadata['preview'][1:4] == 'PNG':
+ preview_data = self._metadata['preview']
+ else:
+ # TODO: We are close to be able to drop this.
+ import base64
+ preview_data = base64.b64decode(
+ self._metadata['preview'])
+
+ png_file = StringIO.StringIO(preview_data)
+ try:
+ surface = cairo.ImageSurface.create_from_png(png_file)
+ has_preview = True
+ except Exception:
+ logging.exception('Error while loading the preview')
+ has_preview = False
+ else:
+ has_preview = False
+
+ if has_preview:
+ preview_box = hippo.CanvasImage(image=surface,
+ border=style.LINE_WIDTH,
+ border_color=style.COLOR_BUTTON_GREY.get_int(),
+ xalign=hippo.ALIGNMENT_CENTER,
+ yalign=hippo.ALIGNMENT_CENTER,
+ scale_width=width,
+ scale_height=height)
+ else:
+ preview_box = hippo.CanvasText(text=_('No preview'),
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ xalign=hippo.ALIGNMENT_CENTER,
+ yalign=hippo.ALIGNMENT_CENTER,
+ border=style.LINE_WIDTH,
+ border_color=style.COLOR_BUTTON_GREY.get_int(),
+ color=style.COLOR_BUTTON_GREY.get_int(),
+ box_width=width,
+ box_height=height)
+ preview_box.connect_after('button-release-event',
+ self._preview_box_button_release_event_cb)
+ box.append(preview_box)
+ return box
+
+ def _create_technical(self):
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ 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'])),)]
+
+ for line in lines:
+ text = hippo.CanvasText(text=line,
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ return vbox
+
+ 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')
+
+ def _create_buddy_list(self):
+
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Participants:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ if self._metadata.has_key('buddies') and \
+ self._metadata['buddies']:
+ buddies = simplejson.loads(self._metadata['buddies']).values()
+ vbox.append(BuddyList(buddies))
+ return vbox
+ else:
+ return vbox
+
+ def _create_description(self):
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Description:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ text_view = CanvasTextView('',
+ box_height=style.GRID_CELL_SIZE * 2)
+ vbox.append(text_view, hippo.PACK_EXPAND)
+
+ text_view.text_view_widget.props.accepts_tab = False
+ text_view.text_view_widget.connect('focus-out-event',
+ self._description_focus_out_event_cb)
+
+ return vbox, text_view
+
+ def _create_tags(self):
+ vbox = hippo.CanvasBox()
+ vbox.props.spacing = style.DEFAULT_SPACING
+
+ text = hippo.CanvasText(text=_('Tags:'),
+ font_desc=style.FONT_NORMAL.get_pango_desc())
+ text.props.color = style.COLOR_BUTTON_GREY.get_int()
+
+ if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL:
+ text.props.xalign = hippo.ALIGNMENT_END
+ else:
+ text.props.xalign = hippo.ALIGNMENT_START
+
+ vbox.append(text)
+
+ text_view = CanvasTextView('',
+ box_height=style.GRID_CELL_SIZE * 2)
+ vbox.append(text_view, hippo.PACK_EXPAND)
+
+ text_view.text_view_widget.props.accepts_tab = False
+ text_view.text_view_widget.connect('focus-out-event',
+ self._tags_focus_out_event_cb)
+
+ return vbox, text_view
+
+ def _title_notify_text_cb(self, entry, pspec):
+ if not self._update_title_sid:
+ self._update_title_sid = gobject.timeout_add_seconds(1,
+ self._update_title_cb)
+
+ def _title_focus_out_event_cb(self, entry, event):
+ self._update_entry()
+
+ def _description_focus_out_event_cb(self, text_view, event):
+ self._update_entry()
+
+ def _tags_focus_out_event_cb(self, text_view, event):
+ self._update_entry()
+
+ def _update_entry(self):
+ if not model.is_editable(self._metadata):
+ return
+
+ needs_update = False
+
+ old_title = self._metadata.get('title', None)
+ new_title = self._title.props.widget.props.text
+ if old_title != new_title:
+ self._icon.palette.props.primary_text = new_title
+ self._metadata['title'] = new_title
+ self._metadata['title_set_by_user'] = '1'
+ needs_update = True
+
+ old_tags = self._metadata.get('tags', None)
+ new_tags = self._tags.text_view_widget.props.buffer.props.text
+ if old_tags != new_tags:
+ self._metadata['tags'] = new_tags
+ needs_update = True
+
+ old_description = self._metadata.get('description', None)
+ new_description = \
+ self._description.text_view_widget.props.buffer.props.text
+ if old_description != new_description:
+ self._metadata['description'] = new_description
+ needs_update = True
+
+ if needs_update:
+ model.write(self._metadata, update_mtime=False)
+
+ self._update_title_sid = None
+
+ def get_keep(self):
+ return int(self._metadata.get('keep', 0)) == 1
+
+ def _keep_icon_activated_cb(self, keep_icon):
+ if not model.is_editable(self._metadata):
+ return
+ if self.get_keep():
+ self._metadata['keep'] = 0
+ else:
+ self._metadata['keep'] = 1
+ model.write(self._metadata, update_mtime=False)
+
+ keep_icon.props.keep = self.get_keep()
+
+ def _icon_button_release_event_cb(self, button, event):
+ logging.debug('_icon_button_release_event_cb')
+ misc.resume(self._metadata)
+ return True
+
+ def _preview_box_button_release_event_cb(self, button, event):
+ logging.debug('_preview_box_button_release_event_cb')
+ misc.resume(self._metadata)
+ return True
diff --git a/shell/src/jarabe/journal/journalactivity.py b/shell/src/jarabe/journal/journalactivity.py
new file mode 100644
index 0000000..e278420
--- /dev/null
+++ b/shell/src/jarabe/journal/journalactivity.py
@@ -0,0 +1,371 @@
+# Copyright (C) 2006, Red Hat, Inc.
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+import sys
+import traceback
+import uuid
+
+import gtk
+import dbus
+import statvfs
+import os
+
+from sugar.graphics.window import Window
+from sugar.graphics.alert import ErrorAlert
+
+from sugar.bundle.bundle import ZipExtractException, RegistrationException
+from sugar import env
+from sugar.activity import activityfactory
+from sugar import wm
+
+from jarabe.model import bundleregistry
+from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox
+from jarabe.journal.listview import ListView
+from jarabe.journal.detailview import DetailView
+from jarabe.journal.volumestoolbar import VolumesToolbar
+from jarabe.journal import misc
+from jarabe.journal.journalentrybundle import JournalEntryBundle
+from jarabe.journal.objectchooser import ObjectChooser
+from jarabe.journal.modalalert import ModalAlert
+from jarabe.journal import model
+
+J_DBUS_SERVICE = 'org.laptop.Journal'
+J_DBUS_INTERFACE = 'org.laptop.Journal'
+J_DBUS_PATH = '/org/laptop/Journal'
+
+_SPACE_TRESHOLD = 52428800
+_BUNDLE_ID = 'org.laptop.JournalActivity'
+
+class JournalActivityDBusService(dbus.service.Object):
+ def __init__(self, parent):
+ self._parent = parent
+ session_bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(J_DBUS_SERVICE,
+ bus=session_bus, replace_existing=False, allow_replacement=False)
+ logging.debug('bus_name: %r', bus_name)
+ dbus.service.Object.__init__(self, bus_name, J_DBUS_PATH)
+
+ @dbus.service.method(J_DBUS_INTERFACE,
+ in_signature='s', out_signature='')
+ def ShowObject(self, object_id):
+ """Pop-up journal and show object with object_id"""
+
+ logging.debug('Trying to show object %s', object_id)
+
+ if self._parent.show_object(object_id):
+ self._parent.reveal()
+
+ def _chooser_response_cb(self, chooser, response_id, chooser_id):
+ logging.debug('JournalActivityDBusService._chooser_response_cb')
+ if response_id == gtk.RESPONSE_ACCEPT:
+ object_id = chooser.get_selected_object_id()
+ self.ObjectChooserResponse(chooser_id, object_id)
+ else:
+ self.ObjectChooserCancelled(chooser_id)
+ chooser.destroy()
+ del chooser
+
+ @dbus.service.method(J_DBUS_INTERFACE, in_signature='is', out_signature='s')
+ def ChooseObject(self, parent_xid, what_filter=''):
+ chooser_id = uuid.uuid4().hex
+ if parent_xid > 0:
+ parent = gtk.gdk.window_foreign_new(parent_xid)
+ else:
+ parent = None
+ chooser = ObjectChooser(parent, what_filter)
+ chooser.connect('response', self._chooser_response_cb, chooser_id)
+ chooser.show()
+
+ return chooser_id
+
+ @dbus.service.signal(J_DBUS_INTERFACE, signature="ss")
+ def ObjectChooserResponse(self, chooser_id, object_id):
+ pass
+
+ @dbus.service.signal(J_DBUS_INTERFACE, signature="s")
+ def ObjectChooserCancelled(self, chooser_id):
+ pass
+
+class JournalActivity(Window):
+ def __init__(self):
+ logging.debug("STARTUP: Loading the journal")
+ Window.__init__(self)
+
+ self.set_title(_('Journal'))
+
+ self._main_view = None
+ self._secondary_view = None
+ self._list_view = None
+ self._detail_view = None
+ self._main_toolbox = None
+ self._detail_toolbox = None
+ self._volumes_toolbar = None
+
+ self._setup_main_view()
+ self._setup_secondary_view()
+
+ self.add_events(gtk.gdk.ALL_EVENTS_MASK |
+ gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self._realized_sid = self.connect('realize', self.__realize_cb)
+ self.connect('visibility-notify-event',
+ self.__visibility_notify_event_cb)
+ self.connect('window-state-event', self.__window_state_event_cb)
+ self.connect('key-press-event', self._key_press_event_cb)
+ self.connect('focus-in-event', self._focus_in_event_cb)
+
+ model.created.connect(self.__model_created_cb)
+ model.updated.connect(self.__model_updated_cb)
+ model.deleted.connect(self.__model_deleted_cb)
+
+ self._dbus_service = JournalActivityDBusService(self)
+
+ self.iconify()
+
+ self._critical_space_alert = None
+ self._check_available_space()
+
+ def __alert_notify_cb(self, gobject, strerror, severity):
+ alert = ErrorAlert(title=severity, msg=strerror)
+ 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()
+ wm.set_activity_id(window.window, str(activity_id))
+ self.disconnect(self._realized_sid)
+ self._realized_sid = None
+
+ def can_close(self):
+ return False
+
+ def _setup_main_view(self):
+ self._main_toolbox = MainToolbox()
+ self._main_view = gtk.VBox()
+
+ self._list_view = ListView()
+ self._list_view.connect('detail-clicked', self.__detail_clicked_cb)
+ self._list_view.connect('clear-clicked', self.__clear_clicked_cb)
+ self._main_view.pack_start(self._list_view)
+ self._list_view.show()
+
+ self._volumes_toolbar = VolumesToolbar()
+ self._volumes_toolbar.connect('volume-changed',
+ self.__volume_changed_cb)
+ self._volumes_toolbar.connect('volume-error', self.__alert_notify_cb)
+ self._main_view.pack_start(self._volumes_toolbar, expand=False)
+
+ search_toolbar = self._main_toolbox.search_toolbar
+ search_toolbar.connect('query-changed', self._query_changed_cb)
+ search_toolbar.set_mount_point('/')
+
+ def _setup_secondary_view(self):
+ self._secondary_view = gtk.VBox()
+
+ self._detail_toolbox = DetailToolbox()
+ entry_toolbar = self._detail_toolbox.entry_toolbar
+
+ self._detail_view = DetailView()
+ self._detail_view.connect('go-back-clicked', self.__go_back_clicked_cb)
+ self._secondary_view.pack_end(self._detail_view)
+ self._detail_view.show()
+
+ def _key_press_event_cb(self, widget, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'Escape':
+ self.show_main_view()
+
+ def __detail_clicked_cb(self, list_view, object_id):
+ self._show_secondary_view(object_id)
+
+ def __clear_clicked_cb(self, list_view):
+ self._main_toolbox.search_toolbar.clear_query()
+
+ def __go_back_clicked_cb(self, detail_view):
+ self.show_main_view()
+
+ def _query_changed_cb(self, toolbar, query):
+ self._list_view.update_with_query(query)
+ self.show_main_view()
+
+ def show_main_view(self):
+ if self.toolbar_box != self._main_toolbox:
+ self.set_toolbar_box(self._main_toolbox)
+ self._main_toolbox.show()
+
+ if self.canvas != self._main_view:
+ self.set_canvas(self._main_view)
+ self._main_view.show()
+
+ def _show_secondary_view(self, object_id):
+ metadata = model.get(object_id)
+ 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())))
+
+ self.set_toolbar_box(self._detail_toolbox)
+ self._detail_toolbox.show()
+
+ try:
+ self._detail_view.props.metadata = metadata
+ except Exception:
+ logging.error('Exception while displaying entry:\n' + \
+ ''.join(traceback.format_exception(*sys.exc_info())))
+
+ self.set_canvas(self._secondary_view)
+ self._secondary_view.show()
+
+ def show_object(self, object_id):
+ metadata = model.get(object_id)
+ if metadata is None:
+ return False
+ else:
+ self._show_secondary_view(object_id)
+ return True
+
+ def __volume_changed_cb(self, volume_toolbar, mount_point):
+ logging.debug('Selected volume: %r.', mount_point)
+ self._main_toolbox.search_toolbar.set_mount_point(mount_point)
+ self._main_toolbox.set_current_toolbar(0)
+
+ def __model_created_cb(self, sender, **kwargs):
+ self._check_for_bundle(kwargs['object_id'])
+ self._main_toolbox.search_toolbar.refresh_filters()
+ self._check_available_space()
+
+ def __model_updated_cb(self, sender, **kwargs):
+ self._check_for_bundle(kwargs['object_id'])
+
+ if self.canvas == self._secondary_view and \
+ kwargs['object_id'] == self._detail_view.props.metadata['uid']:
+ self._detail_view.refresh()
+
+ self._check_available_space()
+
+ def __model_deleted_cb(self, sender, **kwargs):
+ if self.canvas == self._secondary_view and \
+ kwargs['object_id'] == self._detail_view.props.metadata['uid']:
+ self.show_main_view()
+
+ def _focus_in_event_cb(self, window, event):
+ self.search_grab_focus()
+ self._list_view.update_dates()
+
+ def _check_for_bundle(self, object_id):
+ registry = bundleregistry.get_registry()
+
+ metadata = model.get(object_id)
+ if metadata.get('progress', '').isdigit():
+ if int(metadata['progress']) < 100:
+ return
+
+ bundle = misc.get_bundle(metadata)
+ if bundle is None:
+ return
+
+ if registry.is_installed(bundle):
+ logging.debug('_check_for_bundle bundle already installed')
+ return
+
+ if metadata['mime_type'] == JournalEntryBundle.MIME_TYPE:
+ # JournalEntryBundle code takes over the datastore entry and
+ # transforms it into the journal entry from the bundle -- we have
+ # nothing more to do.
+ try:
+ registry.install(bundle, metadata['uid'])
+ except (ZipExtractException, RegistrationException):
+ logging.exception('Could not install bundle %s',
+ bundle.get_path())
+ return
+
+ try:
+ registry.install(bundle)
+ except (ZipExtractException, RegistrationException):
+ logging.exception('Could not install bundle %s', bundle.get_path())
+ return
+
+ metadata['bundle_id'] = bundle.get_bundle_id()
+ model.write(metadata)
+
+ def search_grab_focus(self):
+ search_toolbar = self._main_toolbox.search_toolbar
+ search_toolbar.give_entry_focus()
+
+ def __window_state_event_cb(self, window, event):
+ logging.debug('window_state_event_cb %r', self)
+ if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED:
+ state = event.new_window_state
+ visible = not state & gtk.gdk.WINDOW_STATE_ICONIFIED
+ self._list_view.set_is_visible(visible)
+
+ def __visibility_notify_event_cb(self, window, event):
+ logging.debug('visibility_notify_event_cb %r', self)
+ visible = event.state != gtk.gdk.VISIBILITY_FULLY_OBSCURED
+ self._list_view.set_is_visible(visible)
+
+ def _check_available_space(self):
+ ''' Check available space on device
+
+ If the available space is below 50MB an alert will be
+ shown which encourages to delete old journal entries.
+ '''
+
+ if self._critical_space_alert:
+ return
+ stat = os.statvfs(env.get_profile_path())
+ free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL]
+ if free_space < _SPACE_TRESHOLD:
+ self._critical_space_alert = ModalAlert()
+ self._critical_space_alert.connect('destroy',
+ self.__alert_closed_cb)
+ self._critical_space_alert.show()
+
+ def __alert_closed_cb(self, data):
+ self.show_main_view()
+ self.reveal()
+ self._critical_space_alert = None
+
+ def set_active_volume(self, mount):
+ self._volumes_toolbar.set_active_volume(mount)
+
+ def focus_search(self):
+ """Become visible and give focus to the search entry
+ """
+ self.reveal()
+ self.show_main_view()
+ self.search_grab_focus()
+
+_journal = None
+
+def get_journal():
+ global _journal
+ if _journal is None:
+ _journal = JournalActivity()
+ _journal.show()
+ return _journal
+
+def start():
+ get_journal()
+
diff --git a/shell/src/jarabe/journal/journalentrybundle.py b/shell/src/jarabe/journal/journalentrybundle.py
new file mode 100644
index 0000000..41777c7
--- /dev/null
+++ b/shell/src/jarabe/journal/journalentrybundle.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import tempfile
+import shutil
+
+import simplejson
+import dbus
+
+from sugar.bundle.bundle import Bundle, MalformedBundleException
+
+from jarabe.journal import model
+
+class JournalEntryBundle(Bundle):
+ """A Journal entry bundle
+
+ See http://wiki.laptop.org/go/Journal_entry_bundles for details
+ """
+
+ MIME_TYPE = 'application/vnd.olpc-journal-entry'
+
+ _zipped_extension = '.xoj'
+ _unzipped_extension = None
+ _infodir = None
+
+ def __init__(self, path):
+ Bundle.__init__(self, path)
+
+ def install(self, uid=''):
+ if os.environ.has_key('SUGAR_ACTIVITY_ROOT'):
+ install_dir = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'],
+ 'data')
+ else:
+ install_dir = tempfile.gettempdir()
+ bundle_dir = os.path.join(install_dir, self._zip_root_dir)
+ temp_uid = self._zip_root_dir
+ self._unzip(install_dir)
+ try:
+ metadata = self._read_metadata(bundle_dir)
+ metadata['uid'] = uid
+
+ preview = self._read_preview(temp_uid, bundle_dir)
+ if preview is not None:
+ metadata['preview'] = dbus.ByteArray(preview)
+
+ file_path = os.path.join(bundle_dir, temp_uid)
+ model.write(metadata, file_path)
+ finally:
+ shutil.rmtree(bundle_dir, ignore_errors=True)
+
+ def get_bundle_id(self):
+ return None
+
+ def _read_metadata(self, bundle_dir):
+ metadata_path = os.path.join(bundle_dir, '_metadata.json')
+ if not os.path.exists(metadata_path):
+ raise MalformedBundleException(
+ 'Bundle must contain the file "_metadata.json"')
+ f = open(metadata_path, 'r')
+ try:
+ json_data = f.read()
+ finally:
+ f.close()
+ return simplejson.loads(json_data)
+
+ def _read_preview(self, uid, bundle_dir):
+ preview_path = os.path.join(bundle_dir, 'preview', uid)
+ if not os.path.exists(preview_path):
+ return ''
+ f = open(preview_path, 'r')
+ try:
+ preview_data = f.read()
+ finally:
+ f.close()
+ return preview_data
+
+ def is_installed(self):
+ # These bundles can be reinstalled as many times as desired.
+ return False
+
diff --git a/shell/src/jarabe/journal/journaltoolbox.py b/shell/src/jarabe/journal/journaltoolbox.py
new file mode 100644
index 0000000..61671bc
--- /dev/null
+++ b/shell/src/jarabe/journal/journaltoolbox.py
@@ -0,0 +1,458 @@
+# Copyright (C) 2007, One Laptop Per Child
+# Copyright (C) 2009, Walter Bender
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+import logging
+from datetime import datetime, timedelta
+import os
+import gconf
+import time
+
+import gobject
+import gio
+import gtk
+
+from sugar.graphics.toolbox import Toolbox
+from sugar.graphics.toolcombobox import ToolComboBox
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.toggletoolbutton import ToggleToolButton
+from sugar.graphics.combobox import ComboBox
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics import iconentry
+from sugar.graphics import style
+from sugar import mime
+from sugar import profile
+
+from jarabe.model import bundleregistry
+from jarabe.journal import misc
+from jarabe.journal import model
+
+_AUTOSEARCH_TIMEOUT = 1000
+
+_ACTION_ANYTIME = 0
+_ACTION_TODAY = 1
+_ACTION_SINCE_YESTERDAY = 2
+_ACTION_PAST_WEEK = 3
+_ACTION_PAST_MONTH = 4
+_ACTION_PAST_YEAR = 5
+
+_ACTION_ANYTHING = 0
+
+_ACTION_EVERYBODY = 0
+_ACTION_MY_FRIENDS = 1
+_ACTION_MY_CLASS = 2
+
+
+class MainToolbox(Toolbox):
+ def __init__(self):
+ Toolbox.__init__(self)
+
+ self.search_toolbar = SearchToolbar()
+ self.search_toolbar.set_size_request(-1, style.GRID_CELL_SIZE)
+ self.add_toolbar(_('Search'), self.search_toolbar)
+ self.search_toolbar.show()
+
+class SearchToolbar(gtk.Toolbar):
+ __gtype_name__ = 'SearchToolbar'
+
+ __gsignals__ = {
+ 'query-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._mount_point = None
+
+ self._search_entry = iconentry.IconEntry()
+ self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
+ 'system-search')
+ self._search_entry.connect('activate', self._search_entry_activated_cb)
+ self._search_entry.connect('changed', self._search_entry_changed_cb)
+ self._search_entry.add_clear_button()
+ self._autosearch_timer = None
+ self._add_widget(self._search_entry, expand=True)
+
+ self._favorite_button = ToggleToolButton('emblem-favorite')
+ self._favorite_button.connect('toggled',
+ self.__favorite_button_toggled_cb)
+ self.insert(self._favorite_button, -1)
+ self._favorite_button.show()
+
+ self._what_search_combo = ComboBox()
+ self._what_combo_changed_sid = self._what_search_combo.connect(
+ 'changed', self._combo_changed_cb)
+ tool_item = ToolComboBox(self._what_search_combo)
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ self._when_search_combo = self._get_when_search_combo()
+ tool_item = ToolComboBox(self._when_search_combo)
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ # TODO: enable it when the DS supports saving the buddies.
+ #self._with_search_combo = self._get_with_search_combo()
+ #tool_item = ToolComboBox(self._with_search_combo)
+ #self.insert(tool_item, -1)
+ #tool_item.show()
+
+ self._query = self._build_query()
+
+ self.refresh_filters()
+
+ def give_entry_focus(self):
+ self._search_entry.grab_focus()
+
+ def _get_when_search_combo(self):
+ when_search = ComboBox()
+ when_search.append_item(_ACTION_ANYTIME, _('Anytime'))
+ when_search.append_separator()
+ when_search.append_item(_ACTION_TODAY, _('Today'))
+ when_search.append_item(_ACTION_SINCE_YESTERDAY,
+ _('Since yesterday'))
+ # TRANS: Filter entries modified during the last 7 days.
+ when_search.append_item(_ACTION_PAST_WEEK, _('Past week'))
+ # TRANS: Filter entries modified during the last 30 days.
+ when_search.append_item(_ACTION_PAST_MONTH, _('Past month'))
+ # TRANS: Filter entries modified during the last 356 days.
+ when_search.append_item(_ACTION_PAST_YEAR, _('Past year'))
+ when_search.set_active(0)
+ when_search.connect('changed', self._combo_changed_cb)
+ return when_search
+
+ def _get_with_search_combo(self):
+ with_search = ComboBox()
+ with_search.append_item(_ACTION_EVERYBODY, _('Anyone'))
+ with_search.append_separator()
+ with_search.append_item(_ACTION_MY_FRIENDS, _('My friends'))
+ with_search.append_item(_ACTION_MY_CLASS, _('My class'))
+ with_search.append_separator()
+
+ # TODO: Ask the model for buddies.
+ with_search.append_item(3, 'Dan', 'theme:xo')
+
+ with_search.set_active(0)
+ with_search.connect('changed', self._combo_changed_cb)
+ return with_search
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ def _build_query(self):
+ query = {}
+
+ if self._mount_point:
+ query['mountpoints'] = [self._mount_point]
+
+ if self._favorite_button.props.active:
+ query['keep'] = 1
+
+ if self._what_search_combo.props.value:
+ value = self._what_search_combo.props.value
+ generic_type = mime.get_generic_type(value)
+ if generic_type:
+ mime_types = generic_type.mime_types
+ query['mime_type'] = mime_types
+ else:
+ query['activity'] = self._what_search_combo.props.value
+
+ if self._when_search_combo.props.value:
+ date_from, date_to = self._get_date_range()
+ query['timestamp'] = {'start': date_from, 'end': date_to}
+
+ if self._search_entry.props.text:
+ text = self._search_entry.props.text.strip()
+ if text:
+ query['query'] = text
+
+ return query
+
+ def _get_date_range(self):
+ today_start = datetime.today().replace(hour=0, minute=0, second=0)
+ right_now = datetime.today()
+ if self._when_search_combo.props.value == _ACTION_TODAY:
+ date_range = (today_start, right_now)
+ elif self._when_search_combo.props.value == _ACTION_SINCE_YESTERDAY:
+ date_range = (today_start - timedelta(1), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_WEEK:
+ date_range = (today_start - timedelta(7), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_MONTH:
+ date_range = (today_start - timedelta(30), right_now)
+ elif self._when_search_combo.props.value == _ACTION_PAST_YEAR:
+ date_range = (today_start - timedelta(356), right_now)
+
+ return (time.mktime(date_range[0].timetuple()),
+ time.mktime(date_range[1].timetuple()))
+
+ def _combo_changed_cb(self, combo):
+ self._update_if_needed()
+
+ def _update_if_needed(self):
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def _search_entry_activated_cb(self, search_entry):
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def _search_entry_changed_cb(self, search_entry):
+ if not search_entry.props.text:
+ search_entry.activate()
+ return
+
+ if self._autosearch_timer:
+ gobject.source_remove(self._autosearch_timer)
+ self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT,
+ self._autosearch_timer_cb)
+
+ def _autosearch_timer_cb(self):
+ logging.debug('_autosearch_timer_cb')
+ self._autosearch_timer = None
+ self._search_entry.activate()
+ return False
+
+ def set_mount_point(self, mount_point):
+ self._mount_point = mount_point
+ new_query = self._build_query()
+ if self._query != new_query:
+ self._query = new_query
+ self.emit('query-changed', self._query)
+
+ def set_what_filter(self, what_filter):
+ combo_model = self._what_search_combo.get_model()
+ what_filter_index = -1
+ for i in range(0, len(combo_model) - 1):
+ if combo_model[i][0] == what_filter:
+ what_filter_index = i
+ break
+
+ if what_filter_index == -1:
+ logging.warning('what_filter %r not known', what_filter)
+ else:
+ self._what_search_combo.set_active(what_filter_index)
+
+ def refresh_filters(self):
+ current_value = self._what_search_combo.props.value
+ current_value_index = 0
+
+ self._what_search_combo.handler_block(self._what_combo_changed_sid)
+ try:
+ self._what_search_combo.remove_all()
+ # TRANS: Item in a combo box that filters by entry type.
+ self._what_search_combo.append_item(_ACTION_ANYTHING,
+ _('Anything'))
+
+ registry = bundleregistry.get_registry()
+ appended_separator = False
+
+ types = mime.get_all_generic_types()
+ for generic_type in types:
+ if not appended_separator:
+ self._what_search_combo.append_separator()
+ appended_separator = True
+ self._what_search_combo.append_item(
+ generic_type.type_id, generic_type.name, generic_type.icon)
+ if generic_type.type_id == current_value:
+ current_value_index = \
+ len(self._what_search_combo.get_model()) - 1
+
+ self._what_search_combo.set_active(current_value_index)
+
+ self._what_search_combo.append_separator()
+
+ for service_name in model.get_unique_values('activity'):
+ activity_info = registry.get_bundle(service_name)
+ if not activity_info is None:
+ if os.path.exists(activity_info.get_icon()):
+ self._what_search_combo.append_item(service_name,
+ activity_info.get_name(),
+ file_name=activity_info.get_icon())
+ else:
+ self._what_search_combo.append_item(service_name,
+ activity_info.get_name(),
+ icon_name='application-octet-stream')
+
+ if service_name == current_value:
+ current_value_index = \
+ len(self._what_search_combo.get_model()) - 1
+ finally:
+ self._what_search_combo.handler_unblock(
+ self._what_combo_changed_sid)
+
+ def __favorite_button_toggled_cb(self, favorite_button):
+ self._update_if_needed()
+
+ def clear_query(self):
+ self._search_entry.props.text = ''
+ self._what_search_combo.set_active(0)
+ self._when_search_combo.set_active(0)
+ self._favorite_button.props.active = False
+
+class DetailToolbox(Toolbox):
+ def __init__(self):
+ Toolbox.__init__(self)
+
+ self.entry_toolbar = EntryToolbar()
+ self.add_toolbar('', self.entry_toolbar)
+ self.entry_toolbar.show()
+
+class EntryToolbar(gtk.Toolbar):
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+
+ self._metadata = None
+ self._temp_file_path = None
+
+ self._resume = ToolButton('activity-start')
+ self._resume.connect('clicked', self._resume_clicked_cb)
+ self.add(self._resume)
+ self._resume.show()
+
+ self._copy = ToolButton()
+
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ icon = Icon(icon_name='edit-copy', xo_color=color)
+ self._copy.set_icon_widget(icon)
+ icon.show()
+
+ self._copy.set_tooltip(_('Copy'))
+ self._copy.connect('clicked', self._copy_clicked_cb)
+ self.add(self._copy)
+ self._copy.show()
+
+ separator = gtk.SeparatorToolItem()
+ self.add(separator)
+ separator.show()
+
+ erase_button = ToolButton('list-remove')
+ erase_button.set_tooltip(_('Erase'))
+ erase_button.connect('clicked', self._erase_button_clicked_cb)
+ self.add(erase_button)
+ erase_button.show()
+
+ def set_metadata(self, metadata):
+ self._metadata = metadata
+ self._refresh_copy_palette()
+ self._refresh_resume_palette()
+
+ def _resume_clicked_cb(self, button):
+ misc.resume(self._metadata)
+
+ def _copy_clicked_cb(self, button):
+ clipboard = gtk.Clipboard()
+ clipboard.set_with_data([('text/uri-list', 0, 0)],
+ self.__clipboard_get_func_cb,
+ self.__clipboard_clear_func_cb)
+
+ def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ # Get hold of a reference so the temp file doesn't get deleted
+ self._temp_file_path = model.get_file(self._metadata['uid'])
+ selection_data.set_uris(['file://' + self._temp_file_path])
+
+ def __clipboard_clear_func_cb(self, clipboard, data):
+ # Release and delete the temp file
+ self._temp_file_path = None
+
+ def _erase_button_clicked_cb(self, button):
+ 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 _resume_menu_item_activate_cb(self, menu_item, service_name):
+ misc.resume(self._metadata, service_name)
+
+ def _copy_menu_item_activate_cb(self, menu_item, mount_point):
+ model.copy(self._metadata, mount_point)
+
+ def _refresh_copy_palette(self):
+ palette = self._copy.get_palette()
+
+ for menu_item in palette.menu.get_children():
+ palette.menu.remove(menu_item)
+ menu_item.destroy()
+
+ if self._metadata['mountpoint'] != '/':
+ journal_item = MenuItem(_('Journal'))
+ journal_item.set_image(Icon(
+ icon_name='activity-journal',
+ xo_color=profile.get_color(),
+ icon_size=gtk.ICON_SIZE_MENU))
+ journal_item.connect('activate',
+ self._copy_menu_item_activate_cb, '/')
+ journal_item.show()
+ palette.menu.append(journal_item)
+
+ volume_monitor = gio.volume_monitor_get()
+ for mount in volume_monitor.get_mounts():
+ if self._metadata['mountpoint'] == mount.get_root().get_path():
+ continue
+ menu_item = MenuItem(mount.get_name())
+
+ # TODO: fallback to the more generic icons when needed
+ menu_item.set_image(Icon(icon_name=mount.get_icon().props.names[0],
+ icon_size=gtk.ICON_SIZE_MENU))
+
+ menu_item.connect('activate',
+ self._copy_menu_item_activate_cb,
+ mount.get_root().get_path())
+ palette.menu.append(menu_item)
+ menu_item.show()
+
+ def _refresh_resume_palette(self):
+ if self._metadata.get('activity_id', ''):
+ # TRANS: Action label for resuming an activity.
+ self._resume.set_tooltip(_('Resume'))
+ else:
+ # TRANS: Action label for starting an entry.
+ self._resume.set_tooltip(_('Start'))
+
+ palette = self._resume.get_palette()
+
+ for menu_item in palette.menu.get_children():
+ palette.menu.remove(menu_item)
+ menu_item.destroy()
+
+ for activity_info in misc.get_activities(self._metadata):
+ menu_item = MenuItem(activity_info.get_name())
+ menu_item.set_image(Icon(file=activity_info.get_icon(),
+ icon_size=gtk.ICON_SIZE_MENU))
+ menu_item.connect('activate', self._resume_menu_item_activate_cb,
+ activity_info.get_bundle_id())
+ palette.menu.append(menu_item)
+ menu_item.show()
diff --git a/shell/src/jarabe/journal/keepicon.py b/shell/src/jarabe/journal/keepicon.py
new file mode 100644
index 0000000..2c692c6
--- /dev/null
+++ b/shell/src/jarabe/journal/keepicon.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2006, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gobject
+import hippo
+import gconf
+
+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',
+ box_width=style.GRID_CELL_SIZE * 3 / 5,
+ size=style.SMALL_ICON_SIZE)
+ self.connect('motion-notify-event', self.__motion_notify_event_cb)
+
+ self._keep = None
+ self.set_keep(keep)
+
+ def set_keep(self, keep):
+ if keep == self._keep:
+ return
+
+ self._keep = keep
+ if keep:
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ self.props.xo_color = color
+ else:
+ self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg()
+ self.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
+
+ def get_keep(self):
+ return self._keep
+
+ keep = gobject.property(type=int, default=0, getter=get_keep,
+ setter=set_keep)
+
+ def __motion_notify_event_cb(self, icon, event):
+ if not self._keep:
+ if event.detail == hippo.MOTION_DETAIL_ENTER:
+ icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg()
+ elif event.detail == hippo.MOTION_DETAIL_LEAVE:
+ icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
diff --git a/shell/src/jarabe/journal/listmodel.py b/shell/src/jarabe/journal/listmodel.py
new file mode 100644
index 0000000..07f8544
--- /dev/null
+++ b/shell/src/jarabe/journal/listmodel.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2009, Tomeu Vizoso
+#
+# 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 simplejson
+import gobject
+import gtk
+
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics import style
+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,
+ ([])),
+ }
+
+ COLUMN_UID = 0
+ COLUMN_FAVORITE = 1
+ 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}
+
+ _PAGE_SIZE = 10
+
+ def __init__(self, query):
+ gobject.GObject.__init__(self)
+
+ self._last_requested_index = None
+ self._cached_row = None
+ self._result_set = model.find(query, ListModel._PAGE_SIZE)
+ self._temp_drag_file_path = None
+
+ # HACK: The view will tell us that it is resizing so the model can
+ # avoid hitting D-Bus and disk.
+ self.view_is_resizing = False
+
+ self._result_set.ready.connect(self.__result_set_ready_cb)
+ self._result_set.progress.connect(self.__result_set_progress_cb)
+
+ def __result_set_ready_cb(self, **kwargs):
+ self.emit('ready')
+
+ def __result_set_progress_cb(self, **kwargs):
+ self.emit('progress')
+
+ def setup(self):
+ self._result_set.setup()
+
+ def stop(self):
+ self._result_set.stop()
+
+ def get_metadata(self, path):
+ return model.get(self[path][ListModel.COLUMN_UID])
+
+ def on_get_n_columns(self):
+ return len(ListModel._COLUMN_TYPES)
+
+ def on_get_column_type(self, index):
+ return ListModel._COLUMN_TYPES[index]
+
+ def on_iter_n_children(self, iterator):
+ if iterator == None:
+ return self._result_set.length
+ else:
+ return 0
+
+ def on_get_value(self, index, column):
+ if self.view_is_resizing:
+ return None
+
+ if index == self._last_requested_index:
+ return self._cached_row[column]
+
+ if index >= self._result_set.length:
+ return None
+
+ self._result_set.seek(index)
+ metadata = self._result_set.read()
+
+ self._last_requested_index = index
+ self._cached_row = []
+ self._cached_row.append(metadata['uid'])
+ self._cached_row.append(metadata.get('keep', '0') == '1')
+ self._cached_row.append(misc.get_icon_name(metadata))
+
+ if misc.is_activity_bundle(metadata):
+ xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ else:
+ 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)
+
+ timestamp = int(metadata.get('timestamp', 0))
+ self._cached_row.append(util.timestamp_to_elapsed_string(timestamp))
+
+ self._cached_row.append(int(metadata.get('progress', 100)))
+
+ if metadata.get('buddies', ''):
+ buddies = simplejson.loads(metadata['buddies']).values()
+ else:
+ 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)
+
+ return self._cached_row[column]
+
+ def on_iter_nth_child(self, iterator, n):
+ return n
+
+ def on_get_path(self, iterator):
+ return (iterator)
+
+ def on_get_iter(self, path):
+ return path[0]
+
+ def on_iter_next(self, iterator):
+ if iterator != None:
+ if iterator >= self._result_set.length - 1:
+ return None
+ return iterator + 1
+ return None
+
+ def on_get_flags(self):
+ return gtk.TREE_MODEL_ITERS_PERSIST | gtk.TREE_MODEL_LIST_ONLY
+
+ def on_iter_children(self, iterator):
+ return None
+
+ def on_iter_has_child(self, iterator):
+ return False
+
+ def on_iter_parent(self, iterator):
+ return None
+
+ def do_drag_data_get(self, path, selection):
+ uid = self[path][ListModel.COLUMN_UID]
+ if selection.target == 'text/uri-list':
+ # Get hold of a reference so the temp file doesn't get deleted
+ self._temp_drag_file_path = model.get_file(uid)
+ logging.debug('putting %r in selection', self._temp_drag_file_path)
+ selection.set(selection.target, 8, self._temp_drag_file_path)
+ return True
+ elif selection.target == 'journal-object-id':
+ selection.set(selection.target, 8, uid)
+ return True
+
+ return False
+
diff --git a/shell/src/jarabe/journal/listview.py b/shell/src/jarabe/journal/listview.py
new file mode 100644
index 0000000..9e19f70
--- /dev/null
+++ b/shell/src/jarabe/journal/listview.py
@@ -0,0 +1,641 @@
+# Copyright (C) 2009, Tomeu Vizoso
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+import time
+
+import gobject
+import gtk
+import hippo
+import gconf
+import pango
+
+from sugar.graphics import style
+from sugar.graphics.icon import CanvasIcon, Icon, CellRendererIcon
+from sugar.graphics.xocolor import XoColor
+from sugar import util
+
+from jarabe.journal.listmodel import ListModel
+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'
+
+ def __init__(self):
+ gtk.TreeView.__init__(self)
+ 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.
+ tree_model = self.get_model()
+ if tree_model is not None:
+ tree_model.view_is_resizing = True
+ try:
+ gtk.TreeView.do_size_request(self, requisition)
+ finally:
+ 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,
+ ([]))
+ }
+
+ def __init__(self):
+ self._query = {}
+ self._model = None
+ self._progress_bar = None
+ self._last_progress_bar_pulse = None
+ self._scroll_position = 0.
+
+ gobject.GObject.__init__(self)
+
+ self.connect('map', self.__map_cb)
+ self.connect('unrealize', self.__unrealize_cb)
+ self.connect('destroy', self.__destroy_cb)
+
+ self._scrolled_window = gtk.ScrolledWindow()
+ self._scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ self.add(self._scrolled_window)
+ self._scrolled_window.show()
+
+ self.tree_view = TreeView()
+ selection = self.tree_view.get_selection()
+ selection.set_mode(gtk.SELECTION_NONE)
+ self.tree_view.props.fixed_height_mode = True
+ self.tree_view.modify_base(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._scrolled_window.add(self.tree_view)
+ self.tree_view.show()
+
+ self.cell_title = None
+ self.cell_icon = None
+ self._title_column = None
+ self.date_column = None
+ self._add_columns()
+
+ self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
+ [('text/uri-list', 0, 0),
+ ('journal-object-id', 0, 0)],
+ gtk.gdk.ACTION_COPY)
+
+ # Auto-update stuff
+ self._fully_obscured = True
+ self._dirty = False
+ self._refresh_idle_handler = None
+ self._update_dates_timer = None
+
+ model.created.connect(self.__model_created_cb)
+ model.updated.connect(self.__model_updated_cb)
+ model.deleted.connect(self.__model_deleted_cb)
+
+ def __model_created_cb(self, sender, **kwargs):
+ self._set_dirty()
+
+ def __model_updated_cb(self, sender, **kwargs):
+ self._set_dirty()
+
+ def __model_deleted_cb(self, sender, **kwargs):
+ self._set_dirty()
+
+ def _add_columns(self):
+ cell_favorite = CellRendererFavorite(self.tree_view)
+ cell_favorite.connect('clicked', self.__favorite_clicked_cb)
+
+ column = gtk.TreeViewColumn()
+ column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
+ column.props.fixed_width = cell_favorite.props.width
+ column.pack_start(cell_favorite)
+ column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb)
+ self.tree_view.append_column(column)
+
+ self.cell_icon = CellRendererActivityIcon(self.tree_view)
+
+ column = gtk.TreeViewColumn()
+ 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, 'xo-color',
+ ListModel.COLUMN_ICON_COLOR)
+ self.tree_view.append_column(column)
+
+ self.cell_title = gtk.CellRendererText()
+ self.cell_title.props.ellipsize = pango.ELLIPSIZE_MIDDLE
+ self.cell_title.props.ellipsize_set = True
+
+ self._title_column = gtk.TreeViewColumn()
+ self._title_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
+ self._title_column.props.expand = True
+ self._title_column.props.clickable = True
+ self._title_column.pack_start(self.cell_title)
+ self._title_column.add_attribute(self.cell_title, 'markup',
+ ListModel.COLUMN_TITLE)
+ self.tree_view.append_column(self._title_column)
+
+ buddies_column = gtk.TreeViewColumn()
+ 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,
+ ListModel.COLUMN_BUDDY_3]:
+ cell_icon = CellRendererBuddy(self.tree_view,
+ column_index=column_index)
+ buddies_column.pack_start(cell_icon)
+ buddies_column.props.fixed_width += cell_icon.props.width
+ buddies_column.add_attribute(cell_icon, 'buddy', column_index)
+ buddies_column.set_cell_data_func(cell_icon,
+ self.__buddies_set_data_cb)
+
+ cell_progress = gtk.CellRendererProgress()
+ cell_progress.props.ypad = style.GRID_CELL_SIZE / 4
+ buddies_column.pack_start(cell_progress)
+ buddies_column.add_attribute(cell_progress, 'value',
+ ListModel.COLUMN_PROGRESS)
+ buddies_column.set_cell_data_func(cell_progress,
+ self.__progress_data_cb)
+
+ cell_text = gtk.CellRendererText()
+ cell_text.props.xalign = 1
+
+ # Measure the required width for a date in the form of "10 hours, 10
+ # minutes ago"
+ timestamp = time.time() - 10 * 60 - 10 * 60 * 60
+ 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)
+
+ def _get_width_for_string(self, text):
+ # Add some extra margin
+ text = text + 'aaaaa'
+
+ widget = gtk.Label('')
+ context = widget.get_pango_context()
+ layout = pango.Layout(context)
+ layout.set_text(text)
+ width, height_ = layout.get_size()
+ return pango.PIXELS(width)
+
+ def do_size_allocate(self, allocation):
+ self.allocation = allocation
+ self.child.size_allocate(allocation)
+
+ def do_size_request(self, requisition):
+ requisition.width, requisition.height = self.child.size_request()
+
+ def __destroy_cb(self, widget):
+ if self._model is not None:
+ self._model.stop()
+
+ def __buddies_set_data_cb(self, column, cell, tree_model, tree_iter):
+ progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
+ cell.props.visible = progress >= 100
+
+ def __progress_data_cb(self, column, cell, tree_model, tree_iter):
+ progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS]
+ cell.props.visible = progress < 100
+
+ def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter):
+ favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
+ if favorite:
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ cell.props.xo_color = color
+ else:
+ cell.props.xo_color = None
+
+ def __favorite_clicked_cb(self, cell, path):
+ row = self._model[path]
+ metadata = model.get(row[ListModel.COLUMN_UID])
+ if not model.is_editable(metadata):
+ return
+ if metadata.get('keep', 0) == '1':
+ metadata['keep'] = '0'
+ else:
+ metadata['keep'] = '1'
+ model.write(metadata, update_mtime=False)
+
+ def update_with_query(self, query_dict):
+ logging.debug('ListView.update_with_query')
+ self._query = query_dict
+
+ if 'order_by' not in self._query:
+ self._query['order_by'] = ['+timestamp']
+
+ self.refresh()
+
+ def refresh(self):
+ logging.debug('ListView.refresh query %r', self._query)
+ self._stop_progress_bar()
+
+ if self._model is not None:
+ self._model.stop()
+ self._dirty = False
+
+ self._model = ListModel(self._query)
+ self._model.connect('ready', self.__model_ready_cb)
+ self._model.connect('progress', self.__model_progress_cb)
+ self._model.setup()
+
+ def __model_ready_cb(self, tree_model):
+ self._stop_progress_bar()
+
+ self._scroll_position = self.tree_view.props.vadjustment.props.value
+ logging.debug('ListView.__model_ready_cb %r', self._scroll_position)
+
+ if self.tree_view.window is not None:
+ # prevent glitches while later vadjustment setting, see #1235
+ self.tree_view.get_bin_window().hide()
+
+ # Cannot set it up earlier because will try to access the model
+ # and it needs to be ready.
+ self.tree_view.set_model(self._model)
+
+ self.tree_view.props.vadjustment.props.value = self._scroll_position
+ self.tree_view.props.vadjustment.value_changed()
+
+ if self.tree_view.window is not None:
+ # prevent glitches while later vadjustment setting, see #1235
+ self.tree_view.get_bin_window().show()
+
+ if len(tree_model) == 0:
+ if self._is_query_empty():
+ self._show_message(MESSAGE_EMPTY_JOURNAL)
+ else:
+ self._show_message(MESSAGE_NO_MATCH)
+ else:
+ self._clear_message()
+
+ def __map_cb(self, widget):
+ logging.debug('ListView.__map_cb %r', self._scroll_position)
+ self.tree_view.props.vadjustment.props.value = self._scroll_position
+ self.tree_view.props.vadjustment.value_changed()
+
+ def __unrealize_cb(self, widget):
+ self._scroll_position = self.tree_view.props.vadjustment.props.value
+ logging.debug('ListView.__map_cb %r', self._scroll_position)
+
+ is_editable = self._query.get('mountpoints', '') == '/'
+ self.cell_title.props.editable = is_editable
+
+ 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
+
+ def __model_progress_cb(self, tree_model):
+ if self._progress_bar is None:
+ self._start_progress_bar()
+
+ if time.time() - self._last_progress_bar_pulse > 0.05:
+ self._progress_bar.pulse()
+ self._last_progress_bar_pulse = time.time()
+
+ def _start_progress_bar(self):
+ alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5)
+ self.remove(self.child)
+ self.add(alignment)
+ alignment.show()
+
+ self._progress_bar = gtk.ProgressBar()
+ self._progress_bar.props.pulse_step = 0.01
+ self._last_progress_bar_pulse = time.time()
+ alignment.add(self._progress_bar)
+ self._progress_bar.show()
+
+ def _stop_progress_bar(self):
+ if self._progress_bar is None:
+ return
+ self.remove(self.child)
+ self.add(self._scrolled_window)
+ self._progress_bar = None
+
+ def _show_message(self, message):
+ canvas = hippo.Canvas()
+ self.remove(self.child)
+ self.add(canvas)
+ canvas.show()
+
+ box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL,
+ background_color=style.COLOR_WHITE.get_int(),
+ yalign=hippo.ALIGNMENT_CENTER,
+ spacing=style.DEFAULT_SPACING,
+ padding_bottom=style.GRID_CELL_SIZE)
+ canvas.set_root(box)
+
+ 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())
+ box.append(icon)
+
+ if message == MESSAGE_EMPTY_JOURNAL:
+ text = _('Your Journal is empty')
+ elif message == MESSAGE_NO_MATCH:
+ text = _('No matching entries')
+ else:
+ raise ValueError('Invalid message')
+
+ text = hippo.CanvasText(text=text,
+ xalign=hippo.ALIGNMENT_CENTER,
+ font_desc=style.FONT_BOLD.get_pango_desc(),
+ color = style.COLOR_BUTTON_GREY.get_int())
+ box.append(text)
+
+ if message == MESSAGE_NO_MATCH:
+ button = gtk.Button(label=_('Clear search'))
+ button.connect('clicked', self.__clear_button_clicked_cb)
+ button.props.image = Icon(icon_name='dialog-cancel',
+ icon_size=gtk.ICON_SIZE_BUTTON)
+ canvas_button = hippo.CanvasWidget(widget=button,
+ xalign=hippo.ALIGNMENT_CENTER)
+ box.append(canvas_button)
+
+ def __clear_button_clicked_cb(self, button):
+ self.emit('clear-clicked')
+
+ def _clear_message(self):
+ if self.child == self._scrolled_window:
+ return
+ self.remove(self.child)
+ self.add(self._scrolled_window)
+ self._scrolled_window.show()
+
+ def update_dates(self):
+ if not self.tree_view.flags() & gtk.REALIZED:
+ return
+ visible_range = self.tree_view.get_visible_range()
+ if visible_range is None:
+ return
+
+ logging.debug('ListView.update_dates')
+
+ path, end_path = visible_range
+ tree_model = self.tree_view.get_model()
+
+ while True:
+ x, y, width, height = self.tree_view.get_cell_area(path,
+ self.date_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:
+ break
+ else:
+ next_iter = tree_model.iter_next(tree_model.get_iter(path))
+ path = tree_model.get_path(next_iter)
+
+ def _set_dirty(self):
+ if self._fully_obscured:
+ self._dirty = True
+ else:
+ self.refresh()
+
+ def set_is_visible(self, visible):
+ if visible != self._fully_obscured:
+ return
+
+ logging.debug('canvas_visibility_notify_event_cb %r', visible)
+ if visible:
+ self._fully_obscured = False
+ if self._dirty:
+ self.refresh()
+ if self._update_dates_timer is None:
+ logging.debug('Adding date updating timer')
+ self._update_dates_timer = \
+ gobject.timeout_add_seconds(UPDATE_INTERVAL,
+ self.__update_dates_timer_cb)
+ else:
+ self._fully_obscured = True
+ if self._update_dates_timer is not None:
+ logging.debug('Remove date updating timer')
+ gobject.source_remove(self._update_dates_timer)
+ self._update_dates_timer = None
+
+ def __update_dates_timer_cb(self):
+ self.update_dates()
+ return True
+
+class ListView(BaseListView):
+ __gtype_name__ = 'JournalListView'
+
+ __gsignals__ = {
+ 'detail-clicked': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([object]))
+ }
+
+ def __init__(self):
+ BaseListView.__init__(self)
+ self._is_dragging = False
+
+ self.tree_view.connect('drag-begin', self.__drag_begin_cb)
+ self.tree_view.connect('button-release-event',
+ self.__button_release_event_cb)
+
+ self.cell_title.connect('edited', self.__cell_title_edited_cb)
+ self.cell_title.connect('editing-canceled', self.__editing_canceled_cb)
+
+ self.cell_icon.connect('clicked', self.__icon_clicked_cb)
+ self.cell_icon.connect('detail-clicked', self.__detail_clicked_cb)
+
+ cell_detail = CellRendererDetail(self.tree_view)
+ cell_detail.connect('clicked', self.__detail_cell_clicked_cb)
+
+ column = gtk.TreeViewColumn()
+ column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
+ column.props.fixed_width = cell_detail.props.width
+ column.pack_start(cell_detail)
+ self.tree_view.append_column(column)
+
+ def __drag_begin_cb(self, widget, drag_context):
+ self._is_dragging = True
+
+ def __button_release_event_cb(self, tree_view, event):
+ try:
+ if self._is_dragging:
+ return
+ finally:
+ self._is_dragging = False
+
+ pos = tree_view.get_path_at_pos(int(event.x), int(event.y))
+ if pos is None:
+ return
+
+ path, column, x_, y_ = pos
+ if column != self._title_column:
+ return
+
+ row = self.tree_view.get_model()[path]
+ metadata = model.get(row[ListModel.COLUMN_UID])
+ self.cell_title.props.editable = model.is_editable(metadata)
+
+ tree_view.set_cursor_on_cell(path, column, start_editing=True)
+
+ def __detail_cell_clicked_cb(self, cell, path):
+ row = self.tree_view.get_model()[path]
+ self.emit('detail-clicked', row[ListModel.COLUMN_UID])
+
+ def __detail_clicked_cb(self, cell, uid):
+ self.emit('detail-clicked', uid)
+
+ def __icon_clicked_cb(self, cell, path):
+ row = self.tree_view.get_model()[path]
+ metadata = model.get(row[ListModel.COLUMN_UID])
+ misc.resume(metadata)
+
+ def __cell_title_edited_cb(self, cell, path, new_text):
+ row = self._model[path]
+ metadata = model.get(row[ListModel.COLUMN_UID])
+ metadata['title'] = new_text
+ model.write(metadata, update_mtime=False)
+ self.cell_title.props.editable = False
+
+ def __editing_canceled_cb(self, cell):
+ self.cell_title.props.editable = False
+
+class CellRendererFavorite(CellRendererIcon):
+ __gtype_name__ = 'JournalCellRendererFavorite'
+
+ def __init__(self, tree_view):
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.SMALL_ICON_SIZE
+ self.props.icon_name = 'emblem-favorite'
+ self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE
+ 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'
+
+ def __init__(self, tree_view):
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.SMALL_ICON_SIZE
+ self.props.icon_name = 'go-right'
+ self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE
+ self.props.stroke_color = style.COLOR_TRANSPARENT.get_svg()
+ self.props.fill_color = style.COLOR_BUTTON_GREY.get_svg()
+ 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,
+ ([str])),
+ }
+
+ def __init__(self, tree_view):
+ self._show_palette = True
+
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.STANDARD_ICON_SIZE
+ self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE
+
+ self.tree_view = tree_view
+
+ def create_palette(self):
+ if not self._show_palette:
+ return None
+
+ tree_model = self.tree_view.get_model()
+ metadata = tree_model.get_metadata(self.props.palette_invoker.path)
+
+ palette = ObjectPalette(metadata, detail=True)
+ palette.connect('detail-clicked',
+ self.__detail_clicked_cb)
+ return palette
+
+ def __detail_clicked_cb(self, palette, uid):
+ self.emit('detail-clicked', uid)
+
+ def set_show_palette(self, show_palette):
+ self._show_palette = show_palette
+
+ show_palette = gobject.property(type=bool, default=True,
+ setter=set_show_palette)
+
+class CellRendererBuddy(CellRendererIcon):
+ __gtype_name__ = 'JournalCellRendererBuddy'
+
+ def __init__(self, tree_view, column_index):
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.STANDARD_ICON_SIZE
+ self.props.height = style.STANDARD_ICON_SIZE
+ self.props.size = style.STANDARD_ICON_SIZE
+ self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE
+
+ self.tree_view = tree_view
+ self._model_column_index = column_index
+
+ def create_palette(self):
+ tree_model = self.tree_view.get_model()
+ row = tree_model[self.props.palette_invoker.path]
+
+ if row[self._model_column_index] is not None:
+ nick, xo_color = row[self._model_column_index]
+ return BuddyPalette((nick, xo_color.to_string()))
+ else:
+ return None
+
+ def set_buddy(self, buddy):
+ if buddy is None:
+ self.props.icon_name = None
+ else:
+ nick_, xo_color = buddy
+ self.props.icon_name = 'computer-xo'
+ self.props.xo_color = xo_color
+
+ buddy = gobject.property(type=object, setter=set_buddy)
+
diff --git a/shell/src/jarabe/journal/misc.py b/shell/src/jarabe/journal/misc.py
new file mode 100644
index 0000000..657b60e
--- /dev/null
+++ b/shell/src/jarabe/journal/misc.py
@@ -0,0 +1,262 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import time
+import os
+from gettext import gettext as _
+
+import gio
+import gconf
+import gtk
+
+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 import mime
+from sugar.bundle.activitybundle import ActivityBundle
+from sugar.bundle.contentbundle import ContentBundle
+from sugar import util
+
+from jarabe.view import launcher
+from jarabe.model import bundleregistry, shell
+from jarabe.journal.journalentrybundle import JournalEntryBundle
+from jarabe.journal import model
+
+def _get_icon_for_mime(mime_type):
+ generic_types = mime.get_all_generic_types()
+ for generic_type in generic_types:
+ if mime_type in generic_type.mime_types:
+ file_name = get_icon_file_name(generic_type.icon)
+ if file_name is not None:
+ return file_name
+
+ icons = gio.content_type_get_icon(mime_type)
+ logging.debug('icons for this file: %r', icons.props.names)
+ for icon_name in icons.props.names:
+ file_name = get_icon_file_name(icon_name)
+ if file_name is not None:
+ return file_name
+
+def get_icon_name(metadata):
+ file_name = None
+
+ bundle_id = metadata.get('activity', '')
+ if not bundle_id:
+ bundle_id = metadata.get('bundle_id', '')
+
+ if bundle_id:
+ activity_info = bundleregistry.get_registry().get_bundle(bundle_id)
+ if activity_info:
+ file_name = activity_info.get_icon()
+
+ if file_name is None and is_activity_bundle(metadata):
+ file_path = model.get_file(metadata['uid'])
+ if file_path is not None and os.path.exists(file_path):
+ try:
+ bundle = ActivityBundle(file_path)
+ file_name = bundle.get_icon()
+ except Exception:
+ logging.exception('Could not read bundle')
+
+ if file_name is None:
+ file_name = _get_icon_for_mime(metadata.get('mime_type', ''))
+
+ if file_name is None:
+ file_name = get_icon_file_name('application-octet-stream')
+
+ 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')
+
+def get_bundle(metadata):
+ try:
+ if is_activity_bundle(metadata):
+ file_path = util.TempFilePath(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']))
+ 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']))
+ if not os.path.exists(file_path):
+ logging.warning('Invalid path: %r', file_path)
+ return None
+ return JournalEntryBundle(file_path)
+ else:
+ return None
+ except Exception:
+ 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)
+ if not result:
+ for parent_mime in mime.get_mime_parents(mime_type):
+ for activity in registry.get_activities_for_type(parent_mime):
+ if activity not in result:
+ result.append(activity)
+ return result
+
+def get_activities(metadata):
+ activities = []
+
+ bundle_id = metadata.get('activity', '')
+ if bundle_id:
+ activity_info = bundleregistry.get_registry().get_bundle(bundle_id)
+ if activity_info:
+ activities.append(activity_info)
+
+ mime_type = metadata.get('mime_type', '')
+ if mime_type:
+ activities_info = _get_activities_for_mime(mime_type)
+ for activity_info in activities_info:
+ if activity_info not in activities:
+ activities.append(activity_info)
+
+ return activities
+
+def resume(metadata, bundle_id=None):
+ registry = bundleregistry.get_registry()
+
+ if is_activity_bundle(metadata) and bundle_id is None:
+
+ logging.debug('Creating activity bundle')
+
+ file_path = model.get_file(metadata['uid'])
+ bundle = ActivityBundle(file_path)
+ if not registry.is_installed(bundle):
+ logging.debug('Installing activity bundle')
+ registry.install(bundle)
+ 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:
+ launch(installed_bundle)
+ else:
+ logging.error('Bundle %r is not installed.',
+ bundle.get_bundle_id())
+
+ elif is_content_bundle(metadata) and bundle_id is None:
+
+ logging.debug('Creating content bundle')
+
+ file_path = model.get_file(metadata['uid'])
+ bundle = ContentBundle(file_path)
+ if not bundle.is_installed():
+ logging.debug('Installing content bundle')
+ bundle.install()
+
+ activities = _get_activities_for_mime('text/html')
+ if len(activities) == 0:
+ logging.warning('No activity can open HTML content bundles')
+ return
+
+ uri = bundle.get_start_uri()
+ logging.debug('activityfactory.creating with uri %s', uri)
+
+ activity_bundle = registry.get_bundle(activities[0].get_bundle_id())
+ launch(activity_bundle, uri=uri)
+ else:
+ activity_id = metadata.get('activity_id', '')
+
+ if bundle_id is None:
+ activities = get_activities(metadata)
+ if not activities:
+ logging.warning('No activity can open this object, %s.',
+ metadata.get('mime_type', None))
+ return
+ bundle_id = activities[0].get_bundle_id()
+
+ bundle = registry.get_bundle(bundle_id)
+
+
+ if metadata.get('mountpoint', '/') == '/':
+ object_id = metadata['uid']
+ else:
+ object_id = model.copy(metadata, '/')
+
+ launch(bundle, activity_id=activity_id, object_id=object_id,
+ color=get_icon_color(metadata))
+
+def launch(bundle, activity_id=None, object_id=None, uri=None, color=None,
+ invited=False):
+ if activity_id is None:
+ 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 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()
+ return XoColor(client.get_string('/desktop/sugar/user/color'))
+ else:
+ return XoColor(metadata['icon-color'])
diff --git a/shell/src/jarabe/journal/modalalert.py b/shell/src/jarabe/journal/modalalert.py
new file mode 100644
index 0000000..c7c6a0a
--- /dev/null
+++ b/shell/src/jarabe/journal/modalalert.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+from gettext import gettext as _
+import gconf
+
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor
+
+class ModalAlert(gtk.Window):
+
+ __gtype_name__ = 'SugarModalAlert'
+
+ def __init__(self):
+ gtk.Window.__init__(self)
+
+ self.set_border_width(style.LINE_WIDTH)
+ offset = style.GRID_CELL_SIZE
+ width = gtk.gdk.screen_width() - offset * 2
+ height = gtk.gdk.screen_height() - offset * 2
+ self.set_size_request(width, height)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(True)
+
+ self._main_view = gtk.EventBox()
+ self._vbox = gtk.VBox()
+ self._vbox.set_spacing(style.DEFAULT_SPACING)
+ self._vbox.set_border_width(style.GRID_CELL_SIZE * 2)
+ self._main_view.modify_bg(gtk.STATE_NORMAL,
+ style.COLOR_BLACK.get_gdk_color())
+ self._main_view.add(self._vbox)
+ self._vbox.show()
+
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+
+ icon = Icon(icon_name='activity-journal',
+ pixel_size=style.XLARGE_ICON_SIZE,
+ xo_color=color)
+ self._vbox.pack_start(icon, False)
+ icon.show()
+
+ self._title = gtk.Label()
+ self._title.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._title.set_markup('<b>%s</b>' % _('Your Journal is full'))
+ self._vbox.pack_start(self._title, False)
+ self._title.show()
+
+ self._message = gtk.Label(_('Please delete some old Journal'
+ ' entries to make space for new ones.'))
+ self._message.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._vbox.pack_start(self._message, False)
+ self._message.show()
+
+ alignment = gtk.Alignment(xalign=0.5, yalign=0.5)
+ self._vbox.pack_start(alignment, expand=False)
+ alignment.show()
+
+ self._show_journal = gtk.Button()
+ self._show_journal.set_label(_('Show Journal'))
+ alignment.add(self._show_journal)
+ self._show_journal.show()
+ self._show_journal.connect('clicked', self.__show_journal_cb)
+
+ self.add(self._main_view)
+ self._main_view.show()
+
+ self.connect("realize", self.__realize_cb)
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+
+ def __show_journal_cb(self, button):
+ '''The opener will listen on the destroy signal
+ '''
+ self.destroy()
+
diff --git a/shell/src/jarabe/journal/model.py b/shell/src/jarabe/journal/model.py
new file mode 100644
index 0000000..ffc62e0
--- /dev/null
+++ b/shell/src/jarabe/journal/model.py
@@ -0,0 +1,541 @@
+# Copyright (C) 2007-2008, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import os
+from datetime import datetime
+import time
+import shutil
+from stat import S_IFMT, S_IFDIR, S_IFREG
+import re
+
+import gobject
+import dbus
+import gconf
+import gio
+
+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']
+
+MIN_PAGES_TO_CACHE = 3
+MAX_PAGES_TO_CACHE = 5
+
+class _Cache(object):
+
+ __gtype_name__ = 'model_Cache'
+
+ def __init__(self, entries=None):
+ self._array = []
+ if entries is not None:
+ self.append_all(entries)
+
+ def prepend_all(self, entries):
+ self._array[0:0] = entries
+
+ def append_all(self, entries):
+ self._array += entries
+
+ def __len__(self):
+ return len(self._array)
+
+ def __getitem__(self, key):
+ return self._array[key]
+
+ def __delitem__(self, key):
+ del self._array[key]
+
+
+class BaseResultSet(object):
+ """Encapsulates the result of a query
+ """
+
+ def __init__(self, query, page_size):
+ self._total_count = -1
+ self._position = -1
+ self._query = query
+ self._page_size = page_size
+
+ self._offset = 0
+ self._cache = _Cache()
+
+ self.ready = dispatch.Signal()
+ self.progress = dispatch.Signal()
+
+ def setup(self):
+ self.ready.send(self)
+
+ def stop(self):
+ pass
+
+ def get_length(self):
+ if self._total_count == -1:
+ query = self._query.copy()
+ query['limit'] = self._page_size * MIN_PAGES_TO_CACHE
+ entries, self._total_count = self.find(query)
+ self._cache.append_all(entries)
+ self._offset = 0
+ return self._total_count
+
+ length = property(get_length)
+
+ def find(self, query):
+ raise NotImplementedError()
+
+ def seek(self, position):
+ self._position = position
+
+ def read(self):
+ logging.debug('ResultSet.read position: %r', self._position)
+
+ if self._position == -1:
+ self.seek(0)
+
+ if self._position < self._offset:
+ remaining_forward_entries = 0
+ else:
+ remaining_forward_entries = self._offset + len(self._cache) - \
+ self._position
+
+ if self._position > self._offset + len(self._cache):
+ remaining_backwards_entries = 0
+ else:
+ remaining_backwards_entries = self._position - self._offset
+
+ last_cached_entry = self._offset + len(self._cache)
+
+ if remaining_forward_entries <= 0 and remaining_backwards_entries <= 0:
+
+ # Total cache miss: remake it
+ limit = self._page_size * MIN_PAGES_TO_CACHE
+ offset = max(0, self._position - limit / 2)
+ logging.debug('remaking cache, offset: %r limit: %r', offset,
+ limit)
+ query = self._query.copy()
+ query['limit'] = limit
+ query['offset'] = offset
+ entries, self._total_count = self.find(query)
+
+ del self._cache[:]
+ self._cache.append_all(entries)
+ self._offset = offset
+
+ 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',
+ last_cached_entry)
+ query = self._query.copy()
+ query['limit'] = self._page_size
+ query['offset'] = last_cached_entry
+ entries, self._total_count = self.find(query)
+
+ # update cache
+ self._cache.append_all(entries)
+
+ # apply the cache limit
+ cache_limit = self._page_size * MAX_PAGES_TO_CACHE
+ objects_excess = len(self._cache) - cache_limit
+ if objects_excess > 0:
+ self._offset += objects_excess
+ del self._cache[:objects_excess]
+
+ elif remaining_forward_entries > 0 and \
+ remaining_backwards_entries <= 0 and self._offset > 0:
+
+ # Add one page to the beginning of cache
+ limit = min(self._offset, self._page_size)
+ self._offset = max(0, self._offset - limit)
+
+ logging.debug('prepending one more page, offset: %r limit: %r',
+ self._offset, limit)
+ query = self._query.copy()
+ query['limit'] = limit
+ query['offset'] = self._offset
+ entries, self._total_count = self.find(query)
+
+ # update cache
+ self._cache.prepend_all(entries)
+
+ # apply the cache limit
+ cache_limit = self._page_size * MAX_PAGES_TO_CACHE
+ 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
+ """
+ def __init__(self, query, page_size):
+
+ if query.get('query', '') and not query['query'].startswith('"'):
+ query_text = ''
+ words = query['query'].split(' ')
+ for word in words:
+ if word:
+ if query_text:
+ query_text += ' '
+ query_text += word + '*'
+
+ query['query'] = query_text
+
+ BaseResultSet.__init__(self, query, page_size)
+
+ def find(self, query):
+ entries, total_count = _get_datastore().find(query, PROPERTIES,
+ byte_arrays=True)
+
+ for entry in entries:
+ entry['mountpoint'] = '/'
+
+ return entries, total_count
+
+class InplaceResultSet(BaseResultSet):
+ """Encapsulates the result of a query on a mount point
+ """
+ def __init__(self, query, page_size, mount_point):
+ BaseResultSet.__init__(self, query, page_size)
+ self._mount_point = mount_point
+ self._file_list = None
+ self._pending_directories = 0
+ self._stopped = False
+
+ query_text = query.get('query', '')
+ if query_text.startswith('"') and query_text.endswith('"'):
+ self._regex = re.compile('*%s*' % query_text.strip(['"']))
+ elif query_text:
+ expression = ''
+ for word in query_text.split(' '):
+ expression += '(?=.*%s.*)' % word
+ self._regex = re.compile(expression, re.IGNORECASE)
+ else:
+ self._regex = None
+
+ if query.get('timestamp', ''):
+ self._date_start = int(query['timestamp']['start'])
+ self._date_end = int(query['timestamp']['end'])
+ else:
+ self._date_start = None
+ self._date_end = None
+
+ self._mime_types = query.get('mime_type', [])
+
+ def setup(self):
+ self._file_list = []
+ self._recurse_dir(self._mount_point)
+
+ def stop(self):
+ self._stopped = True
+
+ def setup_ready(self):
+ self._file_list.sort(lambda a, b: b[2] - a[2])
+ self.ready.send(self)
+
+ def find(self, query):
+ if self._file_list is None:
+ raise ValueError('Need to call setup() first')
+
+ if self._stopped:
+ raise ValueError('InplaceResultSet already stopped')
+
+ t = time.time()
+
+ offset = int(query.get('offset', 0))
+ 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:
+ metadata = _get_file_metadata(file_path, stat)
+ metadata['mountpoint'] = self._mount_point
+ entries.append(metadata)
+
+ logging.debug('InplaceResultSet.find took %f s.', time.time() - t)
+
+ return entries, total_count
+
+ def _recurse_dir(self, dir_path):
+ self._pending_directories += 1
+ gobject.idle_add(self._idle_recurse_dir, dir_path)
+
+ 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()
+
+ def _real_recurse_dir(self, dir_path):
+ if self._stopped:
+ return
+
+ try:
+ dirs = os.listdir(dir_path)
+ except Exception:
+ logging.exception('Error reading directory %r', dir_path)
+ dirs = []
+
+ 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
+
+ if self._regex is not None and \
+ not self._regex.match(full_path):
+ add_to_list = False
+
+ 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._mime_types:
+ mime_type = gio.content_type_guess(filename=full_path)
+ if mime_type not in self._mime_types:
+ add_to_list = False
+
+ if add_to_list:
+ file_info = (full_path, stat, int(stat.st_mtime))
+ self._file_list.append(file_info)
+
+ self.progress.send(self)
+
+ 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,
+ '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:
+ bus = dbus.SessionBus()
+ remote_object = bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH)
+ _datastore = dbus.Interface(remote_object, DS_DBUS_INTERFACE)
+
+ _datastore.connect_to_signal('Created', _datastore_created_cb)
+ _datastore.connect_to_signal('Updated', _datastore_updated_cb)
+ _datastore.connect_to_signal('Deleted', _datastore_deleted_cb)
+
+ 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
+ """
+ query = query_.copy()
+
+ mount_points = query.pop('mountpoints', ['/'])
+ if mount_points is None or len(mount_points) != 1:
+ raise ValueError('Exactly one mount point must be specified')
+
+ if mount_points[0] == '/':
+ return DatastoreResultSet(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:
+ if os.path.ismount(dir_path):
+ return dir_path
+ else:
+ dir_path = dir_path.rsplit(os.sep, 1)[0]
+
+def get(object_id):
+ """Returns the metadata for an object
+ """
+ if os.path.exists(object_id):
+ stat = os.stat(object_id)
+ metadata = _get_file_metadata(object_id, stat)
+ metadata['mountpoint'] = _get_mount_point(object_id)
+ else:
+ metadata = _get_datastore().get_properties(object_id, byte_arrays=True)
+ metadata['mountpoint'] = '/'
+ return metadata
+
+def get_file(object_id):
+ """Returns the file for an object
+ """
+ if os.path.exists(object_id):
+ logging.debug('get_file asked for file with path %r', object_id)
+ return object_id
+ else:
+ logging.debug('get_file asked for entry with id %r', object_id)
+ file_path = _get_datastore().get_filename(object_id)
+ if file_path:
+ return util.TempFilePath(file_path)
+ else:
+ return None
+
+def get_file_size(object_id):
+ """Return the file size for an object
+ """
+ logging.debug('get_file_size %r', object_id)
+ if os.path.exists(object_id):
+ return os.stat(object_id).st_size
+
+ file_path = _get_datastore().get_filename(object_id)
+ if file_path:
+ size = os.stat(file_path).st_size
+ os.remove(file_path)
+ return size
+
+ 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
+ """
+ if os.path.exists(object_id):
+ os.unlink(object_id)
+ deleted.send(None, object_id=object_id)
+ else:
+ _get_datastore().delete(object_id)
+
+def copy(metadata, mount_point):
+ """Copies an object to another mount point
+ """
+ metadata = get(metadata['uid'])
+ file_path = get_file(metadata['uid'])
+
+ metadata['mountpoint'] = mount_point
+ del metadata['uid']
+
+ 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
+ """
+ logging.debug('model.write %r %r %r', metadata.get('uid', ''), file_path,
+ update_mtime)
+ if update_mtime:
+ metadata['mtime'] = datetime.now().isoformat()
+ metadata['timestamp'] = int(time.time())
+
+ if metadata.get('mountpoint', '/') == '/':
+ if metadata.get('uid', ''):
+ object_id = _get_datastore().update(metadata['uid'],
+ dbus.Dictionary(metadata),
+ file_path,
+ transfer_ownership)
+ else:
+ object_id = _get_datastore().create(dbus.Dictionary(metadata),
+ file_path,
+ transfer_ownership)
+ else:
+ if not os.path.exists(file_path):
+ raise ValueError('Entries without a file cannot be copied to '
+ 'removable devices')
+
+ file_name = _get_file_name(metadata['title'], metadata['mime_type'])
+ file_name = _get_unique_file_name(metadata['mountpoint'], file_name)
+
+ destination_path = os.path.join(metadata['mountpoint'], file_name)
+ shutil.copy(file_path, destination_path)
+ object_id = destination_path
+ created.send(None, object_id=object_id)
+
+ return object_id
+
+def _get_file_name(title, mime_type):
+ file_name = title
+
+ extension = mime.get_primary_extension(mime_type)
+ if extension is not None and extension:
+ extension = '.' + extension
+ if not file_name.endswith(extension):
+ file_name += extension
+
+ # Invalid characters in VFAT filenames. From
+ # http://en.wikipedia.org/wiki/File_Allocation_Table
+ invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\x7F']
+ invalid_chars.extend([chr(x) for x in range(0, 32)])
+ for char in invalid_chars:
+ file_name = file_name.replace(char, '_')
+
+ # FAT limit is 255, leave some space for uniqueness
+ max_len = 250
+ if len(file_name) > max_len:
+ name, extension = os.path.splitext(file_name)
+ file_name = name[0:max_len - len(extension)] + extension
+
+ 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
+ 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
+ i += 1
+
+ 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/shell/src/jarabe/journal/objectchooser.py b/shell/src/jarabe/journal/objectchooser.py
new file mode 100644
index 0000000..49af3e6
--- /dev/null
+++ b/shell/src/jarabe/journal/objectchooser.py
@@ -0,0 +1,199 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+import logging
+
+import gobject
+import gtk
+import wnck
+
+from sugar.graphics import style
+from sugar.graphics.toolbutton import ToolButton
+
+from jarabe.journal.listview import BaseListView
+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]))
+ }
+
+ def __init__(self, parent=None, what_filter=''):
+ gtk.Window.__init__(self)
+ self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.set_decorated(False)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_border_width(style.LINE_WIDTH)
+
+ self._selected_object_id = None
+
+ self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK)
+ self.connect('visibility-notify-event',
+ self.__visibility_notify_event_cb)
+ self.connect('delete-event', self.__delete_event_cb)
+ self.connect('key-press-event', self.__key_press_event_cb)
+
+ if parent is None:
+ logging.warning('ObjectChooser: No parent window specified')
+ else:
+ self.connect('realize', self.__realize_cb, parent)
+
+ screen = wnck.screen_get_default()
+ screen.connect('window-closed', self.__window_closed_cb, parent)
+
+ vbox = gtk.VBox()
+ self.add(vbox)
+ vbox.show()
+
+ title_box = TitleBox()
+ title_box.connect('volume-changed', self.__volume_changed_cb)
+ title_box.close_button.connect('clicked',
+ self.__close_button_clicked_cb)
+ title_box.set_size_request(-1, style.GRID_CELL_SIZE)
+ vbox.pack_start(title_box, expand=False)
+ title_box.show()
+
+ separator = gtk.HSeparator()
+ vbox.pack_start(separator, expand=False)
+ separator.show()
+
+ self._toolbar = SearchToolbar()
+ self._toolbar.connect('query-changed', self.__query_changed_cb)
+ self._toolbar.set_size_request(-1, style.GRID_CELL_SIZE)
+ vbox.pack_start(self._toolbar, expand=False)
+ self._toolbar.show()
+
+ self._list_view = ChooserListView()
+ self._list_view.connect('entry-activated', self.__entry_activated_cb)
+ vbox.pack_start(self._list_view)
+ self._list_view.show()
+
+ self._toolbar.set_mount_point('/')
+
+ width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2
+ height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2
+ self.set_size_request(width, height)
+
+ if what_filter:
+ self._toolbar.set_what_filter(what_filter)
+
+ def __realize_cb(self, chooser, parent):
+ self.window.set_transient_for(parent)
+ # TODO: Should we disconnect the signal here?
+
+ def __window_closed_cb(self, screen, window, parent):
+ if window.get_xid() == parent.xid:
+ self.destroy()
+
+ def __entry_activated_cb(self, list_view, uid):
+ self._selected_object_id = uid
+ self.emit('response', gtk.RESPONSE_ACCEPT)
+
+ def __delete_event_cb(self, chooser, event):
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def __key_press_event_cb(self, widget, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'Escape':
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def __close_button_clicked_cb(self, button):
+ self.emit('response', gtk.RESPONSE_DELETE_EVENT)
+
+ def get_selected_object_id(self):
+ return self._selected_object_id
+
+ def __query_changed_cb(self, toolbar, query):
+ self._list_view.update_with_query(query)
+
+ def __volume_changed_cb(self, volume_toolbar, mount_point):
+ logging.debug('Selected volume: %r.', mount_point)
+ self._toolbar.set_mount_point(mount_point)
+
+ def __visibility_notify_event_cb(self, window, event):
+ logging.debug('visibility_notify_event_cb %r', self)
+ visible = event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED
+ self._list_view.set_is_visible(visible)
+
+class TitleBox(VolumesToolbar):
+ __gtype_name__ = 'TitleBox'
+
+ def __init__(self):
+ VolumesToolbar.__init__(self)
+
+ label = gtk.Label()
+ label.set_markup('<b>%s</b>' % _('Choose an object'))
+ label.set_alignment(0, 0.5)
+ self._add_widget(label, expand=True)
+
+ self.close_button = ToolButton(icon_name='dialog-cancel')
+ self.close_button.set_tooltip(_('Close'))
+ self.insert(self.close_button, -1)
+ self.close_button.show()
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+class ChooserListView(BaseListView):
+ __gtype_name__ = 'ChooserListView'
+
+ __gsignals__ = {
+ 'entry-activated': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str])),
+ }
+
+ def __init__(self):
+ BaseListView.__init__(self)
+
+ self.cell_icon.props.show_palette = False
+ self.tree_view.props.hover_selection = True
+
+ self.tree_view.connect('button-release-event',
+ self.__button_release_event_cb)
+
+ def __entry_activated_cb(self, entry):
+ self.emit('entry-activated', entry)
+
+ def __button_release_event_cb(self, tree_view, event):
+ if event.window != tree_view.get_bin_window():
+ return False
+
+ pos = tree_view.get_path_at_pos(event.x, event.y)
+ if pos is None:
+ return False
+
+ path, column_, x_, y_ = pos
+ uid = tree_view.get_model()[path][ListModel.COLUMN_UID]
+ self.emit('entry-activated', uid)
+
+ return False
+
diff --git a/shell/src/jarabe/journal/palettes.py b/shell/src/jarabe/journal/palettes.py
new file mode 100644
index 0000000..7c3e5ff
--- /dev/null
+++ b/shell/src/jarabe/journal/palettes.py
@@ -0,0 +1,235 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+import logging
+
+import gobject
+import gtk
+import gconf
+
+from sugar.graphics import style
+from sugar.graphics.palette import Palette
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.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,
+ ([str])),
+ }
+
+ def __init__(self, metadata, detail=False):
+
+ self._metadata = metadata
+ self._temp_file_path = None
+
+ activity_icon = Icon(icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR)
+ activity_icon.props.file = misc.get_icon_name(metadata)
+ activity_icon.props.xo_color = misc.get_icon_color(metadata)
+
+ if metadata.has_key('title'):
+ title = gobject.markup_escape_text(metadata['title'])
+ else:
+ title = _('Untitled')
+
+ 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()
+
+ 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)
+
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ menu_item = MenuItem(_('Copy'))
+ icon = Icon(icon_name='edit-copy', xo_color=color,
+ icon_size=gtk.ICON_SIZE_MENU)
+ menu_item.set_image(icon)
+ menu_item.connect('activate', self.__copy_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ menu_item = MenuItem(_('Send to'), 'document-send')
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ friends_menu = FriendsMenu()
+ friends_menu.connect('friend-selected', self.__friend_selected_cb)
+ menu_item.set_submenu(friends_menu)
+
+ if detail == True:
+ menu_item = MenuItem(_('View Details'), 'go-right')
+ menu_item.connect('activate', self.__detail_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ menu_item = MenuItem(_('Erase'), 'list-remove')
+ menu_item.connect('activate', self.__erase_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __start_activate_cb(self, menu_item):
+ misc.resume(self._metadata)
+
+ def __copy_activate_cb(self, menu_item):
+ clipboard = gtk.Clipboard()
+ clipboard.set_with_data([('text/uri-list', 0, 0)],
+ self.__clipboard_get_func_cb,
+ self.__clipboard_clear_func_cb)
+
+ def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ # Get hold of a reference so the temp file doesn't get deleted
+ self._temp_file_path = model.get_file(self._metadata['uid'])
+ logging.debug('__clipboard_get_func_cb %r', self._temp_file_path)
+ selection_data.set_uris(['file://' + self._temp_file_path])
+
+ def __clipboard_clear_func_cb(self, clipboard, data):
+ # Release and delete the temp file
+ self._temp_file_path = None
+
+ def __erase_activate_cb(self, menu_item):
+ model.delete(self._metadata['uid'])
+
+ def __detail_activate_cb(self, menu_item):
+ self.emit('detail-clicked', self._metadata['uid'])
+
+ def __friend_selected_cb(self, menu_item, buddy):
+ logging.debug('__friend_selected_cb')
+ file_name = model.get_file(self._metadata['uid'])
+
+ title = str(self._metadata['title'])
+ description = str(self._metadata.get('description', ''))
+ mime_type = str(self._metadata['mime_type'])
+
+ if not mime_type:
+ mime_type = mime.get_for_file(file_name)
+
+ 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])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ if filetransfer.file_transfer_available():
+ friends_model = friends.get_model()
+ for friend in friends_model:
+ if friend.is_present():
+ menu_item = MenuItem(text_label=friend.get_nick(),
+ icon_name='computer-xo',
+ xo_color=friend.get_color())
+ menu_item.connect('activate', self.__item_activate_cb,
+ friend)
+ self.append(menu_item)
+ menu_item.show()
+
+ if not self.get_children():
+ menu_item = MenuItem(_('No friends present'))
+ menu_item.set_sensitive(False)
+ self.append(menu_item)
+ menu_item.show()
+ else:
+ menu_item = MenuItem(_('No valid connection found'))
+ menu_item.set_sensitive(False)
+ self.append(menu_item)
+ menu_item.show()
+
+ def __item_activate_cb(self, menu_item, friend):
+ self.emit('friend-selected', friend)
+
+
+class StartWithMenu(gtk.Menu):
+ __gtype_name__ = 'JournalStartWithMenu'
+
+ def __init__(self, metadata):
+ gobject.GObject.__init__(self)
+
+ self._metadata = metadata
+
+ for activity_info in misc.get_activities(metadata):
+ menu_item = MenuItem(activity_info.get_name())
+ menu_item.set_image(Icon(file=activity_info.get_icon(),
+ icon_size=gtk.ICON_SIZE_MENU))
+ menu_item.connect('activate', self.__item_activate_cb,
+ activity_info.get_bundle_id())
+ self.append(menu_item)
+ menu_item.show()
+
+ if not self.get_children():
+ if metadata.get('activity_id', ''):
+ resume_label = _('No activity to resume entry')
+ else:
+ resume_label = _('No activity to start entry')
+ menu_item = MenuItem(resume_label)
+ menu_item.set_sensitive(False)
+ self.append(menu_item)
+ menu_item.show()
+
+ def __item_activate_cb(self, menu_item, service_name):
+ mime_type = self._metadata.get('mime_type', '')
+ if mime_type:
+ mime_registry = mimeregistry.get_registry()
+ mime_registry.set_default_activity(mime_type, service_name)
+ misc.resume(self._metadata, service_name)
+
+
+class BuddyPalette(Palette):
+ def __init__(self, buddy):
+ self._buddy = buddy
+
+ nick, colors = buddy
+ buddy_icon = Icon(icon_name='computer-xo',
+ icon_size=style.STANDARD_ICON_SIZE,
+ xo_color=XoColor(colors))
+
+ Palette.__init__(self, primary_text=nick,
+ icon=buddy_icon)
+
+ # TODO: Support actions on buddies, like make friend, invite, etc.
diff --git a/shell/src/jarabe/journal/volumestoolbar.py b/shell/src/jarabe/journal/volumestoolbar.py
new file mode 100644
index 0000000..8b7786f
--- /dev/null
+++ b/shell/src/jarabe/journal/volumestoolbar.py
@@ -0,0 +1,207 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import os
+from gettext import gettext as _
+
+import gobject
+import gio
+import gtk
+import gconf
+
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.graphics.palette import Palette
+from sugar.graphics.xocolor import XoColor
+
+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-error': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str, str]))
+ }
+
+ def __init__(self):
+ gtk.Toolbar.__init__(self)
+ self._mount_added_hid = None
+ 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()
+ self._volume_buttons = [button]
+
+ self.connect('destroy', self.__destroy_cb)
+
+ gobject.idle_add(self._set_up_volumes)
+
+ def __destroy_cb(self, widget):
+ volume_monitor = gio.volume_monitor_get()
+ volume_monitor.disconnect(self._mount_added_hid)
+ volume_monitor.disconnect(self._mount_removed_hid)
+
+ 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)
+
+ for mount in volume_monitor.get_mounts():
+ self._add_button(mount)
+
+ def __mount_added_cb(self, volume_monitor, mount):
+ self._add_button(mount)
+
+ def __mount_removed_cb(self, volume_monitor, mount):
+ self._remove_button(mount)
+
+ def _add_button(self, mount):
+ logging.debug('VolumeToolbar._add_button: %r', mount.get_name())
+
+ 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()
+
+ self._volume_buttons.append(button)
+
+ 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)
+
+ def _unmount_activated_cb(self, menu_item, mount):
+ logging.debug('VolumesToolbar._unmount_activated_cb: %r', mount)
+ mount.unmount(self.__unmount_cb)
+
+ def __unmount_cb(self, source, result):
+ logging.debug('__unmount_cb %r %r', source, result)
+
+ def _get_button_for_mount(self, mount):
+ mount_point = mount.get_root().get_path()
+ for button in self.get_children():
+ if button.mount_point == mount_point:
+ return button
+ logging.error('Couldnt find button with mount_point %r', mount_point)
+ return None
+
+ def _remove_button(self, mount):
+ button = self._get_button_for_mount(mount)
+ self._volume_buttons.remove(button)
+ self.remove(button)
+ self.get_children()[0].props.active = True
+
+ if len(self.get_children()) < 2:
+ self.hide()
+
+ def set_active_volume(self, mount):
+ 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)
+
+ self.mount_point = mount_point
+
+ self.drag_dest_set(gtk.DEST_DEFAULT_ALL,
+ [('journal-object-id', 0, 0)],
+ gtk.gdk.ACTION_COPY)
+ self.connect('drag-data-received', self._drag_data_received_cb)
+
+ def _drag_data_received_cb(self, widget, drag_context, x, y, selection_data,
+ info, timestamp):
+ object_id = selection_data.data
+ metadata = model.get(object_id)
+ file_path = model.get_file(metadata['uid'])
+ if not file_path or not os.path.exists(file_path):
+ logging.warn('File does not exist')
+ self.emit('volume-error', _('Entries without a file cannot'
+ ' be copied'), _('Warning'))
+ return
+
+ try:
+ model.copy(metadata, self.mount_point)
+ except IOError:
+ logging.exception('BaseButton._drag_data_received_cb: Error'
+ 'while copying')
+ self.emit('volume-error', _('Input/Output error'), _('Error'))
+
+class VolumeButton(BaseButton):
+ def __init__(self, mount):
+ self._mount = mount
+ mount_point = mount.get_root().get_path()
+ BaseButton.__init__(self, mount_point)
+
+ icon_name = None
+ icon_theme = gtk.icon_theme_get_default()
+ for icon_name in mount.get_icon().props.names:
+ icon_info = icon_theme.lookup_icon(icon_name,
+ gtk.ICON_SIZE_LARGE_TOOLBAR, 0)
+ if icon_info is not None:
+ break
+
+ if icon_name is None:
+ icon_name = 'drive'
+
+ self.props.named_icon = icon_name
+
+ # TODO: retrieve the colors from the owner of the device
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ self.props.xo_color = color
+
+ def create_palette(self):
+ palette = VolumePalette(self._mount)
+ #palette.props.invoker = FrameWidgetInvoker(self)
+ #palette.set_group_id('frame')
+ return palette
+
+class JournalButton(BaseButton):
+ def __init__(self):
+ BaseButton.__init__(self, mount_point='/')
+
+ self.props.named_icon = 'activity-journal'
+
+ client = gconf.client_get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ self.props.xo_color = color
+
diff --git a/shell/src/jarabe/model/Makefile.am b/shell/src/jarabe/model/Makefile.am
new file mode 100644
index 0000000..92e8712
--- /dev/null
+++ b/shell/src/jarabe/model/Makefile.am
@@ -0,0 +1,19 @@
+sugardir = $(pythondir)/jarabe/model
+sugar_PYTHON = \
+ adhoc.py \
+ __init__.py \
+ buddy.py \
+ bundleregistry.py \
+ filetransfer.py \
+ friends.py \
+ invites.py \
+ olpcmesh.py \
+ mimeregistry.py \
+ neighborhood.py \
+ network.py \
+ notifications.py \
+ shell.py \
+ screen.py \
+ session.py \
+ sound.py \
+ telepathyclient.py
diff --git a/shell/src/jarabe/model/__init__.py b/shell/src/jarabe/model/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/shell/src/jarabe/model/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2006-2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
diff --git a/shell/src/jarabe/model/adhoc.py b/shell/src/jarabe/model/adhoc.py
new file mode 100644
index 0000000..ad0c941
--- /dev/null
+++ b/shell/src/jarabe/model/adhoc.py
@@ -0,0 +1,292 @@
+# 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, 'org.freedesktop.DBus.Properties')
+ 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/shell/src/jarabe/model/buddy.py b/shell/src/jarabe/model/buddy.py
new file mode 100644
index 0000000..6cf7be9
--- /dev/null
+++ b/shell/src/jarabe/model/buddy.py
@@ -0,0 +1,250 @@
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import gobject
+import 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"
+
+CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
+
+class BaseBuddyModel(gobject.GObject):
+ __gtype_name__ = 'SugarBaseBuddyModel'
+
+ def __init__(self, **kwargs):
+ self._key = None
+ self._nick = None
+ self._color = None
+ self._tags = None
+ self._present = False
+ self._current_activity = None
+
+ gobject.GObject.__init__(self, **kwargs)
+
+ def is_present(self):
+ return self._present
+
+ def set_present(self, present):
+ self._present = present
+
+ present = gobject.property(type=bool, default=False, getter=is_present,
+ setter=set_present)
+
+ 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
+
+ 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
+
+ def get_buddy(self):
+ raise NotImplementedError
+
+
+class OwnerBuddyModel(BaseBuddyModel):
+ __gtype_name__ = 'SugarOwnerBuddyModel'
+ def __init__(self):
+ BaseBuddyModel.__init__(self)
+ self.props.present = True
+
+ 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('org.freedesktop.Telepathy.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, pspec):
+ self._sync_properties()
+
+ def __current_activity_changed_cb(self, 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):
+ return True
+
+ def get_buddy(self):
+ raise NotImplementedError
+
+
+_owner_instance = None
+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
+
+ BaseBuddyModel.__init__(self, **kwargs)
+
+ def is_owner(self):
+ return False
+
+ def get_buddy(self):
+ raise NotImplementedError
+
+ 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)
+
+
+class FriendBuddyModel(BuddyModel):
+ __gtype_name__ = 'SugarFriendBuddyModel'
+ def __init__(self, nick, key):
+ BuddyModel.__init__(self, nick=nick, key=key)
+
+ def get_buddy(self):
+ raise NotImplementedError
diff --git a/shell/src/jarabe/model/bundleregistry.py b/shell/src/jarabe/model/bundleregistry.py
new file mode 100644
index 0000000..858655f
--- /dev/null
+++ b/shell/src/jarabe/model/bundleregistry.py
@@ -0,0 +1,444 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2009 Aleksey Lim
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+import traceback
+
+import gobject
+import gio
+import simplejson
+
+from sugar.bundle.activitybundle import ActivityBundle
+from sugar.bundle.contentbundle import ContentBundle
+from jarabe.journal.journalentrybundle import JournalEntryBundle
+from sugar.bundle.bundle import MalformedBundleException, \
+ AlreadyInstalledException, RegistrationException
+from sugar import env
+
+from jarabe import config
+from jarabe.model import mimeregistry
+
+class BundleRegistry(gobject.GObject):
+ """Tracks the available activity bundles"""
+
+ __gsignals__ = {
+ '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]))
+ }
+
+ def __init__(self):
+ logging.debug('STARTUP: Loading the bundle registry')
+ gobject.GObject.__init__(self)
+
+ self._mime_defaults = self._load_mime_defaults()
+
+ self._bundles = []
+ # hold a reference to the monitors so they don't get disposed
+ self._gio_monitors = []
+
+ user_path = env.get_user_activities_path()
+ for activity_dir in [user_path, config.activities_path]:
+ self._scan_directory(activity_dir)
+ directory = gio.File(activity_dir)
+ monitor = directory.monitor_directory()
+ monitor.connect('changed', self.__file_monitor_changed_cb)
+ self._gio_monitors.append(monitor)
+
+ self._last_defaults_mtime = -1
+ self._favorite_bundles = {}
+
+ try:
+ self._load_favorites()
+ except Exception:
+ logging.exception('Error while loading favorite_activities.')
+
+ self._merge_default_favorites()
+
+ def __file_monitor_changed_cb(self, monitor, one_file, other_file,
+ event_type):
+ if not one_file.get_path().endswith('.activity'):
+ return
+ if event_type == gio.FILE_MONITOR_EVENT_CREATED:
+ self.add_bundle(one_file.get_path(), install_mime_type=True)
+ elif event_type == gio.FILE_MONITOR_EVENT_DELETED:
+ self.remove_bundle(one_file.get_path())
+
+ def _load_mime_defaults(self):
+ defaults = {}
+
+ f = open(os.path.join(config.data_path, 'mime.defaults'), 'r')
+ for line in f.readlines():
+ line = line.strip()
+ if line and not line.startswith('#'):
+ mime = line[:line.find(' ')]
+ handler = line[line.rfind(' ') + 1:]
+ defaults[mime] = handler
+ f.close()
+
+ return defaults
+
+ def _get_favorite_key(self, bundle_id, version):
+ """We use a string as a composite key for the favorites dictionary
+ because JSON doesn't support tuples and python won't accept a list
+ as a dictionary key.
+ """
+ if ' ' in bundle_id:
+ raise ValueError('bundle_id cannot contain spaces')
+ return '%s %s' % (bundle_id, version)
+
+ def _load_favorites(self):
+ favorites_path = env.get_profile_path('favorite_activities')
+ if os.path.exists(favorites_path):
+ favorites_data = simplejson.load(open(favorites_path))
+
+ favorite_bundles = favorites_data['favorites']
+ if not isinstance(favorite_bundles, dict):
+ raise ValueError('Invalid format in %s.' % favorites_path)
+ if favorite_bundles:
+ first_key = favorite_bundles.keys()[0]
+ if not isinstance(first_key, basestring):
+ raise ValueError('Invalid format in %s.' % favorites_path)
+
+ first_value = favorite_bundles.values()[0]
+ if first_value is not None and \
+ not isinstance(first_value, dict):
+ raise ValueError('Invalid format in %s.' % favorites_path)
+
+ self._last_defaults_mtime = float(favorites_data['defaults-mtime'])
+ self._favorite_bundles = favorite_bundles
+
+ def _merge_default_favorites(self):
+ default_activities = []
+ defaults_path = os.path.join(config.data_path, 'activities.defaults')
+ if os.path.exists(defaults_path):
+ file_mtime = os.stat(defaults_path).st_mtime
+ if file_mtime > self._last_defaults_mtime:
+ f = open(defaults_path, 'r')
+ for line in f.readlines():
+ line = line.strip()
+ if line and not line.startswith('#'):
+ default_activities.append(line)
+ f.close()
+ self._last_defaults_mtime = file_mtime
+
+ if not default_activities:
+ return
+
+ for bundle_id in default_activities:
+ max_version = -1
+ for bundle in self._bundles:
+ if bundle.get_bundle_id() == bundle_id and \
+ max_version < 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:
+ self._favorite_bundles[key] = None
+
+ logging.debug('After merging: %r', self._favorite_bundles)
+
+ self._write_favorites_file()
+
+ def get_bundle(self, bundle_id):
+ """Returns a bundle given service name or substring,
+ returns None if there is either no match, or more than one
+ match by substring."""
+ result = []
+ key = bundle_id.lower()
+
+ for bundle in self._bundles:
+ name = bundle.get_bundle_id()
+ if name == bundle_id:
+ return bundle
+ if key in name.lower():
+ result.append(bundle)
+ if len(result) == 1:
+ return result[0]
+ return None
+
+ def __iter__(self):
+ return self._bundles.__iter__()
+
+ def __len__(self):
+ return len(self._bundles)
+
+ def _scan_directory(self, path):
+ if not os.path.isdir(path):
+ return
+
+ # Sort by mtime to ensure a stable activity order
+ bundles = {}
+ for f in os.listdir(path):
+ if not f.endswith('.activity'):
+ continue
+ try:
+ bundle_dir = os.path.join(path, f)
+ 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()))
+
+ 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()))
+
+ def add_bundle(self, bundle_path, install_mime_type=False):
+ bundle = self._add_bundle(bundle_path, install_mime_type)
+ if bundle is not None:
+ self._set_bundle_favorite(bundle.get_bundle_id(),
+ bundle.get_activity_version(),
+ True)
+ self.emit('bundle-added', bundle)
+ return True
+ else:
+ return False
+
+ def _add_bundle(self, bundle_path, install_mime_type=False):
+ logging.debug('STARTUP: Adding bundle %r', bundle_path)
+ try:
+ bundle = ActivityBundle(bundle_path)
+ if install_mime_type:
+ bundle.install_mime_type(bundle_path)
+ except MalformedBundleException:
+ logging.exception('Error loading bundle %r', bundle_path)
+ return None
+
+ bundle_id = bundle.get_bundle_id()
+ installed = self.get_bundle(bundle_id)
+
+ if installed is not None:
+ if installed.get_activity_version() >= \
+ bundle.get_activity_version():
+ logging.debug('Skip old version for %s', bundle_id)
+ return None
+ else:
+ logging.debug('Upgrade %s', bundle_id)
+ self.remove_bundle(installed.get_path())
+
+ self._bundles.append(bundle)
+ return bundle
+
+ def remove_bundle(self, bundle_path):
+ for bundle in self._bundles:
+ if bundle.get_path() == bundle_path:
+ self._bundles.remove(bundle)
+ self.emit('bundle-removed', bundle)
+ return True
+ return False
+
+ def get_activities_for_type(self, mime_type):
+ result = []
+
+ mime = mimeregistry.get_registry()
+ default_bundle_id = mime.get_default_activity(mime_type)
+ default_bundle = None
+
+ for bundle in self._bundles:
+ if bundle.get_mime_types() and mime_type in bundle.get_mime_types():
+
+ if bundle.get_bundle_id() == default_bundle_id:
+ default_bundle = bundle
+ elif self.get_default_for_type(mime_type) == \
+ bundle.get_bundle_id():
+ result.insert(0, bundle)
+ else:
+ result.append(bundle)
+
+ if default_bundle is not None:
+ result.insert(0, default_bundle)
+
+ 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
+
+ def _find_bundle(self, bundle_id, version):
+ for bundle in self._bundles:
+ if bundle.get_bundle_id() == bundle_id and \
+ bundle.get_activity_version() == version:
+ return bundle
+ raise ValueError('No bundle %r with version %r exists.' % \
+ (bundle_id, version))
+
+ def set_bundle_favorite(self, bundle_id, version, favorite):
+ changed = self._set_bundle_favorite(bundle_id, version, favorite)
+ if changed:
+ bundle = self._find_bundle(bundle_id, version)
+ self.emit('bundle-changed', bundle)
+
+ def _set_bundle_favorite(self, bundle_id, version, favorite):
+ key = self._get_favorite_key(bundle_id, version)
+ if favorite and not key in self._favorite_bundles:
+ self._favorite_bundles[key] = None
+ elif not favorite and key in self._favorite_bundles:
+ del self._favorite_bundles[key]
+ else:
+ return False
+
+ self._write_favorites_file()
+ return True
+
+ def is_bundle_favorite(self, bundle_id, version):
+ key = self._get_favorite_key(bundle_id, version)
+ return key in self._favorite_bundles
+
+ 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))
+
+ if self._favorite_bundles[key] is None:
+ self._favorite_bundles[key] = {}
+ if 'position' not in self._favorite_bundles[key] or \
+ [x, y] != self._favorite_bundles[key]['position']:
+ self._favorite_bundles[key]['position'] = [x, y]
+ else:
+ return
+
+ self._write_favorites_file()
+ bundle = self._find_bundle(bundle_id, version)
+ self.emit('bundle-changed', bundle)
+
+ def get_bundle_position(self, bundle_id, version):
+ """Get the coordinates where the user wants the representation of this
+ bundle to be displayed. Coordinates are relative to a 1000x1000 area.
+ """
+ key = self._get_favorite_key(bundle_id, version)
+ if key not in self._favorite_bundles or \
+ self._favorite_bundles[key] is None or \
+ 'position' not in self._favorite_bundles[key]:
+ return (-1, -1)
+ else:
+ return tuple(self._favorite_bundles[key]['position'])
+
+ def _write_favorites_file(self):
+ path = env.get_profile_path('favorite_activities')
+ favorites_data = {'defaults-mtime': self._last_defaults_mtime,
+ 'favorites': self._favorite_bundles}
+ simplejson.dump(favorites_data, open(path, 'w'), indent=1)
+
+ def is_installed(self, bundle):
+ # TODO treat ContentBundle in special way
+ # needs rethinking while fixing ContentBundle support
+ if isinstance(bundle, ContentBundle) or \
+ isinstance(bundle, JournalEntryBundle):
+ return bundle.is_installed()
+
+ 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():
+ return True
+ return False
+
+ def install(self, bundle, uid=None):
+ 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
+ elif bundle.get_bundle_id() == installed_bundle.get_bundle_id():
+ self.uninstall(installed_bundle, force=True)
+
+ install_dir = env.get_user_activities_path()
+ if isinstance(bundle, JournalEntryBundle):
+ install_path = bundle.install(uid)
+ else:
+ install_path = bundle.install(install_dir)
+
+ # TODO treat ContentBundle in special way
+ # needs rethinking while fixing ContentBundle support
+ if isinstance(bundle, ContentBundle) or \
+ isinstance(bundle, JournalEntryBundle):
+ pass
+ elif not self.add_bundle(install_path):
+ raise RegistrationException
+
+ def uninstall(self, bundle, force=False):
+ # TODO treat ContentBundle in special way
+ # needs rethinking while fixing ContentBundle support
+ if isinstance(bundle, ContentBundle) or \
+ isinstance(bundle, JournalEntryBundle):
+ if bundle.is_installed():
+ bundle.uninstall()
+ else:
+ logging.warning('Not uninstalling, bundle is not installed')
+ return
+
+ act = self.get_bundle(bundle.get_bundle_id())
+ if not force and \
+ act.get_activity_version() != bundle.get_activity_version():
+ logging.warning('Not uninstalling, different bundle present')
+ return
+
+ if not act.is_user_activity():
+ logging.debug('Do not uninstall system activity')
+ return
+
+ install_path = act.get_path()
+
+ bundle.uninstall(install_path, force)
+
+ if not self.remove_bundle(install_path):
+ raise RegistrationException
+
+ def upgrade(self, bundle):
+ act = self.get_bundle(bundle.get_bundle_id())
+ if act is None:
+ logging.warning('Activity not installed')
+ elif act.get_activity_version() == bundle.get_activity_version():
+ logging.debug('No upgrade needed, same version already installed.')
+ return
+ elif act.is_user_activity():
+ try:
+ self.uninstall(bundle, force=True)
+ except Exception:
+ logging.error('Uninstall failed, still trying to install ' \
+ 'newer bundle:\n' + \
+ traceback.format_exc())
+ else:
+ 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/shell/src/jarabe/model/filetransfer.py b/shell/src/jarabe/model/filetransfer.py
new file mode 100644
index 0000000..e0809bb
--- /dev/null
+++ b/shell/src/jarabe/model/filetransfer.py
@@ -0,0 +1,374 @@
+# Copyright (C) 2008 Tomeu Vizoso
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+import socket
+
+import gobject
+import gio
+import dbus
+from telepathy.interfaces import CONNECTION_INTERFACE_REQUESTS, CHANNEL
+from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT, \
+ SOCKET_ADDRESS_TYPE_UNIX, \
+ SOCKET_ACCESS_CONTROL_LOCALHOST
+from telepathy.client import Connection, Channel
+
+from sugar.presence import presenceservice
+from sugar import dispatch
+
+from jarabe.util.telepathy import connection_watcher
+
+FT_STATE_NONE = 0
+FT_STATE_PENDING = 1
+FT_STATE_ACCEPTED = 2
+FT_STATE_OPEN = 3
+FT_STATE_COMPLETED = 4
+FT_STATE_CANCELLED = 5
+
+FT_REASON_NONE = 0
+FT_REASON_REQUESTED = 1
+FT_REASON_LOCAL_STOPPED = 2
+FT_REASON_REMOTE_STOPPED = 3
+FT_REASON_LOCAL_ERROR = 4
+FT_REASON_LOCAL_ERROR = 5
+FT_REASON_REMOTE_ERROR = 6
+
+# FIXME: use constants from tp-python once the spec is undrafted
+CHANNEL_TYPE_FILE_TRANSFER = \
+ 'org.freedesktop.Telepathy.Channel.Type.FileTransfer'
+
+# TODO Move to use splice_async() in Sugar 0.88
+class StreamSplicer(gobject.GObject):
+ _CHUNK_SIZE = 10240 # 10K
+ __gsignals__ = {
+ 'finished': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([])),
+ }
+ def __init__(self, input_stream, output_stream):
+ gobject.GObject.__init__(self)
+
+ self._input_stream = input_stream
+ self._output_stream = output_stream
+ self._pending_buffers = []
+
+ def start(self):
+ self._input_stream.read_async(self._CHUNK_SIZE, self.__read_async_cb,
+ gobject.PRIORITY_LOW)
+
+ def __read_async_cb(self, input_stream, result):
+ data = input_stream.read_finish(result)
+
+ if not data:
+ logging.debug('closing input stream')
+ self._input_stream.close()
+ else:
+ self._pending_buffers.append(data)
+ self._input_stream.read_async(self._CHUNK_SIZE,
+ self.__read_async_cb,
+ gobject.PRIORITY_LOW)
+ self._write_next_buffer()
+
+ def __write_async_cb(self, output_stream, result, user_data):
+ count_ = output_stream.write_finish(result)
+
+ if not self._pending_buffers and \
+ not self._output_stream.has_pending() and \
+ not self._input_stream.has_pending():
+ logging.debug('closing output stream')
+ output_stream.close()
+ self.emit('finished')
+ else:
+ self._write_next_buffer()
+
+ def _write_next_buffer(self):
+ if self._pending_buffers and not self._output_stream.has_pending():
+ data = self._pending_buffers.pop(0)
+ # TODO: we pass the buffer as user_data because of
+ # http://bugzilla.gnome.org/show_bug.cgi?id=564102
+ self._output_stream.write_async(data, self.__write_async_cb,
+ gobject.PRIORITY_LOW,
+ user_data=data)
+
+class BaseFileTransfer(gobject.GObject):
+
+ def __init__(self, connection):
+ gobject.GObject.__init__(self)
+ self._connection = connection
+ self._state = FT_STATE_NONE
+ self._transferred_bytes = 0
+
+ self.channel = None
+ self.buddy = None
+ self.title = None
+ self.file_size = None
+ self.description = None
+ self.mime_type = None
+ self.initial_offset = 0
+ self.reason_last_change = FT_REASON_NONE
+
+ def set_channel(self, channel):
+ self.channel = channel
+ self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal(
+ 'FileTransferStateChanged', self.__state_changed_cb)
+ self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal(
+ 'TransferredBytesChanged', self.__transferred_bytes_changed_cb)
+ self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal(
+ 'InitialOffsetDefined', self.__initial_offset_defined_cb)
+
+ channel_properties = self.channel[dbus.PROPERTIES_IFACE]
+
+ props = channel_properties.GetAll(CHANNEL_TYPE_FILE_TRANSFER)
+ self._state = props['State']
+ self.title = props['Filename']
+ self.file_size = props['Size']
+ self.description = props['Description']
+ 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)
+
+ def __transferred_bytes_changed_cb(self, transferred_bytes):
+ logging.debug('__transferred_bytes_changed_cb %r', transferred_bytes)
+ self.props.transferred_bytes = transferred_bytes
+
+ def _set_transferred_bytes(self, transferred_bytes):
+ self._transferred_bytes = transferred_bytes
+
+ def _get_transferred_bytes(self):
+ return self._transferred_bytes
+
+ transferred_bytes = gobject.property(type=int, default=0,
+ getter=_get_transferred_bytes, setter=_set_transferred_bytes)
+
+ def __initial_offset_defined_cb(self, offset):
+ logging.debug('__initial_offset_defined_cb %r', offset)
+ self.initial_offset = offset
+
+ def __state_changed_cb(self, state, reason):
+ logging.debug('__state_changed_cb %r %r', state, reason)
+ self.reason_last_change = reason
+ self.props.state = state
+
+ def _set_state(self, state):
+ self._state = state
+
+ def _get_state(self):
+ return self._state
+
+ state = gobject.property(type=int, getter=_get_state, setter=_set_state)
+
+ def cancel(self):
+ self.channel[CHANNEL].Close()
+
+class IncomingFileTransfer(BaseFileTransfer):
+ def __init__(self, connection, object_path, props):
+ BaseFileTransfer.__init__(self, connection)
+
+ channel = Channel(connection.service_name, object_path)
+ self.set_channel(channel)
+
+ self.connect('notify::state', self.__notify_state_cb)
+
+ self.destination_path = None
+ self._socket_address = None
+ self._socket = None
+ self._splicer = None
+
+ def accept(self, destination_path):
+ if os.path.exists(destination_path):
+ raise ValueError('Destination path already exists: %r' % \
+ destination_path)
+
+ self.destination_path = destination_path
+
+ channel_ft = self.channel[CHANNEL_TYPE_FILE_TRANSFER]
+ self._socket_address = channel_ft.AcceptFile(SOCKET_ADDRESS_TYPE_UNIX,
+ SOCKET_ACCESS_CONTROL_LOCALHOST, '', 0, byte_arrays=True)
+
+ def __notify_state_cb(self, file_transfer, pspec):
+ logging.debug('__notify_state_cb %r', self.props.state)
+ if self.props.state == FT_STATE_OPEN:
+ # Need to hold a reference to the socket so that python doesn't
+ # close the fd when it goes out of scope
+ self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self._socket.connect(self._socket_address)
+ input_stream = gio.unix.InputStream(self._socket.fileno(), True)
+
+ destination_file = gio.File(self.destination_path)
+ if self.initial_offset == 0:
+ output_stream = destination_file.create()
+ else:
+ output_stream = destination_file.append_to()
+
+ # TODO: Use splice_async when it gets implemented
+ self._splicer = StreamSplicer(input_stream, output_stream)
+ self._splicer.start()
+
+class OutgoingFileTransfer(BaseFileTransfer):
+ def __init__(self, buddy, file_name, title, description, mime_type):
+
+ presence_service = presenceservice.get_instance()
+ name, path = presence_service.get_preferred_connection()
+ connection = Connection(name, path,
+ ready_handler=self.__connection_ready_cb)
+
+ BaseFileTransfer.__init__(self, connection)
+ self.connect('notify::state', self.__notify_state_cb)
+
+ self._file_name = file_name
+ self._socket_address = None
+ self._socket = None
+ self._splicer = None
+ self._output_stream = None
+
+ self.buddy = buddy.get_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_TYPE_FILE_TRANSFER + '.ContentType': self.mime_type,
+ CHANNEL_TYPE_FILE_TRANSFER + '.Filename': self.title,
+ CHANNEL_TYPE_FILE_TRANSFER + '.Size': self.file_size,
+ CHANNEL_TYPE_FILE_TRANSFER + '.Description': self.description,
+ CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset': 0})
+
+ self.set_channel(Channel(connection.service_name, object_path))
+
+ channel_file_transfer = self.channel[CHANNEL_TYPE_FILE_TRANSFER]
+ self._socket_address = channel_file_transfer.ProvideFile(
+ 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:
+ # Need to hold a reference to the socket so that python doesn't
+ # closes the fd when it goes out of scope
+ self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self._socket.connect(self._socket_address)
+ output_stream = gio.unix.OutputStream(self._socket.fileno(), True)
+
+ logging.debug('opening %s for reading', self._file_name)
+ input_stream = gio.File(self._file_name).read()
+ if self.initial_offset > 0:
+ input_stream.skip(self.initial_offset)
+
+ # TODO: Use splice_async when it gets implemented
+ self._splicer = StreamSplicer(input_stream, output_stream)
+ self._splicer.start()
+
+ 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 \
+ not props[CHANNEL + '.Requested']:
+
+ logging.debug('__new_channels_cb %r', object_path)
+
+ incoming_file_transfer = IncomingFileTransfer(connection,
+ 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_added_cb(conn_watcher, connection):
+ _monitor_connection(connection)
+
+def _connection_removed_cb(conn_watcher, connection):
+ logging.debug('connection removed %r', connection)
+
+def init():
+ 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():
+ _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():
+ 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)
+ classes = properties['RequestableChannelClasses']
+ for prop, allowed_prop in classes:
+
+ channel_type = prop.get(CHANNEL + '.ChannelType', '')
+ target_handle_type = prop.get(CHANNEL + '.TargetHandleType', '')
+
+ if len(prop) == 2 and \
+ channel_type == CHANNEL_TYPE_FILE_TRANSFER and \
+ target_handle_type == CONNECTION_HANDLE_TYPE_CONTACT:
+ return True
+
+ 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()
+
+ # TODO: Use splice_async when it gets implemented
+ splicer = StreamSplicer(input_stream, output_stream)
+ splicer.start()
+
+ loop = gobject.MainLoop()
+ loop.run()
+
diff --git a/shell/src/jarabe/model/friends.py b/shell/src/jarabe/model/friends.py
new file mode 100644
index 0000000..b7bf7f1
--- /dev/null
+++ b/shell/src/jarabe/model/friends.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+from ConfigParser import ConfigParser
+
+import gobject
+import dbus
+
+from jarabe.model.buddy import BuddyModel
+from sugar import env
+
+class Friends(gobject.GObject):
+ __gsignals__ = {
+ 'friend-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([object])),
+ 'friend-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._friends = {}
+ self._path = os.path.join(env.get_profile_path(), 'friends')
+
+ self.load()
+
+ def has_buddy(self, buddy):
+ return self._friends.has_key(buddy.get_key())
+
+ def add_friend(self, buddy_info):
+ self._friends[buddy_info.get_key()] = buddy_info
+ self.emit('friend-added', buddy_info)
+
+ def make_friend(self, buddy):
+ if not self.has_buddy(buddy):
+ self.add_friend(buddy)
+ self.save()
+
+ def remove(self, buddy_info):
+ del self._friends[buddy_info.get_key()]
+ self.save()
+ self.emit('friend-removed', buddy_info.get_key())
+
+ def __iter__(self):
+ return self._friends.values().__iter__()
+
+ def load(self):
+ cp = ConfigParser()
+
+ try:
+ success = cp.read([self._path])
+ if success:
+ for key in cp.sections():
+ # HACK: don't screw up on old friends files
+ if len(key) < 20:
+ continue
+ buddy = BuddyModel(key=key, nick=cp.get(key, 'nick'))
+ self.add_friend(buddy)
+ except Exception:
+ logging.exception('Error parsing friends file')
+
+ def save(self):
+ cp = ConfigParser()
+
+ for friend in self:
+ section = friend.get_key()
+ cp.add_section(section)
+ cp.set(section, 'nick', friend.get_nick())
+ cp.set(section, 'color', friend.get_color().to_string())
+
+ fileobject = open(self._path, 'w')
+ cp.write(fileobject)
+ fileobject.close()
+
+ self._sync_friends()
+
+ def _sync_friends(self):
+ # XXX: temporary hack
+ # remove this when the shell service has a D-Bus API for buddies
+
+ def friends_synced():
+ pass
+
+ def friends_synced_error(e):
+ logging.error('Error asking presence service to sync friends: %s',
+ e)
+
+ keys = []
+ for friend in self:
+ keys.append(friend.get_key())
+
+ bus = dbus.SessionBus()
+ ps = bus.get_object('org.laptop.Sugar.Presence',
+ '/org/laptop/Sugar/Presence')
+ psi = dbus.Interface(ps, 'org.laptop.Sugar.Presence')
+ psi.SyncFriends(keys,
+ reply_handler=friends_synced,
+ error_handler=friends_synced_error)
+
+_model = None
+
+def get_model():
+ global _model
+ if _model is None:
+ _model = Friends()
+ return _model
diff --git a/shell/src/jarabe/model/invites.py b/shell/src/jarabe/model/invites.py
new file mode 100644
index 0000000..a386d30
--- /dev/null
+++ b/shell/src/jarabe/model/invites.py
@@ -0,0 +1,239 @@
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from functools import partial
+
+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
+
+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
+
+CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \
+ 'org.laptop.Telepathy.ActivityProperties'
+
+
+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
+
+ if activity_properties is not None:
+ self._activity_properties = activity_properties
+ else:
+ self._activity_properties = {}
+
+ 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]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ 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(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._dispatch_operations[invite.dispatch_operation_path]
+ self.emit('invite-removed', invite)
+
+ def __iter__(self):
+ return self._dispatch_operations.values().__iter__()
+
+
+_instance = None
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = Invites()
+ return _instance
diff --git a/shell/src/jarabe/model/mimeregistry.py b/shell/src/jarabe/model/mimeregistry.py
new file mode 100644
index 0000000..537f6f3
--- /dev/null
+++ b/shell/src/jarabe/model/mimeregistry.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2009 Aleksey Lim
+#
+# 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 re
+
+import gconf
+
+
+_DEFAULTS_KEY = '/desktop/sugar/journal/defaults'
+_GCONF_INVALID_CHARS = re.compile('[^a-zA-Z0-9-_/.]')
+_instance = None
+
+
+class MimeRegistry(object):
+
+ def __init__(self):
+ # TODO move here all mime_type related code from jarabe modules
+ self._gconf = gconf.client_get_default()
+
+ def get_default_activity(self, mime_type):
+ return self._gconf.get_string(_key_name(mime_type))
+
+ def set_default_activity(self, mime_type, bundle_id):
+ self._gconf.set_string(_key_name(mime_type), bundle_id)
+
+
+def get_registry():
+ global _instance
+ if _instance is None:
+ _instance = MimeRegistry()
+ return _instance
+
+
+def _key_name(mime_type):
+ mime_type = _GCONF_INVALID_CHARS.sub('_', mime_type)
+ return '%s/%s' % (_DEFAULTS_KEY, mime_type)
diff --git a/shell/src/jarabe/model/neighborhood.py b/shell/src/jarabe/model/neighborhood.py
new file mode 100644
index 0000000..90531a6
--- /dev/null
+++ b/shell/src/jarabe/model/neighborhood.py
@@ -0,0 +1,863 @@
+# 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
+from functools import partial
+
+import gobject
+import gconf
+import dbus
+from dbus import PROPERTIES_IFACE
+from telepathy.interfaces import ACCOUNT, \
+ ACCOUNT_MANAGER, \
+ CHANNEL, \
+ CHANNEL_INTERFACE_GROUP, \
+ CHANNEL_TYPE_CONTACT_LIST, \
+ CONNECTION, \
+ CONNECTION_INTERFACE_ALIASING, \
+ CONNECTION_INTERFACE_CONTACTS, \
+ CONNECTION_INTERFACE_REQUESTS, \
+ CONNECTION_INTERFACE_SIMPLE_PRESENCE
+from telepathy.constants import 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.profile import get_profile
+
+from jarabe.model.buddy import BuddyModel, get_owner_instance
+from jarabe.model import bundleregistry
+
+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'
+
+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 __init__(self, activity_id, room_handle):
+ gobject.GObject.__init__(self)
+
+ 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 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 == '/':
+ # Account has no connection, wait until it has one.
+ return
+
+ self._prepare_connection(connection_path)
+
+ 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._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():
+ 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
+
+ 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 __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)
+
+ 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:
+ del self._buddy_handles[handle]
+ self.emit('buddy-removed', handle)
+
+ def __buddy_info_updated_cb(self, handle, properties):
+ logging.debug('_Account.__buddy_info_updated_cb %r %r', 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):
+ logging.debug('_Account.__buddy_activities_changed_cb %r %r',
+ 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', properties)
+ self.emit('buddy-added', self._buddy_handles[handle], nick,
+ properties.get('key', None))
+ 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)
+
+ connection.GetActivities(
+ handle,
+ reply_handler=partial(self.__got_activities_cb, handle),
+ error_handler=partial(self.__error_handler_cb,
+ 'BuddyInfo.GetActivities'))
+
+ connection.GetCurrentActivity(
+ handle,
+ reply_handler=partial(self.__get_current_activity_cb,
+ handle),
+ error_handler=partial(self.__error_handler_cb,
+ 'BuddyInfo.GetCurrentActivity'))
+ else:
+ self.emit('buddy-added', contact_id, nick, None)
+
+ 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='org.freedesktop.DBus.Properties')
+
+ def __set_enabled_cb(self):
+ logging.debug('_Account.__set_enabled_cb success')
+
+class Neighborhood(gobject.GObject):
+ __gsignals__ = {
+ '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._link_local_account = None
+ self._server_account = None
+
+ 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')
+ server = client.get_string('/desktop/sugar/collaboration/jabber_server')
+
+ params = {
+ 'nickname': nick,
+ 'first-name': '',
+ 'last-name': '',
+ 'jid': '%s@%s' % (self._sanitize_nick(nick), server),
+ 'published-name': nick,
+ }
+
+ properties = {
+ 'org.freedesktop.Telepathy.Account.Enabled': True,
+ 'org.freedesktop.Telepathy.Account.Nickname': nick,
+ 'org.freedesktop.Telepathy.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': '%s@%s' % (self._sanitize_nick(nick), server),
+ 'password': key_hash,
+ 'server': server,
+ 'resource': 'sugar',
+ 'require-encryption': True,
+ 'ignore-ssl-errors': True,
+ 'register': True,
+ 'old-ssl': True,
+ 'port': dbus.UInt32(5223),
+ }
+
+ properties = {
+ 'org.freedesktop.Telepathy.Account.Enabled': True,
+ 'org.freedesktop.Telepathy.Account.Nickname': nick,
+ 'org.freedesktop.Telepathy.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 _sanitize_nick(self, nick):
+ return nick.replace(' ', '_')
+
+ def __buddy_added_cb(self, account, contact_id, nick, key):
+ logging.debug('__buddy_added_cb %r', contact_id)
+
+ if contact_id in self._buddies:
+ logging.debug('__buddy_added_cb buddy already tracked')
+ return
+
+ buddy = BuddyModel(
+ nick=nick,
+ account=account.object_path,
+ contact_id=contact_id,
+ key=key)
+ self._buddies[contact_id] = buddy
+
+ self.emit('buddy-added', buddy)
+
+ def __buddy_updated_cb(self, account, contact_id, properties):
+ logging.debug('__buddy_updated_cb %r %r', contact_id, properties)
+ if contact_id not in self._buddies:
+ logging.debug('__buddy_updated_cb Unknown buddy with contact_id %r',
+ contact_id)
+ return
+
+ buddy = self._buddies[contact_id]
+ if 'color' in properties:
+ buddy.props.color = XoColor(properties['color'])
+
+ 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
+
+ buddy = self._buddies[contact_id]
+ del self._buddies[contact_id]
+ self.emit('buddy-removed', buddy)
+
+ 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
+
+ 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
+
+ registry = bundleregistry.get_registry()
+ 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]
+ 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 activity_id not in self._activities:
+ logging.debug('__buddy_left_activity_cb Unknown activity with '
+ 'activity_id %r', activity_id)
+ return
+
+ self._activities[activity_id].remove_buddy(self._buddies[contact_id])
+
+ def get_buddies(self):
+ return self._buddies.values()
+
+ def get_activity(self, activity_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
+ if _model is None:
+ _model = Neighborhood()
+ return _model
diff --git a/shell/src/jarabe/model/network.py b/shell/src/jarabe/model/network.py
new file mode 100644
index 0000000..cd0d46e
--- /dev/null
+++ b/shell/src/jarabe/model/network.py
@@ -0,0 +1,751 @@
+# Copyright (C) 2008 Red Hat, Inc.
+# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer
+# Copyright (C) 2009-2010 One Laptop per Child
+# Copyright (C) 2009 Paraguay Educa, Martin Abente
+# Copyright (C) 2010 Plan Ceibal, Daniel Castelo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import os
+import time
+
+import dbus
+import dbus.service
+import gobject
+import ConfigParser
+import gconf
+
+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
+DEVICE_TYPE_802_11_OLPC_MESH = 6
+
+DEVICE_STATE_UNKNOWN = 0
+DEVICE_STATE_UNMANAGED = 1
+DEVICE_STATE_UNAVAILABLE = 2
+DEVICE_STATE_DISCONNECTED = 3
+DEVICE_STATE_PREPARE = 4
+DEVICE_STATE_CONFIG = 5
+DEVICE_STATE_NEED_AUTH = 6
+DEVICE_STATE_IP_CONFIG = 7
+DEVICE_STATE_ACTIVATED = 8
+DEVICE_STATE_FAILED = 9
+
+NM_CONNECTION_TYPE_802_11_WIRELESS = '802-11-wireless'
+NM_CONNECTION_TYPE_GSM = 'gsm'
+
+NM_ACTIVE_CONNECTION_STATE_UNKNOWN = 0
+NM_ACTIVE_CONNECTION_STATE_ACTIVATING = 1
+NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
+
+NM_802_11_AP_FLAGS_NONE = 0x00000000
+NM_802_11_AP_FLAGS_PRIVACY = 0x00000001
+
+NM_802_11_AP_SEC_NONE = 0x00000000
+NM_802_11_AP_SEC_PAIR_WEP40 = 0x00000001
+NM_802_11_AP_SEC_PAIR_WEP104 = 0x00000002
+NM_802_11_AP_SEC_PAIR_TKIP = 0x00000004
+NM_802_11_AP_SEC_PAIR_CCMP = 0x00000008
+NM_802_11_AP_SEC_GROUP_WEP40 = 0x00000010
+NM_802_11_AP_SEC_GROUP_WEP104 = 0x00000020
+NM_802_11_AP_SEC_GROUP_TKIP = 0x00000040
+NM_802_11_AP_SEC_GROUP_CCMP = 0x00000080
+NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x00000100
+NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x00000200
+
+NM_802_11_MODE_UNKNOWN = 0
+NM_802_11_MODE_ADHOC = 1
+NM_802_11_MODE_INFRA = 2
+
+NM_802_11_DEVICE_CAP_NONE = 0x00000000
+NM_802_11_DEVICE_CAP_CIPHER_WEP40 = 0x00000001
+NM_802_11_DEVICE_CAP_CIPHER_WEP104 = 0x00000002
+NM_802_11_DEVICE_CAP_CIPHER_TKIP = 0x00000004
+NM_802_11_DEVICE_CAP_CIPHER_CCMP = 0x00000008
+NM_802_11_DEVICE_CAP_WPA = 0x00000010
+NM_802_11_DEVICE_CAP_RSN = 0x00000020
+
+SETTINGS_SERVICE = 'org.freedesktop.NetworkManagerUserSettings'
+
+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'
+
+GSM_USERNAME_PATH = '/desktop/sugar/network/gsm/username'
+GSM_PASSWORD_PATH = '/desktop/sugar/network/gsm/password'
+GSM_NUMBER_PATH = '/desktop/sugar/network/gsm/number'
+GSM_APN_PATH = '/desktop/sugar/network/gsm/apn'
+GSM_PIN_PATH = '/desktop/sugar/network/gsm/pin'
+GSM_PUK_PATH = '/desktop/sugar/network/gsm/puk'
+
+_nm_settings = None
+_conn_counter = 0
+
+
+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
+ self.proto = None
+ self.group = None
+ self.pairwise = None
+
+ def get_dict(self):
+ wireless_security = {}
+ if self.key_mgmt is not None:
+ wireless_security['key-mgmt'] = self.key_mgmt
+ if self.proto is not None:
+ wireless_security['proto'] = self.proto
+ if self.pairwise is not None:
+ wireless_security['pairwise'] = self.pairwise
+ if self.group is not None:
+ wireless_security['group'] = self.group
+ return wireless_security
+
+class Wireless(object):
+ 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}
+ if self.security:
+ wireless['security'] = self.security
+ if self.mode:
+ 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"
+
+ def __init__(self, channel, anycast_addr):
+ self.channel = channel
+ self.anycast_addr = anycast_addr
+
+ def get_dict(self):
+ ret = {
+ "ssid": dbus.ByteArray("olpc-mesh"),
+ "channel": self.channel,
+ }
+
+ if self.anycast_addr:
+ ret["dhcp-anycast-address"] = dbus.ByteArray(self.anycast_addr)
+ return ret
+
+
+class Connection(object):
+ def __init__(self):
+ self.id = None
+ self.uuid = None
+ self.type = None
+ self.autoconnect = False
+ self.timestamp = None
+
+ def get_dict(self):
+ connection = {'id': self.id,
+ 'uuid': self.uuid,
+ 'type': self.type,
+ 'autoconnect': self.autoconnect}
+ if self.timestamp:
+ connection['timestamp'] = self.timestamp
+ return connection
+
+class IP4Config(object):
+ def __init__(self):
+ self.method = None
+
+ def get_dict(self):
+ ip4_config = {}
+ if self.method is not None:
+ ip4_config['method'] = self.method
+ return ip4_config
+
+class Serial(object):
+ def __init__(self):
+ self.baud = None
+
+ def get_dict(self):
+ serial = {}
+
+ if self.baud is not None:
+ serial['baud'] = self.baud
+
+ return serial
+
+class Ppp(object):
+ def __init__(self):
+ pass
+
+ def get_dict(self):
+ ppp = {}
+ return ppp
+
+class Gsm(object):
+ def __init__(self):
+ self.apn = None
+ self.number = None
+ self.username = None
+
+ def get_dict(self):
+ gsm = {}
+
+ if self.apn is not None:
+ gsm['apn'] = self.apn
+ if self.number is not None:
+ gsm['number'] = self.number
+ if self.username is not None:
+ gsm['username'] = self.username
+
+ return gsm
+
+class Settings(object):
+ def __init__(self, wireless_cfg=None):
+ self.connection = Connection()
+ self.ip4_config = None
+ self.wireless_security = None
+
+ if wireless_cfg is not None:
+ self.wireless = wireless_cfg
+ else:
+ self.wireless = Wireless()
+
+ def get_dict(self):
+ settings = {}
+ settings['connection'] = self.connection.get_dict()
+ settings[self.wireless.nm_name] = self.wireless.get_dict()
+ if self.wireless_security is not None:
+ settings['802-11-wireless-security'] = \
+ self.wireless_security.get_dict()
+ if self.ip4_config is not None:
+ settings['ipv4'] = self.ip4_config.get_dict()
+ return settings
+
+class Secrets(object):
+ def __init__(self, settings):
+ self.settings = settings
+ self.wep_key = None
+ self.psk = None
+ self.auth_alg = None
+
+ def get_dict(self):
+ # Although we could just return the keys here, we instead return all
+ # of the network settings so that we can apply any late decisions made
+ # by the user (e.g. if they selected shared key authentication). see
+ # http://bugs.sugarlabs.org/ticket/1602
+ settings = self.settings.get_dict()
+ if '802-11-wireless-security' not in settings:
+ settings['802-11-wireless-security'] = {}
+
+ if self.wep_key is not None:
+ settings['802-11-wireless-security']['wep-key0'] = self.wep_key
+ if self.psk is not None:
+ settings['802-11-wireless-security']['psk'] = self.psk
+ if self.auth_alg is not None:
+ settings['802-11-wireless-security']['auth-alg'] = self.auth_alg
+
+ return settings
+
+class SettingsGsm(object):
+ def __init__(self):
+ self.connection = Connection()
+ self.ip4_config = IP4Config()
+ self.serial = Serial()
+ self.ppp = Ppp()
+ self.gsm = Gsm()
+
+ def get_dict(self):
+ settings = {}
+
+ settings['connection'] = self.connection.get_dict()
+ settings['serial'] = self.serial.get_dict()
+ settings['ppp'] = self.ppp.get_dict()
+ settings['gsm'] = self.gsm.get_dict()
+ settings['ipv4'] = self.ip4_config.get_dict()
+
+ 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:
+ secrets['puk'] = self.puk
+ return {'gsm': secrets}
+
+class NMSettings(dbus.service.Object):
+ def __init__(self):
+ bus = dbus.SystemBus()
+ bus_name = dbus.service.BusName(SETTINGS_SERVICE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, NM_SETTINGS_PATH)
+
+ self.connections = {}
+ self.secrets_request = dispatch.Signal()
+
+ @dbus.service.method(dbus_interface=NM_SETTINGS_IFACE,
+ in_signature='', out_signature='ao')
+ def ListConnections(self):
+ return self.connections.values()
+
+ @dbus.service.signal(NM_SETTINGS_IFACE, signature='o')
+ def NewConnection(self, connection_path):
+ pass
+
+ def add_connection(self, uuid, conn):
+ self.connections[uuid] = conn
+ conn.secrets_request.connect(self.__secrets_request_cb)
+ self.NewConnection(conn.path)
+
+ def __secrets_request_cb(self, sender, **kwargs):
+ self.secrets_request.send(self, connection=sender,
+ response=kwargs['response'])
+
+class SecretsResponse(object):
+ ''' 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
+ self._error_cb = error_cb
+
+ def set_secrets(self, secrets):
+ self._connection.set_secrets(secrets)
+ self._reply_cb(secrets.get_dict())
+
+ def set_error(self, error):
+ self._error_cb(error)
+
+class NMSettingsConnection(dbus.service.Object):
+ def __init__(self, path, settings, secrets):
+ bus = dbus.SystemBus()
+ bus_name = dbus.service.BusName(SETTINGS_SERVICE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, path)
+
+ self.path = path
+ self.secrets_request = dispatch.Signal()
+
+ self._settings = settings
+ self._secrets = secrets
+
+ 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()
+
+ def set_secrets(self, secrets):
+ self._secrets = secrets
+ 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 = ConfigParser.ConfigParser()
+ try:
+ try:
+ if not config.read(config_path):
+ logging.error('Error reading the nm config file')
+ return
+ except ConfigParser.ParsingError:
+ logging.exception('Error reading the nm config file')
+ return
+ identifier = self._settings.connection.id
+
+ if identifier not in config.sections():
+ config.add_section(identifier)
+ config.set(identifier, 'type', self._settings.connection.type)
+ config.set(identifier, 'ssid', self._settings.wireless.ssid)
+ config.set(identifier, 'uuid', self._settings.connection.uuid)
+ config.set(identifier, 'autoconnect',
+ self._settings.connection.autoconnect)
+ if self._settings.connection.timestamp is not None:
+ config.set(identifier, 'timestamp',
+ self._settings.connection.timestamp)
+ if self._settings.wireless_security is not None:
+ if self._settings.wireless_security.key_mgmt is not None:
+ config.set(identifier, 'key-mgmt',
+ self._settings.wireless_security.key_mgmt)
+ if self._settings.wireless_security.proto is not None:
+ config.set(identifier, 'proto',
+ self._settings.wireless_security.proto)
+ if self._settings.wireless_security.pairwise is not None:
+ config.set(identifier, 'pairwise',
+ self._settings.wireless_security.pairwise)
+ if self._settings.wireless_security.group is not None:
+ config.set(identifier, 'group',
+ self._settings.wireless_security.group)
+ if self._settings.wireless.security is not None:
+ config.set(identifier, 'security',
+ self._settings.wireless.security)
+ if self._secrets is not None:
+ if self._settings.wireless_security.key_mgmt == 'none':
+ config.set(identifier, 'key', self._secrets.wep_key)
+ config.set(identifier, 'auth-alg', self._secrets.auth_alg)
+ elif self._settings.wireless_security.key_mgmt == 'wpa-psk':
+ config.set(identifier, 'key', self._secrets.psk)
+ except ConfigParser.Error, e:
+ logging.exception('Error constructing %s', identifier)
+ else:
+ f = open(config_path, 'w')
+ try:
+ config.write(f)
+ except ConfigParser.Error:
+ logging.exception('Can not write %s', config_path)
+ f.close()
+
+ @dbus.service.method(dbus_interface=NM_CONNECTION_IFACE,
+ in_signature='', out_signature='a{sa{sv}}')
+ def GetSettings(self):
+ return self._settings.get_dict()
+
+ @dbus.service.method(dbus_interface=NM_SECRETS_IFACE,
+ async_callbacks=('reply', 'error'),
+ 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')
+ else:
+ reply(self._secrets.get_dict())
+
+
+class AccessPoint(gobject.GObject):
+ __gsignals__ = {
+ 'props-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+
+ def __init__(self, device, model):
+ self.__gobject_init__()
+ self.device = device
+ self.model = model
+
+ self._initialized = False
+ self._bus = dbus.SystemBus()
+
+ self.name = ''
+ self.strength = 0
+ self.flags = 0
+ 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.GetAll(NM_ACCESSPOINT_IFACE, byte_arrays=True,
+ reply_handler=self._ap_properties_changed_cb,
+ error_handler=self._get_all_props_error_cb)
+
+ self._bus.add_signal_receiver(self._ap_properties_changed_cb,
+ signal_name='PropertiesChanged',
+ path=self.model.object_path,
+ dbus_interface=NM_ACCESSPOINT_IFACE,
+ byte_arrays=True)
+
+ def network_hash(self):
+ """
+ This is a hash which uniquely identifies the network that this AP
+ is a bridge to. i.e. its expected for 2 APs with identical SSID and
+ other settings to have the same network hash, because we assume that
+ they are a part of the same underlying network.
+ """
+
+ # based on logic from nm-applet
+ fl = 0
+
+ if self.mode == NM_802_11_MODE_INFRA:
+ fl |= 1 << 0
+ elif self.mode == NM_802_11_MODE_ADHOC:
+ fl |= 1 << 1
+ else:
+ fl |= 1 << 2
+
+ # Separate out no encryption, WEP-only, and WPA-capable */
+ if (not (self.flags & NM_802_11_AP_FLAGS_PRIVACY)) \
+ and self.wpa_flags == NM_802_11_AP_SEC_NONE \
+ and self.rsn_flags == NM_802_11_AP_SEC_NONE:
+ fl |= 1 << 3
+ elif (self.flags & NM_802_11_AP_FLAGS_PRIVACY) \
+ and self.wpa_flags == NM_802_11_AP_SEC_NONE \
+ and self.rsn_flags == NM_802_11_AP_SEC_NONE:
+ fl |= 1 << 4
+ elif (not (self.flags & NM_802_11_AP_FLAGS_PRIVACY)) \
+ and self.wpa_flags != NM_802_11_AP_SEC_NONE \
+ and self.rsn_flags != NM_802_11_AP_SEC_NONE:
+ fl |= 1 << 5
+ else:
+ fl |= 1 << 6
+
+ hashstr = str(fl) + "@" + self.name
+ return hash(hashstr)
+
+ def _update_properties(self, properties):
+ if self._initialized:
+ old_hash = self.network_hash()
+ else:
+ old_hash = None
+
+ if 'Ssid' in properties:
+ self.name = properties['Ssid']
+ if 'Strength' in properties:
+ self.strength = properties['Strength']
+ if 'Flags' in properties:
+ self.flags = properties['Flags']
+ if 'WpaFlags' in properties:
+ self.wpa_flags = properties['WpaFlags']
+ if 'RsnFlags' in properties:
+ 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)
+
+ def _get_all_props_error_cb(self, err):
+ logging.error('Error getting the access point properties: %s', err)
+
+ def _ap_properties_changed_cb(self, properties):
+ self._update_properties(properties)
+
+ def disconnect(self):
+ self._bus.remove_signal_receiver(self._ap_properties_changed_cb,
+ signal_name='PropertiesChanged',
+ path=self.model.object_path,
+ dbus_interface=NM_ACCESSPOINT_IFACE)
+
+def get_settings():
+ global _nm_settings
+ if _nm_settings is None:
+ try:
+ _nm_settings = NMSettings()
+ except dbus.DBusException:
+ logging.exception('Cannot create the UserSettings service.')
+ 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
+
+ return None
+
+def add_connection(uuid, settings, secrets=None):
+ global _conn_counter
+
+ path = NM_SETTINGS_PATH + '/' + str(_conn_counter)
+ _conn_counter += 1
+
+ conn = NMSettingsConnection(path, settings, secrets)
+ _nm_settings.add_connection(uuid, conn)
+ return conn
+
+def load_wifi_connections():
+ profile_path = env.get_profile_path()
+ config_path = os.path.join(profile_path, 'nm', 'connections.cfg')
+
+ config = ConfigParser.ConfigParser()
+
+ 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()
+
+ try:
+ if not config.read(config_path):
+ logging.error('Error reading the nm config file')
+ return
+ except ConfigParser.ParsingError:
+ logging.exception('Error reading the nm config file')
+ return
+
+ for section in config.sections():
+ try:
+ settings = Settings()
+ settings.connection.id = section
+ ssid = config.get(section, 'ssid')
+ settings.wireless.ssid = dbus.ByteArray(ssid)
+ uuid = config.get(section, 'uuid')
+ settings.connection.uuid = uuid
+ nmtype = config.get(section, 'type')
+ settings.connection.type = nmtype
+ autoconnect = bool(config.get(section, 'autoconnect'))
+ settings.connection.autoconnect = autoconnect
+
+ if config.has_option(section, 'timestamp'):
+ timestamp = int(config.get(section, 'timestamp'))
+ settings.connection.timestamp = timestamp
+
+ secrets = None
+ if config.has_option(section, 'key-mgmt'):
+ secrets = Secrets(settings)
+ settings.wireless_security = WirelessSecurity()
+ mgmt = config.get(section, 'key-mgmt')
+ settings.wireless_security.key_mgmt = mgmt
+ security = config.get(section, 'security')
+ settings.wireless.security = security
+ key = config.get(section, 'key')
+ if mgmt == 'none':
+ secrets.wep_key = key
+ auth_alg = config.get(section, 'auth-alg')
+ secrets.auth_alg = auth_alg
+ elif mgmt == 'wpa-psk':
+ secrets.psk = key
+ if config.has_option(section, 'proto'):
+ value = config.get(section, 'proto')
+ settings.wireless_security.proto = value
+ if config.has_option(section, 'group'):
+ value = config.get(section, 'group')
+ settings.wireless_security.group = value
+ if config.has_option(section, 'pairwise'):
+ value = config.get(section, 'pairwise')
+ settings.wireless_security.pairwise = value
+ except ConfigParser.Error:
+ logging.exception('Error reading section')
+ else:
+ add_connection(uuid, settings, secrets)
+
+
+def load_gsm_connection():
+ _BAUD_RATE = 115200
+
+ client = gconf.client_get_default()
+
+ username = client.get_string(GSM_USERNAME_PATH) or ''
+ password = client.get_string(GSM_PASSWORD_PATH) or ''
+ number = client.get_string(GSM_NUMBER_PATH) or ''
+ apn = client.get_string(GSM_APN_PATH) or ''
+ pin = client.get_string(GSM_PIN_PATH) or ''
+ puk = client.get_string(GSM_PUK_PATH) or ''
+
+ if username and number and apn:
+ settings = SettingsGsm()
+ settings.gsm.username = username
+ settings.gsm.number = number
+ settings.gsm.apn = apn
+
+ secrets = SecretsGsm()
+ secrets.pin = pin
+ secrets.puk = puk
+ secrets.password = password
+
+ settings.connection.id = 'gsm'
+ settings.connection.type = NM_CONNECTION_TYPE_GSM
+ uuid = settings.connection.uuid = unique_id()
+ settings.connection.autoconnect = False
+ settings.ip4_config.method = 'auto'
+ settings.serial.baud = _BAUD_RATE
+
+ try:
+ add_connection(uuid, settings, secrets)
+ except Exception:
+ logging.exception('Error adding gsm connection to NMSettings.')
+ else:
+ logging.exception("No gsm connection was set in GConf.")
+
+def load_connections():
+ load_wifi_connections()
+ load_gsm_connection()
+
+def find_gsm_connection():
+ connections = get_settings().connections
+
+ for connection in connections.values():
+ if connection.get_settings().connection.type == NM_CONNECTION_TYPE_GSM:
+ return connection
+
+ logging.debug('There is no gsm connection in the NMSettings.')
+ return None
diff --git a/shell/src/jarabe/model/notifications.py b/shell/src/jarabe/model/notifications.py
new file mode 100644
index 0000000..f2e2d65
--- /dev/null
+++ b/shell/src/jarabe/model/notifications.py
@@ -0,0 +1,95 @@
+# 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 sys
+import logging
+
+import dbus
+
+from sugar import dispatch
+
+from jarabe import config
+
+_DBUS_SERVICE = "org.freedesktop.Notifications"
+_DBUS_IFACE = "org.freedesktop.Notifications"
+_DBUS_PATH = "/org/freedesktop/Notifications"
+
+class NotificationService(dbus.service.Object):
+ def __init__(self):
+ bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, _DBUS_PATH)
+
+ self._notification_counter = 0
+ self.notification_received = dispatch.Signal()
+ self.notification_cancelled = dispatch.Signal()
+
+ @dbus.service.method(_DBUS_IFACE,
+ in_signature='susssava{sv}i', out_signature='u')
+ def Notify(self, app_name, replaces_id, app_icon, summary, body, actions,
+ hints, expire_timeout):
+
+ logging.debug('Received notification: %r', [app_name, replaces_id,
+ '<app_icon>', summary, body, actions, '<hints>', expire_timeout])
+
+ if replaces_id > 0:
+ notification_id = replaces_id
+ else:
+ if self._notification_counter == sys.maxint:
+ self._notification_counter = 1
+ else:
+ self._notification_counter += 1
+ notification_id = self._notification_counter
+
+ self.notification_received.send(self, app_name=app_name,
+ replaces_id=replaces_id, app_icon=app_icon, summary=summary,
+ body=body, actions=actions, hints=hints,
+ expire_timeout=expire_timeout)
+
+ return notification_id
+
+ @dbus.service.method(_DBUS_IFACE, in_signature='u', out_signature='')
+ def CloseNotification(self, notification_id):
+ self.notification_cancelled.send(self, notification_id=notification_id)
+
+ @dbus.service.method(_DBUS_IFACE, in_signature='', out_signature='as')
+ def GetCapabilities(self):
+ return []
+
+ @dbus.service.method(_DBUS_IFACE, in_signature='', out_signature='sss')
+ def GetServerInformation(self, name, vendor, version):
+ return 'Sugar Shell', 'Sugar', config.version
+
+
+ @dbus.service.signal(_DBUS_IFACE, signature="uu")
+ def NotificationClosed(self, notification_id, reason):
+ pass
+
+ @dbus.service.signal(_DBUS_IFACE, signature="us")
+ def ActionInvoked(self, notification_id, action_key):
+ pass
+
+_instance = None
+
+def get_service():
+ global _instance
+ if not _instance:
+ _instance = NotificationService()
+ return _instance
+
+def init():
+ get_service()
+
diff --git a/shell/src/jarabe/model/olpcmesh.py b/shell/src/jarabe/model/olpcmesh.py
new file mode 100644
index 0000000..60f6be4
--- /dev/null
+++ b/shell/src/jarabe/model/olpcmesh.py
@@ -0,0 +1,214 @@
+# 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
+
+import logging
+
+import dbus
+import gobject
+
+from jarabe.model import network
+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"
+
+DEVICE_STATE_UNKNOWN = 0
+DEVICE_STATE_UNMANAGED = 1
+DEVICE_STATE_UNAVAILABLE = 2
+DEVICE_STATE_DISCONNECTED = 3
+DEVICE_STATE_PREPARE = 4
+DEVICE_STATE_CONFIG = 5
+DEVICE_STATE_NEED_AUTH = 6
+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()
+
+ self.mesh_device = mesh_device
+ self.eth_device = self._get_companion_device()
+
+ self._connection_queue = []
+ """Stack of connections that we'll iterate through until we find one
+ that works.
+
+ """
+
+ props = dbus.Interface(self.mesh_device,
+ 'org.freedesktop.DBus.Properties')
+ 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.Get(_NM_DEVICE_IFACE, 'State',
+ reply_handler=self.__get_eth_state_reply_cb,
+ error_handler=self.__get_state_error_cb)
+
+ self._bus.add_signal_receiver(self.__eth_device_state_changed_cb,
+ signal_name='StateChanged',
+ path=self.eth_device.object_path,
+ dbus_interface=_NM_DEVICE_IFACE)
+
+ self._bus.add_signal_receiver(self.__mshdev_state_changed_cb,
+ signal_name='StateChanged',
+ path=self.mesh_device.object_path,
+ dbus_interface=_NM_DEVICE_IFACE)
+
+ self._idle_source = 0
+ self._mesh_device_state = DEVICE_STATE_UNKNOWN
+ self._eth_device_state = DEVICE_STATE_UNKNOWN
+
+ if self._have_configured_connections():
+ self._start_automesh_timer()
+ else:
+ self._start_automesh()
+
+ def _get_companion_device(self):
+ props = dbus.Interface(self.mesh_device,
+ 'org.freedesktop.DBus.Properties')
+ eth_device_o = props.Get(_NM_OLPC_MESH_IFACE, 'Companion')
+ return self._bus.get_object(_NM_SERVICE, eth_device_o)
+
+ def _have_configured_connections(self):
+ return len(network.get_settings().connections) > 0
+
+ def _start_automesh_timer(self):
+ """Start our timer system which basically looks for 10 seconds of
+ inactivity on both devices, then starts automesh.
+
+ """
+ if self._idle_source != 0:
+ gobject.source_remove(self._idle_source)
+ self._idle_source = gobject.timeout_add_seconds(10, self._idle_check)
+
+ def __get_state_error_cb(self, err):
+ logging.debug('Error getting the device state: %s', err)
+
+ def __get_mesh_state_reply_cb(self, state):
+ self._mesh_device_state = state
+ self._maybe_schedule_idle_check()
+
+ def __get_eth_state_reply_cb(self, state):
+ self._eth_device_state = state
+ self._maybe_schedule_idle_check()
+
+ 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()
+
+ if new_state >= DEVICE_STATE_PREPARE \
+ and new_state <= DEVICE_STATE_ACTIVATED \
+ and len(self._connection_queue) > 0:
+ self._connection_queue = []
+
+ def __mshdev_state_changed_cb(self, new_state, old_state, reason):
+ self._mesh_device_state = new_state
+ self._maybe_schedule_idle_check()
+
+ if new_state == DEVICE_STATE_FAILED:
+ self._try_next_connection_from_queue()
+ elif new_state == DEVICE_STATE_ACTIVATED \
+ and len(self._connection_queue) > 0:
+ self._empty_connection_queue()
+
+ def _maybe_schedule_idle_check(self):
+ if self._mesh_device_state == DEVICE_STATE_DISCONNECTED \
+ and self._eth_device_state == DEVICE_STATE_DISCONNECTED:
+ self._start_automesh_timer()
+
+ 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")
+ self._start_automesh()
+ return False
+
+ def _make_connection(self, channel, anycast_address=None):
+ wireless_config = OlpcMeshSettings(channel, anycast_address)
+ settings = Settings(wireless_cfg=wireless_config)
+ if not anycast_address:
+ settings.ip4_config = network.IP4Config()
+ settings.ip4_config.method = 'link-local'
+ settings.connection.id = 'olpc-mesh-' + str(channel)
+ settings.connection.uuid = unique_id()
+ settings.connection.type = '802-11-olpc-mesh'
+ connection = network.add_connection(settings.connection.id, settings)
+ return connection
+
+ 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 _activate_connection(self, channel, anycast_address=None):
+ 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)
+
+ network_manager.ActivateConnection(network.SETTINGS_SERVICE,
+ connection.path,
+ self.mesh_device.object_path,
+ self.mesh_device.object_path,
+ reply_handler=self.__activate_reply_cb,
+ error_handler=self.__activate_error_cb)
+
+ def _try_next_connection_from_queue(self):
+ if len(self._connection_queue) == 0:
+ return
+
+ channel, anycast = self._connection_queue.pop()
+ self._activate_connection(channel, anycast)
+
+ def _empty_connection_queue(self):
+ self._connection_queue = []
+
+ def user_activate_channel(self, channel):
+ """Activate a mesh connection on a user-specified channel.
+ Looks for XS first, then resorts to simple mesh."""
+ self._empty_connection_queue()
+ self._connection_queue.append((channel, None))
+ self._connection_queue.append((channel, _XS_ANYCAST))
+ self._try_next_connection_from_queue()
+
+ def _start_automesh(self):
+ """Start meshing automatically, intended when there are no better
+ networks to connect to. First looks for XS on all channels, then falls
+ back to simple mesh on channel 1."""
+ self._empty_connection_queue()
+ self._connection_queue.append((1, None))
+ self._connection_queue.append((11, _XS_ANYCAST))
+ self._connection_queue.append((6, _XS_ANYCAST))
+ self._connection_queue.append((1, _XS_ANYCAST))
+ self._try_next_connection_from_queue()
+
diff --git a/shell/src/jarabe/model/screen.py b/shell/src/jarabe/model/screen.py
new file mode 100644
index 0000000..4403c1c
--- /dev/null
+++ b/shell/src/jarabe/model/screen.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2006-2008 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import 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:
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(_HARDWARE_MANAGER_SERVICE,
+ _HARDWARE_MANAGER_OBJECT_PATH,
+ follow_name_owner_changes=True)
+ _ohm_service = dbus.Interface(proxy, _HARDWARE_MANAGER_INTERFACE)
+
+ return _ohm_service
+
+def set_dcon_freeze(frozen):
+ try:
+ _get_ohm().SetKey("display.dcon_freeze", frozen)
+ except dbus.DBusException:
+ logging.error('Cannot unfreeze the DCON')
+
diff --git a/shell/src/jarabe/model/session.py b/shell/src/jarabe/model/session.py
new file mode 100644
index 0000000..9e0f087
--- /dev/null
+++ b/shell/src/jarabe/model/session.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2008, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import dbus
+import os
+import signal
+import sys
+import logging
+
+from sugar import session
+from sugar import env
+
+_session_manager = None
+
+class SessionManager(session.SessionManager):
+ MODE_LOGOUT = 0
+ MODE_SHUTDOWN = 1
+ MODE_REBOOT = 2
+
+ def __init__(self):
+ session.SessionManager.__init__(self)
+ self._logout_mode = None
+
+ def logout(self):
+ self._logout_mode = self.MODE_LOGOUT
+ self.initiate_shutdown()
+
+ def shutdown(self):
+ self._logout_mode = self.MODE_SHUTDOWN
+ self.initiate_shutdown()
+
+ def reboot(self):
+ self._logout_mode = self.MODE_REBOOT
+ self.initiate_shutdown()
+
+ def shutdown_completed(self):
+ if env.is_emulator():
+ self._close_emulator()
+ elif self._logout_mode != self.MODE_LOGOUT:
+ try:
+ bus = dbus.SystemBus()
+ proxy = bus.get_object('org.freedesktop.Hal',
+ '/org/freedesktop/Hal/devices/computer')
+ pm = dbus.Interface(proxy,
+ 'org.freedesktop.Hal.Device.SystemPowerManagement')
+
+ if self._logout_mode == self.MODE_SHUTDOWN:
+ pm.Shutdown()
+ elif self._logout_mode == self.MODE_REBOOT:
+ pm.Reboot()
+ except:
+ logging.exception('Can not stop sugar')
+ self.session.cancel_shutdown()
+ return
+
+ session.SessionManager.shutdown_completed(self)
+ gtk.main_quit()
+
+ def _close_emulator(self):
+ gtk.main_quit()
+
+ if os.environ.has_key('SUGAR_EMULATOR_PID'):
+ 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
+ sys.exit()
+
+def get_session_manager():
+ global _session_manager
+
+ if _session_manager == None:
+ _session_manager = SessionManager()
+ return _session_manager
diff --git a/shell/src/jarabe/model/shell.py b/shell/src/jarabe/model/shell.py
new file mode 100644
index 0000000..db0e050
--- /dev/null
+++ b/shell/src/jarabe/model/shell.py
@@ -0,0 +1,641 @@
+# Copyright (C) 2006-2007 Owen Williams.
+# Copyright (C) 2006-2008 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import time
+
+import gconf
+import wnck
+import gobject
+import gtk
+import dbus
+
+from sugar import wm
+from sugar import dispatch
+from sugar.graphics.xocolor import XoColor
+
+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"
+
+
+class Activity(gobject.GObject):
+ """Activity which appears in the "Home View" of the Sugar shell
+
+ This class stores the Sugar Shell's metadata regarding a
+ given activity/application in the system. It interacts with
+ the sugar.activity.* modules extensively in order to
+ accomplish its tasks.
+ """
+
+ __gtype_name__ = 'SugarHomeActivity'
+
+ LAUNCHING = 0
+ LAUNCH_FAILED = 1
+ LAUNCHED = 2
+
+ def __init__(self, activity_info, activity_id, window=None):
+ """Initialise the HomeActivity
+
+ activity_info -- sugar.activity.registry.ActivityInfo instance,
+ provides the information required to actually
+ create the new instance. This is, in effect,
+ the "type" of activity being created.
+ activity_id -- unique identifier for this instance
+ of the activity type
+ window -- Main WnckWindow of the activity
+ """
+ gobject.GObject.__init__(self)
+
+ self._window = None
+ self._service = None
+ self._activity_id = activity_id
+ self._activity_info = activity_info
+ self._launch_time = time.time()
+ self._launch_status = Activity.LAUNCHING
+
+ if window is not None:
+ self.set_window(window)
+
+ self._retrieve_service()
+
+ self._name_owner_changed_handler = None
+ if not self._service:
+ bus = dbus.SessionBus()
+ self._name_owner_changed_handler = bus.add_signal_receiver(
+ self._name_owner_changed_cb,
+ signal_name="NameOwnerChanged",
+ dbus_interface="org.freedesktop.DBus")
+
+ 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)
+
+ def get_launch_status(self):
+ return self._launch_status
+
+ launch_status = gobject.property(getter=get_launch_status)
+
+ def set_window(self, window):
+ """Set the window for the activity
+
+ We allow resetting the window for an activity so that we
+ can replace the launcher once we get its real window.
+ """
+ if not window:
+ raise ValueError("window must be valid")
+ self._window = window
+
+ def get_service(self):
+ """Get the activity service
+
+ Note that non-native Sugar applications will not have
+ such a service, so the return value will be None in
+ those cases.
+ """
+
+ return self._service
+
+ def get_title(self):
+ """Retrieve the application's root window's suggested title"""
+ if self._window:
+ return self._window.get_name()
+ else:
+ return ''
+
+ def get_icon_path(self):
+ """Retrieve the activity's icon (file) name"""
+ if self.is_journal():
+ icon_theme = gtk.icon_theme_get_default()
+ info = icon_theme.lookup_icon('activity-journal',
+ gtk.ICON_SIZE_SMALL_TOOLBAR, 0)
+ if not info:
+ return None
+ fname = info.get_filename()
+ del info
+ return fname
+ elif self._activity_info:
+ return self._activity_info.get_icon()
+ else:
+ return None
+
+ def get_icon_color(self):
+ """Retrieve the appropriate icon colour for this activity
+
+ Uses activity_id to index into the PresenceService's
+ set of activity colours, if the PresenceService does not
+ have an entry (implying that this is not a Sugar-shared application)
+ uses the local user's profile colour for the icon.
+ """
+ # 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 neighborhood.get_model().get_activities():
+ if self._activity_id == act.activity_id:
+ activity = act
+ break
+
+ if activity != None:
+ return activity.props.color
+ else:
+ client = gconf.client_get_default()
+ return XoColor(client.get_string("/desktop/sugar/user/color"))
+
+ def get_activity_id(self):
+ """Retrieve the "activity_id" passed in to our constructor
+
+ This is a "globally likely unique" identifier generated by
+ sugar.util.unique_id
+ """
+ return self._activity_id
+
+ def get_xid(self):
+ """Retrieve the X-windows ID of our root window"""
+ if self._window is not None:
+ return self._window.get_xid()
+ else:
+ return None
+
+ def get_window(self):
+ """Retrieve the X-windows root window of this application
+
+ This was stored by the set_window method, which was
+ called by HomeModel._add_activity, which was called
+ via a callback that looks for all 'window-opened'
+ events.
+
+ HomeModel currently uses a dbus service query on the
+ activity to determine to which HomeActivity the newly
+ launched window belongs.
+ """
+ return self._window
+
+ def get_type(self):
+ """Retrieve the activity bundle id for future reference"""
+ if self._window is None:
+ return None
+ else:
+ return wm.get_bundle_id(self._window)
+
+ def is_journal(self):
+ """Returns boolean if the activity is of type JournalActivity"""
+ return self.get_type() == 'org.laptop.JournalActivity'
+
+ def get_launch_time(self):
+ """Return the time at which the activity was first launched
+
+ Format is floating-point time.time() value
+ (seconds since the epoch)
+ """
+ return self._launch_time
+
+ def get_pid(self):
+ """Returns the activity's PID"""
+ return self._window.get_pid()
+
+ def get_bundle_path(self):
+ """Returns the activity's bundle directory"""
+ if self._activity_info is None:
+ return None
+ else:
+ return self._activity_info.get_path()
+
+ def get_activity_name(self):
+ """Returns the activity's bundle name"""
+ if self._activity_info is None:
+ return None
+ else:
+ return self._activity_info.get_name()
+
+ def equals(self, activity):
+ if self._activity_id and activity.get_activity_id():
+ return self._activity_id == activity.get_activity_id()
+ if self._window.get_xid() and activity.get_xid():
+ return self._window.get_xid() == activity.get_xid()
+ return False
+
+ def _get_service_name(self):
+ if self._activity_id:
+ return _SERVICE_NAME + self._activity_id
+ else:
+ return None
+
+ def _retrieve_service(self):
+ if not self._activity_id:
+ return
+
+ try:
+ bus = dbus.SessionBus()
+ proxy = bus.get_object(self._get_service_name(),
+ _SERVICE_PATH + "/" + self._activity_id)
+ self._service = dbus.Interface(proxy, _SERVICE_INTERFACE)
+ except dbus.DBusException:
+ self._service = None
+
+ def _name_owner_changed_cb(self, name, old, new):
+ if name == self._get_service_name():
+ 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"""
+ if self._service is not None:
+ self._service.SetActive(state,
+ reply_handler=self._set_active_success,
+ error_handler=self._set_active_error)
+
+ def _set_active_success(self):
+ pass
+
+ def _set_active_error(self, err):
+ logging.error("set_active() failed: %s", err)
+
+ 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)
+
+ The ShellModel is basically the point of registration
+ for all running activities within Sugar. It traps
+ events that tell the system there is a new activity
+ being created (generated by the activity factories),
+ or removed, as well as those which tell us that the
+ currently focussed activity has changed.
+
+ The HomeModel tracks a set of HomeActivity instances,
+ which are tracking the window to activity mappings
+ the activity factories have set up.
+ """
+
+ __gsignals__ = {
+ 'activity-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'activity-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'active-activity-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'tabbing-activity-changed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'launch-started': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'launch-completed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT])),
+ 'launch-failed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([gobject.TYPE_PYOBJECT]))
+ }
+
+ ZOOM_MESH = 0
+ ZOOM_GROUP = 1
+ ZOOM_HOME = 2
+ ZOOM_ACTIVITY = 3
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._screen = wnck.screen_get_default()
+ self._screen.connect('window-opened', self._window_opened_cb)
+ self._screen.connect('window-closed', self._window_closed_cb)
+ self._screen.connect('active-window-changed',
+ self._active_window_changed_cb)
+
+ self.zoom_level_changed = dispatch.Signal()
+
+ self._desktop_level = self.ZOOM_HOME
+ self._zoom_level = self.ZOOM_HOME
+ self._current_activity = None
+ self._activities = []
+ self._active_activity = None
+ self._tabbing_activity = None
+ 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
+ elif window.get_window_type() == wnck.WINDOW_NORMAL:
+ new_level = self.ZOOM_ACTIVITY
+ else:
+ new_level = self._desktop_level
+
+ if self._zoom_level != new_level:
+ old_level = self._zoom_level
+ self._zoom_level = new_level
+ self.zoom_level_changed.send(self, old_level=old_level,
+ new_level=new_level)
+
+ def set_zoom_level(self, new_level, x_event_time=0):
+ old_level = self.zoom_level
+ if old_level == new_level:
+ return
+
+ if old_level != self.ZOOM_ACTIVITY:
+ screen = gtk.gdk.screen_get_default()
+ active_window_type = screen.get_active_window().get_type_hint()
+ if active_window_type != gtk.gdk.WINDOW_TYPE_HINT_DESKTOP:
+ return
+
+ self._zoom_level = new_level
+ if new_level is not self.ZOOM_ACTIVITY:
+ self._desktop_level = new_level
+
+ self.zoom_level_changed.send(self, old_level=old_level,
+ new_level=new_level)
+
+ show_desktop = new_level is not self.ZOOM_ACTIVITY
+ self._screen.toggle_showing_desktop(show_desktop)
+
+ if new_level is self.ZOOM_ACTIVITY:
+ # activate the window, in case it was iconified
+ # (e.g. during sugar launch, the Journal starts in this state)
+ window = self._active_activity.get_window()
+ if window:
+ window.activate(x_event_time or gtk.get_current_event_time())
+
+ def _get_zoom_level(self):
+ return self._zoom_level
+
+ zoom_level = property(_get_zoom_level)
+
+ def _get_activities_with_window(self):
+ ret = []
+ for i in self._activities:
+ if i.get_window() is not None:
+ ret.append(i)
+ return ret
+
+ def get_previous_activity(self, current=None):
+ if not current:
+ current = self._active_activity
+
+ activities = self._get_activities_with_window()
+ i = activities.index(current)
+ if len(activities) == 0:
+ return None
+ elif i - 1 >= 0:
+ return activities[i - 1]
+ else:
+ return activities[len(activities) - 1]
+
+ def get_next_activity(self, current=None):
+ if not current:
+ current = self._active_activity
+
+ activities = self._get_activities_with_window()
+ i = activities.index(current)
+ if len(activities) == 0:
+ return None
+ elif i + 1 < len(activities):
+ return activities[i + 1]
+ else:
+ return activities[0]
+
+ def get_active_activity(self):
+ """Returns the activity that the user is currently working in"""
+ return self._active_activity
+
+ def get_tabbing_activity(self):
+ """Returns the activity that is currently highlighted during tabbing"""
+ return self._tabbing_activity
+
+ def set_tabbing_activity(self, activity):
+ """Sets the activity that is currently highlighted during tabbing"""
+ self._tabbing_activity = activity
+ self.emit("tabbing-activity-changed", self._tabbing_activity)
+
+ def _set_active_activity(self, home_activity):
+ if self._active_activity == home_activity:
+ return
+
+ if home_activity:
+ home_activity.set_active(True)
+
+ if self._active_activity:
+ self._active_activity.set_active(False)
+
+ self._active_activity = home_activity
+ self.emit('active-activity-changed', self._active_activity)
+
+ def __iter__(self):
+ return iter(self._activities)
+
+ def __len__(self):
+ return len(self._activities)
+
+ def __getitem__(self, i):
+ return self._activities[i]
+
+ def index(self, obj):
+ return self._activities.index(obj)
+
+ def _window_opened_cb(self, screen, window):
+ if window.get_window_type() == wnck.WINDOW_NORMAL:
+ home_activity = None
+
+ activity_id = wm.get_activity_id(window)
+
+ service_name = wm.get_bundle_id(window)
+ if service_name:
+ registry = get_registry()
+ activity_info = registry.get_bundle(service_name)
+ else:
+ activity_info = None
+
+ if activity_id:
+ home_activity = self.get_activity_by_id(activity_id)
+
+ xid = window.get_xid()
+ gdk_window = gtk.gdk.window_foreign_new(xid)
+ gdk_window.set_decorations(0)
+
+ window.maximize()
+
+ if not home_activity:
+ 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':
+ 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)
+
+ 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())
+
+ def _get_activity_by_xid(self, xid):
+ for home_activity in self._activities:
+ if home_activity.get_xid() == xid:
+ return home_activity
+ return None
+
+ def get_activity_by_id(self, activity_id):
+ for home_activity in self._activities:
+ if home_activity.get_activity_id() == activity_id:
+ return home_activity
+ return None
+
+ def _active_window_changed_cb(self, screen, previous_window=None):
+ window = screen.get_active_window()
+ if window is None:
+ return
+
+ if window.get_window_type() != wnck.WINDOW_DIALOG:
+ while window.get_transient() is not None:
+ window = window.get_transient()
+
+ act = self._get_activity_by_xid(window.get_xid())
+ if act is not None:
+ self._set_active_activity(act)
+
+ self._update_zoom_level(window)
+
+ def _add_activity(self, home_activity):
+ self._activities.append(home_activity)
+ self.emit('activity-added', home_activity)
+
+ def _remove_activity(self, home_activity):
+ if home_activity == self._active_activity:
+ windows = wnck.screen_get_default().get_windows_stacked()
+ windows.reverse()
+ for window in windows:
+ new_activity = self._get_activity_by_xid(window.get_xid())
+ if new_activity is not None:
+ self._set_active_activity(new_activity)
+ break
+ else:
+ logging.error('No activities are running')
+ self._set_active_activity(None)
+
+ self.emit('activity-removed', home_activity)
+ self._activities.remove(home_activity)
+
+ def _remove_activity_by_xid(self, xid):
+ home_activity = self._get_activity_by_xid(xid)
+ if home_activity:
+ self._remove_activity(home_activity)
+ else:
+ logging.error('Model for window %d does not exist.', xid)
+
+ def notify_launch(self, activity_id, service_name):
+ registry = get_registry()
+ activity_info = registry.get_bundle(service_name)
+ if not activity_info:
+ raise ValueError("Activity service name '%s'" \
+ " was not found in the bundle registry."
+ % service_name)
+ home_activity = Activity(activity_info, activity_id)
+ self._add_activity(home_activity)
+
+ self._set_active_activity(home_activity)
+
+ self.emit('launch-started', home_activity)
+
+ # FIXME: better learn about finishing processes by receiving a signal.
+ # Now just check whether an activity has a window after ~90sec
+ gobject.timeout_add_seconds(90, self._check_activity_launched,
+ activity_id)
+
+ def notify_launch_failed(self, activity_id):
+ home_activity = self.get_activity_by_id(activity_id)
+ if home_activity:
+ logging.debug("Activity %s (%s) launch failed", activity_id,
+ home_activity.get_type())
+ 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.',
+ activity_id)
+
+ def _check_activity_launched(self, activity_id):
+ home_activity = self.get_activity_by_id(activity_id)
+
+ if not home_activity:
+ logging.debug('Activity %s has been closed already.', activity_id)
+ return False
+
+ if 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/shell/src/jarabe/model/sound.py b/shell/src/jarabe/model/sound.py
new file mode 100644
index 0000000..65090a4
--- /dev/null
+++ b/shell/src/jarabe/model/sound.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2006-2008 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gconf
+
+from sugar import env
+from sugar import _sugarext
+from sugar import dispatch
+
+VOLUME_STEP = 10
+
+muted_changed = dispatch.Signal()
+volume_changed = dispatch.Signal()
+
+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)
+
+ volume_changed.send(None)
+ save()
+
+def set_muted(new_state):
+ old_state = _volume.get_mute()
+ _volume.set_mute(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/shell/src/jarabe/model/telepathyclient.py b/shell/src/jarabe/model/telepathyclient.py
new file mode 100644
index 0000000..f4eccc3
--- /dev/null
+++ b/shell/src/jarabe/model/telepathyclient.py
@@ -0,0 +1,100 @@
+# 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'
+
+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)
+
+_instance = None
+
+def get_instance():
+ global _instance
+ if not _instance:
+ _instance = TelepathyClient()
+ return _instance
diff --git a/shell/src/jarabe/util/Makefile.am b/shell/src/jarabe/util/Makefile.am
new file mode 100644
index 0000000..8bda3d6
--- /dev/null
+++ b/shell/src/jarabe/util/Makefile.am
@@ -0,0 +1,7 @@
+SUBDIRS = \
+ telepathy
+
+sugardir = $(pythondir)/jarabe/util
+sugar_PYTHON = \
+ __init__.py \
+ emulator.py
diff --git a/shell/src/jarabe/util/__init__.py b/shell/src/jarabe/util/__init__.py
new file mode 100644
index 0000000..1610dd0
--- /dev/null
+++ b/shell/src/jarabe/util/__init__.py
@@ -0,0 +1,19 @@
+"""OLPC Sugar Jarabe utility modules
+"""
+
+# 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
+
diff --git a/shell/src/jarabe/util/emulator.py b/shell/src/jarabe/util/emulator.py
new file mode 100644
index 0000000..5a99dbe
--- /dev/null
+++ b/shell/src/jarabe/util/emulator.py
@@ -0,0 +1,177 @@
+# Copyright (C) 2006-2008, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import signal
+import subprocess
+import sys
+import time
+from optparse import OptionParser
+
+import gtk
+import gobject
+
+from sugar import env
+
+
+ERROR_NO_DISPLAY = 30
+ERROR_NO_SERVER = 31
+
+
+default_dimensions = (800, 600)
+def _run_xephyr(display, dpi, dimensions, fullscreen):
+ cmd = [ 'Xephyr' ]
+ cmd.append(':%d' % display)
+ cmd.append('-ac')
+
+ screen_size = (gtk.gdk.screen_width(), gtk.gdk.screen_height())
+
+ if (not dimensions) and (fullscreen is None) and \
+ (screen_size < default_dimensions) :
+ # no forced settings, screen too small => fit screen
+ fullscreen = True
+ 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 :
+ dpi = gtk.settings_get_default().get_property('gtk-xft-dpi') / 1024
+
+ if fullscreen :
+ cmd.append('-fullscreen')
+
+ if dimensions :
+ cmd.append('-screen')
+ cmd.append(dimensions)
+
+ if dpi :
+ cmd.append('-dpi')
+ cmd.append('%d' % dpi)
+
+ cmd.append('-noreset')
+
+ try:
+ pipe = subprocess.Popen(cmd)
+
+ except OSError, exc:
+ sys.stderr.write('Error executing server: %s\n' % (exc, ))
+ return None
+
+ return pipe
+
+
+def _check_server(display):
+ result = subprocess.call(['xdpyinfo', '-display', ':%d' % display],
+ stdout=open(os.devnull, "w"),
+ stderr=open(os.devnull, "w"))
+ return result == 0
+
+
+def _kill_pipe(pipe):
+ """Terminate and wait for child process."""
+ try:
+ os.kill(pipe.pid, signal.SIGTERM)
+ except OSError:
+ pass
+
+ pipe.wait()
+
+
+def _start_xephyr(dpi, dimensions, fullscreen):
+ for display in range(30, 40):
+ if not _check_server(display):
+ pipe = _run_xephyr(display, dpi, dimensions, fullscreen)
+ if pipe is None:
+ return None, None
+
+ for i_ in range(10):
+ if _check_server(display):
+ return pipe, display
+
+ time.sleep(0.1)
+
+ _kill_pipe(pipe)
+
+ return None, None
+
+
+def _start_window_manager():
+ cmd = ['metacity']
+
+ cmd.extend(['--no-force-fullscreen'])
+
+ gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH)
+
+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['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
+
+ if scaling:
+ os.environ['SUGAR_SCALING'] = scaling
+
+def main():
+ """Script-level operations"""
+
+ parser = OptionParser()
+ parser.add_option('-d', '--dpi', dest='dpi', type="int",
+ help='Emulator dpi')
+ parser.add_option('-s', '--scaling', dest='scaling',
+ help='Sugar scaling in %')
+ parser.add_option('-i', '--dimensions', dest='dimensions',
+ help='Emulator dimensions (ex. 1200x900)')
+ parser.add_option('-f', '--fullscreen', dest='fullscreen',
+ action='store_true', default=None,
+ help='Run emulator in fullscreen mode')
+ parser.add_option('-F', '--no-fullscreen', dest='fullscreen',
+ action='store_false',
+ help='Do not run emulator in fullscreen mode')
+ (options, args) = parser.parse_args()
+
+ if not os.environ.get('DISPLAY'):
+ sys.stderr.write('DISPLAY not set, cannot connect to host X server.\n')
+ return ERROR_NO_DISPLAY
+
+ 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
+
+ _setup_env(display, options.scaling, str(server.pid))
+
+ command = ['dbus-launch', '--exit-with-session']
+
+ if not args:
+ command.append('sugar')
+ else:
+ _start_window_manager()
+
+ if args[0].endswith('.py'):
+ command.append('python')
+
+ command.append(args[0])
+
+ subprocess.call(command)
+ _kill_pipe(server)
diff --git a/shell/src/jarabe/util/telepathy/Makefile.am b/shell/src/jarabe/util/telepathy/Makefile.am
new file mode 100644
index 0000000..d40349d
--- /dev/null
+++ b/shell/src/jarabe/util/telepathy/Makefile.am
@@ -0,0 +1,4 @@
+sugardir = $(pythondir)/jarabe/util/telepathy
+sugar_PYTHON = \
+ __init__.py \
+ connection_watcher.py
diff --git a/shell/src/jarabe/util/telepathy/__init__.py b/shell/src/jarabe/util/telepathy/__init__.py
new file mode 100644
index 0000000..387d09c
--- /dev/null
+++ b/shell/src/jarabe/util/telepathy/__init__.py
@@ -0,0 +1,19 @@
+"""OLPC Sugar Jarabe utility telepathy modules
+"""
+
+# 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
+
diff --git a/shell/src/jarabe/util/telepathy/connection_watcher.py b/shell/src/jarabe/util/telepathy/connection_watcher.py
new file mode 100644
index 0000000..391bdd5
--- /dev/null
+++ b/shell/src/jarabe/util/telepathy/connection_watcher.py
@@ -0,0 +1,118 @@
+# This should eventually land in telepathy-python, so has the same license:
+# Copyright (C) 2008 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 Lesser General Public License as published
+# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+# FIXME: this sould go upstream, in telepathy-python
+
+import logging
+
+import dbus
+import dbus.mainloop.glib
+import gobject
+
+from telepathy.client import Connection
+from telepathy.interfaces import CONN_INTERFACE
+from telepathy.constants import CONNECTION_STATUS_CONNECTED, \
+ CONNECTION_STATUS_DISCONNECTED
+
+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]))
+ }
+
+ def __init__(self, bus=None):
+ gobject.GObject.__init__(self)
+
+ if bus is None:
+ self.bus = dbus.Bus()
+ else:
+ self.bus = bus
+
+ # D-Bus path -> Connection
+ self._connections = {}
+
+ self.bus.add_signal_receiver(self._status_changed_cb,
+ dbus_interface=CONN_INTERFACE, signal_name='StatusChanged',
+ path_keyword='path')
+
+ for conn in Connection.get_connections(bus):
+ conn.call_when_ready(self._conn_ready_cb)
+
+ def _status_changed_cb(self, *args, **kwargs):
+ path = kwargs['path']
+ if not path.startswith('/org/freedesktop/Telepathy/Connection/'):
+ return
+
+ status, reason_ = args
+ service_name = path.replace('/', '.')[1:]
+
+ if status == CONNECTION_STATUS_CONNECTED:
+ self._add_connection(service_name, path)
+ elif status == CONNECTION_STATUS_DISCONNECTED:
+ self._remove_connection(service_name, path)
+
+ def _conn_ready_cb(self, conn):
+ if conn.object_path in self._connections:
+ return
+
+ self._connections[conn.object_path] = conn
+ self.emit('connection-added', conn)
+
+ def _add_connection(self, service_name, path):
+ if path in self._connections:
+ return
+
+ try:
+ Connection(service_name, path, ready_handler=self._conn_ready_cb)
+ except dbus.exceptions.DBusException:
+ logging.debug('%s is propably already gone.', service_name)
+
+ def _remove_connection(self, service_name, path):
+ conn = self._connections.pop(path, None)
+ if conn is None:
+ return
+
+ self.emit('connection-removed', conn)
+
+ def get_connections(self):
+ return self._connections.values()
+
+_instance = None
+
+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
+
+ def connection_removed_cb(conn_watcher, conn):
+ print "removed connection", conn.service_name
+
+ watcher = ConnectionWatcher()
+ watcher.connect('connection-added', connection_added_cb)
+ watcher.connect('connection-removed', connection_removed_cb)
+
+ loop = gobject.MainLoop()
+ loop.run()
diff --git a/shell/src/jarabe/view/Makefile.am b/shell/src/jarabe/view/Makefile.am
new file mode 100644
index 0000000..1abea6d
--- /dev/null
+++ b/shell/src/jarabe/view/Makefile.am
@@ -0,0 +1,12 @@
+sugardir = $(pythondir)/jarabe/view
+sugar_PYTHON = \
+ __init__.py \
+ buddyicon.py \
+ buddymenu.py \
+ keyhandler.py \
+ launcher.py \
+ palettes.py \
+ pulsingicon.py \
+ service.py \
+ tabbinghandler.py \
+ viewsource.py
diff --git a/shell/src/jarabe/view/__init__.py b/shell/src/jarabe/view/__init__.py
new file mode 100644
index 0000000..a9dd95a
--- /dev/null
+++ b/shell/src/jarabe/view/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2006-2007, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
diff --git a/shell/src/jarabe/view/buddyicon.py b/shell/src/jarabe/view/buddyicon.py
new file mode 100644
index 0000000..37b9167
--- /dev/null
+++ b/shell/src/jarabe/view/buddyicon.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2006-2007 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from sugar.graphics.icon import CanvasIcon
+from sugar.graphics import style
+
+from jarabe.view.buddymenu import BuddyMenu
+
+class BuddyIcon(CanvasIcon):
+ def __init__(self, buddy, size=style.STANDARD_ICON_SIZE):
+ CanvasIcon.__init__(self, icon_name='computer-xo', size=size)
+
+ self._greyed_out = False
+ self._buddy = buddy
+ self._buddy.connect('notify::present', self.__buddy_notify_present_cb)
+ self._buddy.connect('notify::color', self.__buddy_notify_color_cb)
+
+ self._update_color()
+
+ 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 __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()
+ if self._greyed_out:
+ self.props.stroke_color = '#D5D5D5'
+ self.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()
+ 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/shell/src/jarabe/view/buddymenu.py b/shell/src/jarabe/view/buddymenu.py
new file mode 100644
index 0000000..0ba6cc1
--- /dev/null
+++ b/shell/src/jarabe/view/buddymenu.py
@@ -0,0 +1,168 @@
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+
+import gtk
+import gconf
+import dbus
+
+from sugar.graphics.palette import Palette
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+
+from jarabe.model import shell
+from jarabe.model import friends
+from jarabe.model.session import get_session_manager
+from jarabe.controlpanel.gui import ControlPanel
+
+class BuddyMenu(Palette):
+ def __init__(self, buddy):
+ self._buddy = buddy
+
+ buddy_icon = Icon(icon_name='computer-xo',
+ xo_color=buddy.get_color(),
+ icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR)
+ Palette.__init__(self, None, primary_text=buddy.get_nick(),
+ icon=buddy_icon)
+ self._invite_menu = None
+ self._active_activity_changed_hid = None
+ self.connect('destroy', self.__destroy_cb)
+
+ self._buddy.connect('notify::nick', self.__buddy_notify_nick_cb)
+
+ if buddy.is_owner():
+ self._add_my_items()
+ else:
+ self._add_buddy_items()
+
+ def __destroy_cb(self, menu):
+ 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_notify_nick_cb)
+
+ def _add_buddy_items(self):
+ if friends.get_model().has_buddy(self._buddy):
+ menu_item = MenuItem(_('Remove friend'), 'list-remove')
+ menu_item.connect('activate', self._remove_friend_cb)
+ else:
+ menu_item = MenuItem(_('Make friend'), 'list-add')
+ menu_item.connect('activate', self._make_friend_cb)
+
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ self._invite_menu = MenuItem('')
+ self._invite_menu.connect('activate', self._invite_friend_cb)
+ self.menu.append(self._invite_menu)
+
+ home_model = shell.get_model()
+ self._active_activity_changed_hid = home_model.connect(
+ 'active-activity-changed', self._cur_activity_changed_cb)
+ activity = home_model.get_active_activity()
+ self._update_invite_menu(activity)
+
+ def _add_my_items(self):
+ item = MenuItem(_('Shutdown'), 'system-shutdown')
+ item.connect('activate', self.__shutdown_activate_cb)
+ self.menu.append(item)
+ item.show()
+
+ client = gconf.client_get_default()
+
+ if client.get_bool('/desktop/sugar/show_logout'):
+ item = MenuItem(_('Logout'), 'system-logout')
+ item.connect('activate', self.__logout_activate_cb)
+ self.menu.append(item)
+ item.show()
+
+ item = MenuItem(_('My Settings'), 'preferences-system')
+ item.connect('activate', self.__controlpanel_activate_cb)
+ self.menu.append(item)
+ item.show()
+
+ def __logout_activate_cb(self, menu_item):
+ session_manager = get_session_manager()
+ session_manager.logout()
+
+ def __reboot_activate_cb(self, menu_item):
+ session_manager = get_session_manager()
+ session_manager.reboot()
+
+ def __shutdown_activate_cb(self, menu_item):
+ session_manager = get_session_manager()
+ session_manager.shutdown()
+
+ def __controlpanel_activate_cb(self, menu_item):
+ panel = ControlPanel()
+ panel.set_transient_for(self.get_toplevel())
+ panel.show()
+
+ def _update_invite_menu(self, activity):
+ buddy_activity = self._buddy.props.current_activity
+ if buddy_activity is not None:
+ buddy_activity_id = buddy_activity.activity_id
+ else:
+ buddy_activity_id = None
+
+ if activity is None or activity.is_journal() or \
+ activity.get_activity_id() == buddy_activity_id:
+ self._invite_menu.hide()
+ else:
+ title = activity.get_title()
+ label = self._invite_menu.get_children()[0]
+ label.set_text(_('Invite to %s') % title)
+
+ icon = Icon(file=activity.get_icon_path())
+ icon.props.xo_color = activity.get_icon_color()
+ self._invite_menu.set_image(icon)
+ icon.show()
+
+ self._invite_menu.show()
+
+ def _cur_activity_changed_cb(self, home_model, activity_model):
+ self._update_invite_menu(activity_model)
+
+ def __buddy_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)
+
+ def _remove_friend_cb(self, menuitem):
+ friends.get_model().remove(self._buddy)
+
+ def _invite_friend_cb(self, menuitem):
+ activity = shell.get_model().get_active_activity()
+ service = activity.get_service()
+ if service:
+ 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/shell/src/jarabe/view/keyhandler.py b/shell/src/jarabe/view/keyhandler.py
new file mode 100644
index 0000000..8a85ac7
--- /dev/null
+++ b/shell/src/jarabe/view/keyhandler.py
@@ -0,0 +1,242 @@
+# Copyright (C) 2006-2007, Red Hat, Inc.
+# Copyright (C) 2009 Simon Schampijer
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+import traceback
+
+import dbus
+import gtk
+
+from sugar._sugarext import KeyGrabber
+
+from jarabe.model import sound
+from jarabe.model import shell
+from jarabe.model import session
+from jarabe.view.tabbinghandler import TabbingHandler
+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',
+ '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',
+}
+
+SPEECH_DBUS_SERVICE = 'org.laptop.Speech'
+SPEECH_DBUS_PATH = '/org/laptop/Speech'
+SPEECH_DBUS_INTERFACE = 'org.laptop.Speech'
+
+class KeyHandler(object):
+ def __init__(self, frame):
+ self._frame = frame
+ self._key_pressed = None
+ self._keycode_pressed = 0
+ self._keystate_pressed = 0
+ self._speech_proxy = None
+
+ self._key_grabber = KeyGrabber()
+ self._key_grabber.connect('key-pressed',
+ self._key_pressed_cb)
+ self._key_grabber.connect('key-released',
+ self._key_released_cb)
+
+ self._tabbing_handler = TabbingHandler(self._frame, _TABBING_MODIFIER)
+
+ for f in os.listdir(os.path.join(config.ext_path, 'globalkey')):
+ if f.endswith('.py') and not f.startswith('__'):
+ module_name = f[:-3]
+ try:
+ logging.debug('Loading module %r', module_name)
+ module = __import__('globalkey.' + module_name, globals(),
+ locals(), [module_name])
+ for key in module.BOUND_KEYS:
+ if key in _actions_table:
+ raise ValueError('Key %r is already bound' % key)
+ _actions_table[key] = module
+ except Exception:
+ logging.error('Exception while loading extension:\n' + \
+ traceback.format_exc())
+
+ self._key_grabber.grab_keys(_actions_table.keys())
+
+ def _change_volume(self, step=None, value=None):
+ if step is not None:
+ volume = sound.get_volume() + step
+ elif value is not None:
+ volume = value
+
+ volume = min(max(0, volume), _VOLUME_MAX)
+
+ sound.set_volume(volume)
+ sound.set_muted(volume == 0)
+
+ def _get_speech_proxy(self):
+ if self._speech_proxy is None:
+ bus = dbus.SessionBus()
+ speech_obj = bus.get_object(SPEECH_DBUS_SERVICE, SPEECH_DBUS_PATH,
+ follow_name_owner_changes=True)
+ self._speech_proxy = dbus.Interface(speech_obj,
+ SPEECH_DBUS_INTERFACE)
+ return self._speech_proxy
+
+ def _on_speech_err(self, ex):
+ logging.error('An error occurred with the ESpeak service: %r', ex)
+
+ def _primary_selection_cb(self, clipboard, text, user_data):
+ logging.debug('KeyHandler._primary_selection_cb: %r', text)
+ if text:
+ self._get_speech_proxy().SayText(text, reply_handler=lambda: None, \
+ error_handler=self._on_speech_err)
+
+ def handle_say_text(self, event_time):
+ clipboard = gtk.clipboard_get(selection="PRIMARY")
+ clipboard.request_text(self._primary_selection_cb)
+
+ def handle_previous_window(self, event_time):
+ self._tabbing_handler.previous_activity(event_time)
+
+ def handle_next_window(self, event_time):
+ self._tabbing_handler.next_activity(event_time)
+
+ def handle_close_window(self, event_time):
+ active_activity = shell.get_model().get_active_activity()
+ if active_activity.is_journal():
+ return
+
+ active_activity.get_window().close()
+
+ def handle_zoom_mesh(self, event_time):
+ shell.get_model().set_zoom_level(ShellModel.ZOOM_MESH, event_time)
+
+ def handle_zoom_group(self, event_time):
+ shell.get_model().set_zoom_level(ShellModel.ZOOM_GROUP, event_time)
+
+ def handle_zoom_home(self, event_time):
+ shell.get_model().set_zoom_level(ShellModel.ZOOM_HOME, event_time)
+
+ def handle_zoom_activity(self, event_time):
+ shell.get_model().set_zoom_level(ShellModel.ZOOM_ACTIVITY, event_time)
+
+ def handle_volume_max(self, event_time):
+ self._change_volume(value=_VOLUME_MAX)
+
+ def handle_volume_min(self, event_time):
+ self._change_volume(value=0)
+
+ def handle_volume_mute(self, event_time):
+ if sound.get_muted() is True:
+ sound.set_muted(False)
+ else:
+ sound.set_muted(True)
+
+ def handle_volume_up(self, event_time):
+ self._change_volume(step=_VOLUME_STEP)
+
+ def handle_volume_down(self, event_time):
+ self._change_volume(step=-_VOLUME_STEP)
+
+ def handle_frame(self, event_time):
+ self._frame.notify_key_press()
+
+ def handle_quit_emulator(self, event_time):
+ session.get_session_manager().shutdown()
+
+ def handle_open_search(self, event_time):
+ journalactivity.get_journal().focus_search()
+
+ def _key_pressed_cb(self, grabber, keycode, state, event_time):
+ key = grabber.get_key(keycode, state)
+ logging.debug('_key_pressed_cb: %i %i %s', keycode, state, key)
+ if key is not None:
+ self._key_pressed = key
+ self._keycode_pressed = keycode
+ self._keystate_pressed = state
+
+ action = _actions_table[key]
+ if self._tabbing_handler.is_tabbing():
+ # Only accept window tabbing events, everything else
+ # cancels the tabbing operation.
+ if not action in ["next_window", "previous_window"]:
+ self._tabbing_handler.stop(event_time)
+ return True
+
+ if hasattr(action, 'handle_key_press'):
+ action.handle_key_press(key)
+ elif isinstance(action, basestring):
+ method = getattr(self, 'handle_' + action)
+ method(event_time)
+ else:
+ raise TypeError('Invalid action %r' % action)
+
+ return True
+ else:
+ # If this is not a registered key, then cancel tabbing.
+ if self._tabbing_handler.is_tabbing():
+ if not grabber.is_modifier(keycode):
+ self._tabbing_handler.stop(event_time)
+ return True
+
+ return False
+
+ def _key_released_cb(self, grabber, keycode, state, event_time):
+ 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.
+ if grabber.is_modifier(keycode, mask=_TABBING_MODIFIER):
+ self._tabbing_handler.stop(event_time)
+
+ return True
+ return False
+
+_instance = None
+
+def setup(frame):
+ global _instance
+
+ if _instance:
+ del _instance
+
+ _instance = KeyHandler(frame)
+
diff --git a/shell/src/jarabe/view/launcher.py b/shell/src/jarabe/view/launcher.py
new file mode 100644
index 0000000..89251e5
--- /dev/null
+++ b/shell/src/jarabe/view/launcher.py
@@ -0,0 +1,217 @@
+# Copyright (C) 2008, Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+from gettext import gettext as _
+
+import gtk
+import hippo
+import gobject
+
+from sugar import wm
+from sugar.graphics import style
+from sugar.graphics import animator
+from sugar.graphics.xocolor import XoColor
+
+from jarabe.model import shell
+from jarabe.view.pulsingicon import CanvasPulsingIcon
+
+
+class LaunchWindow(gtk.Window):
+
+ def __init__(self, activity_id, icon_path, icon_color):
+ gobject.GObject.__init__(self)
+
+ self.props.type_hint = gtk.gdk.WINDOW_TYPE_HINT_NORMAL
+ self.props.decorated = False
+ self.modify_bg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color())
+
+ canvas = gtk.VBox()
+ canvas.show()
+ self.add(canvas)
+
+ bar_size = gtk.gdk.screen_height() / 5 * 2
+
+ header = gtk.VBox()
+ header.set_size_request(-1, bar_size)
+ header.show()
+ canvas.pack_start(header, expand=False)
+
+ self._activity_id = activity_id
+ self._box = LaunchBox(activity_id, icon_path, icon_color)
+ box = hippo.Canvas()
+ box.modify_bg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color())
+ box.set_root(self._box)
+ box.show()
+ canvas.pack_start(box)
+
+ footer = gtk.VBox(spacing=style.DEFAULT_SPACING)
+ footer.set_size_request(-1, bar_size)
+ footer.show()
+ canvas.pack_end(footer, expand=False)
+
+ self.error_text = gtk.Label()
+ self.error_text.props.use_markup = True
+ footer.pack_start(self.error_text, expand=False)
+
+ button_box = gtk.Alignment(xalign=0.5)
+ button_box.show()
+ footer.pack_start(button_box, expand=False)
+ self.cancel_button = gtk.Button(stock=gtk.STOCK_STOP)
+ button_box.add(self.cancel_button)
+
+ self.connect('realize', self.__realize_cb)
+
+ screen = gtk.gdk.screen_get_default()
+ screen.connect('size-changed', self.__size_changed_cb)
+
+ self._update_size()
+
+ def show(self):
+ self.present()
+ self._box.zoom_in()
+
+ def _update_size(self):
+ self.resize(gtk.gdk.screen_width(), gtk.gdk.screen_height())
+
+ def __realize_cb(self, widget):
+ wm.set_activity_id(widget.window, str(self._activity_id))
+ widget.window.property_change('_SUGAR_WINDOW_TYPE', 'STRING', 8,
+ gtk.gdk.PROP_MODE_REPLACE, 'launcher')
+
+ def __size_changed_cb(self, screen):
+ self._update_size()
+
+
+class LaunchBox(hippo.CanvasBox):
+
+ def __init__(self, activity_id, icon_path, icon_color):
+ gobject.GObject.__init__(self, orientation=hippo.ORIENTATION_VERTICAL)
+
+ self._activity_id = activity_id
+ self._activity_icon = CanvasPulsingIcon(
+ file_name=icon_path,
+ pulse_color=icon_color,
+ background_color=style.COLOR_WHITE.get_gdk_color())
+ self.append(self._activity_icon, hippo.PACK_EXPAND)
+
+ # FIXME support non-xo colors in CanvasPulsingIcon
+ self._activity_icon.props.base_color = \
+ XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+
+ self._animator = animator.Animator(1.0)
+
+ self._home = shell.get_model()
+ self._home.connect('active-activity-changed',
+ self.__active_activity_changed_cb)
+
+ self.connect('destroy', self.__destroy_cb)
+
+ def __destroy_cb(self, box):
+ self._activity_icon.props.pulsing = False
+ self._home.disconnect_by_func(self.__active_activity_changed_cb)
+
+ def zoom_in(self):
+ self._activity_icon.props.size = style.STANDARD_ICON_SIZE
+
+ self._animator.remove_all()
+ self._animator.add(_Animation(self._activity_icon,
+ style.STANDARD_ICON_SIZE,
+ style.XLARGE_ICON_SIZE))
+ self._animator.start()
+ self._activity_icon.props.pulsing = True
+
+ def __active_activity_changed_cb(self, model, activity):
+ if activity.get_activity_id() == self._activity_id:
+ self._activity_icon.props.paused = False
+ else:
+ self._activity_icon.props.paused = True
+
+
+class _Animation(animator.Animation):
+
+ def __init__(self, icon, start_size, end_size):
+ animator.Animation.__init__(self, 0.0, 1.0)
+
+ self._icon = icon
+ self.start_size = start_size
+ self.end_size = end_size
+
+ def next_frame(self, current):
+ d = (self.end_size - self.start_size) * current
+ self._icon.props.size = int(self.start_size + d)
+
+
+def setup():
+ model = shell.get_model()
+ model.connect('launch-started', __launch_started_cb)
+ model.connect('launch-failed', __launch_failed_cb)
+ model.connect('launch-completed', __launch_completed_cb)
+
+
+def add_launcher(activity_id, icon_path, icon_color):
+ model = shell.get_model()
+
+ if model.get_launcher(activity_id) is not None:
+ return
+
+ launch_window = LaunchWindow(activity_id, icon_path, icon_color)
+ launch_window.show()
+
+ model.register_launcher(activity_id, launch_window)
+
+
+def __launch_started_cb(home_model, home_activity):
+ add_launcher(home_activity.get_activity_id(),
+ home_activity.get_icon_path(), home_activity.get_icon_color())
+
+
+def __launch_failed_cb(home_model, home_activity):
+ activity_id = home_activity.get_activity_id()
+ launcher = shell.get_model().get_launcher(activity_id)
+
+ if launcher is None:
+ logging.error('Launcher for %s is missing', activity_id)
+ else:
+ launcher.error_text.props.label = _('<b>%s</b> failed to start.') % \
+ home_activity.get_activity_name()
+ launcher.error_text.show()
+
+ launcher.cancel_button.connect('clicked',
+ __cancel_button_clicked_cb, home_activity)
+ launcher.cancel_button.show()
+
+
+def __cancel_button_clicked_cb(button, home_activity):
+ _destroy_launcher(home_activity)
+
+
+def __launch_completed_cb(home_model, home_activity):
+ _destroy_launcher(home_activity)
+
+
+def _destroy_launcher(home_activity):
+ activity_id = home_activity.get_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/shell/src/jarabe/view/palettes.py b/shell/src/jarabe/view/palettes.py
new file mode 100644
index 0000000..43612d4
--- /dev/null
+++ b/shell/src/jarabe/view/palettes.py
@@ -0,0 +1,250 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import statvfs
+from gettext import gettext as _
+import logging
+
+import gconf
+import gtk
+
+from sugar import env
+from sugar.graphics.palette import Palette
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+from sugar.graphics.xocolor import XoColor
+from sugar.activity import activityfactory
+from sugar.activity.activityhandle import ActivityHandle
+
+from jarabe.model import shell
+from jarabe.view.viewsource import setup_view_source
+from jarabe.journal import misc
+
+class BasePalette(Palette):
+ def __init__(self, home_activity):
+ Palette.__init__(self)
+
+ 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 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
+ BasePalette.__init__(self, home_activity)
+
+ def setup_palette(self):
+ self.props.primary_text = self._home_activity.get_activity_name()
+
+ if self._home_activity.get_title() != self.props.primary_text:
+ self.props.secondary_text = self._home_activity.get_title()
+
+ menu_item = MenuItem(_('Resume'), 'activity-start')
+ menu_item.connect('activate', self.__resume_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ # TODO: share-with, keep
+
+ menu_item = MenuItem(_('View Source'), 'view-source')
+ # TODO Make this accelerator translatable
+ menu_item.props.accelerator = '<Alt><Shift>v'
+ menu_item.connect('activate', self.__view_source__cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ separator = gtk.SeparatorMenuItem()
+ self.menu.append(separator)
+ separator.show()
+
+ menu_item = MenuItem(_('Stop'), 'activity-stop')
+ menu_item.connect('activate', self.__stop_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __resume_activate_cb(self, menu_item):
+ self._home_activity.get_window().activate(gtk.get_current_event_time())
+
+ def __view_source__cb(self, menu_item):
+ setup_view_source(self._home_activity)
+ shell_model = shell.get_model()
+ if self._home_activity is not shell_model.get_active_activity():
+ 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)
+
+
+class ActivityPalette(Palette):
+ __gtype_name__ = 'SugarActivityPalette'
+
+ def __init__(self, activity_info):
+ self._activity_info = activity_info
+
+ client = gconf.client_get_default()
+ 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)
+
+ Palette.__init__(self, primary_text=activity_info.get_name(),
+ icon=activity_icon)
+
+ xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(),
+ style.COLOR_TRANSPARENT.get_svg()))
+ menu_item = MenuItem(text_label=_('Start new'),
+ file_name=activity_info.get_icon(),
+ xo_color=xo_color)
+ menu_item.connect('activate', self.__start_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ # TODO: start-with
+
+ def __start_activate_cb(self, menu_item):
+ self.popdown(immediate=True)
+ misc.launch(self._activity_info)
+
+class JournalPalette(BasePalette):
+ def __init__(self, home_activity):
+ self._home_activity = home_activity
+ self._progress_bar = None
+ self._free_space_label = None
+
+ BasePalette.__init__(self, home_activity)
+
+ def setup_palette(self):
+ self.set_primary_text(self._home_activity.get_title())
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ self._progress_bar = gtk.ProgressBar()
+ vbox.add(self._progress_bar)
+ self._progress_bar.show()
+
+ self._free_space_label = gtk.Label()
+ self._free_space_label.set_alignment(0.5, 0.5)
+ vbox.add(self._free_space_label)
+ self._free_space_label.show()
+
+ self.connect('popup', self.__popup_cb)
+
+ menu_item = MenuItem(_('Show contents'))
+
+ icon = Icon(file=self._home_activity.get_icon_path(),
+ icon_size=gtk.ICON_SIZE_MENU,
+ xo_color=self._home_activity.get_icon_color())
+ menu_item.set_image(icon)
+ icon.show()
+
+ menu_item.connect('activate', self.__open_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __open_activate_cb(self, menu_item):
+ self._home_activity.get_window().activate(gtk.get_current_event_time())
+
+ def __popup_cb(self, palette):
+ 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)}
+
+class VolumePalette(Palette):
+ def __init__(self, mount):
+ Palette.__init__(self, label=mount.get_name())
+ self._mount = mount
+
+ self.props.secondary_text = mount.get_root().get_path()
+
+ vbox = gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ self._progress_bar = gtk.ProgressBar()
+ vbox.add(self._progress_bar)
+ self._progress_bar.show()
+
+ self._free_space_label = gtk.Label()
+ self._free_space_label.set_alignment(0.5, 0.5)
+ vbox.add(self._free_space_label)
+ self._free_space_label.show()
+
+ self.connect('popup', self.__popup_cb)
+
+ menu_item = MenuItem(_('Remove'))
+
+ icon = Icon(icon_name='media-eject', icon_size=gtk.ICON_SIZE_MENU)
+ menu_item.set_image(icon)
+ icon.show()
+
+ menu_item.connect('activate', self.__unmount_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def __unmount_activate_cb(self, menu_item):
+ self._mount.unmount(self.__unmount_cb)
+
+ def __unmount_cb(self, mount, result):
+ logging.debug('__unmount_cb %r %r', mount, result)
+ mount.unmount_finish(result)
+
+ def __popup_cb(self, palette):
+ mount_point = self._mount.get_root().get_path()
+ stat = os.statvfs(mount_point)
+ 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/shell/src/jarabe/view/pulsingicon.py b/shell/src/jarabe/view/pulsingicon.py
new file mode 100644
index 0000000..43ec358
--- /dev/null
+++ b/shell/src/jarabe/view/pulsingicon.py
@@ -0,0 +1,229 @@
+# Copyright (C) 2008 One Laptop Per Child
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import math
+
+import gtk
+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
+ self._icon = icon
+ self._level = 0
+ self._phase = 0
+
+ def start(self, restart=False):
+ if restart:
+ self._phase = 0
+ if self._pulse_hid is None:
+ self._pulse_hid = gobject.timeout_add(_INTERVAL, self.__pulse_cb)
+
+ def stop(self):
+ if self._pulse_hid is not None:
+ gobject.source_remove(self._pulse_hid)
+ self._pulse_hid = None
+ self._icon.xo_color = self._icon.get_base_color()
+
+ def update(self):
+ if self._icon.get_pulsing():
+ base_color = self._icon.get_base_color()
+ pulse_color = self._icon.get_pulse_color()
+
+ base_stroke = self._get_as_rgba(base_color.get_stroke_color())
+ pulse_stroke = self._get_as_rgba(pulse_color.get_stroke_color())
+ base_fill = self._get_as_rgba(base_color.get_fill_color())
+ pulse_fill = self._get_as_rgba(pulse_color.get_fill_color())
+
+ self._icon.set_stroke_color(
+ self._get_color_string(base_stroke, pulse_stroke))
+ self._icon.set_fill_color(
+ self._get_color_string(base_fill, pulse_fill))
+ else:
+ self._icon.xo_color = self._icon.base_color
+
+ def _get_as_rgba(self, html_color):
+ if html_color == 'none':
+ return 1.0, 1.0, 1.0
+ else:
+ color = gtk.gdk.color_parse(html_color)
+ return color.red / 65535.0, \
+ color.green / 65535.0, \
+ color.blue / 65535.0
+
+ def _get_color_string(self, orig_color, target_color):
+ r = orig_color[0] + self._level * (target_color[0] - orig_color[0])
+ g = orig_color[1] + self._level * (target_color[1] - orig_color[1])
+ b = orig_color[2] + self._level * (target_color[2] - orig_color[2])
+
+ return '#%02x%02x%02x' % (int(r * 255), int(g * 255), int(b * 255))
+
+ def __pulse_cb(self):
+ self._phase += _STEP
+ self._level = (math.sin(self._phase) + 1) / 2
+ self.update()
+
+ return True
+
+class PulsingIcon(Icon):
+ __gtype_name__ = 'SugarPulsingIcon'
+
+ def __init__(self, **kwargs):
+ self._pulser = Pulser(self)
+ self._base_color = None
+ self._pulse_color = None
+ self._paused = False
+ self._pulsing = False
+
+ Icon.__init__(self, **kwargs)
+
+ self._palette = None
+ self.connect('destroy', self.__destroy_cb)
+
+ def set_pulse_color(self, pulse_color):
+ self._pulse_color = pulse_color
+ self._pulser.update()
+
+ def get_pulse_color(self):
+ return self._pulse_color
+
+ pulse_color = gobject.property(
+ type=object, getter=get_pulse_color, setter=set_pulse_color)
+
+ def set_base_color(self, base_color):
+ self._base_color = base_color
+ self._pulser.update()
+
+ def get_base_color(self):
+ return self._base_color
+
+ base_color = gobject.property(
+ type=object, getter=get_base_color, setter=set_base_color)
+
+ def set_paused(self, paused):
+ self._paused = paused
+
+ if self._paused:
+ self._pulser.stop()
+ else:
+ self._pulser.start(restart=False)
+
+ def get_paused(self):
+ return self._paused
+
+ paused = gobject.property(
+ type=bool, default=False, getter=get_paused, setter=set_paused)
+
+ def set_pulsing(self, pulsing):
+ self._pulsing = pulsing
+
+ if self._pulsing:
+ self._pulser.start(restart=True)
+ else:
+ self._pulser.stop()
+
+ def get_pulsing(self):
+ return self._pulsing
+
+ pulsing = gobject.property(
+ type=bool, default=False, getter=get_pulsing, setter=set_pulsing)
+
+ def _get_palette(self):
+ return self._palette
+
+ def _set_palette(self, palette):
+ if self._palette is not None:
+ self._palette.props.invoker = None
+ self._palette = palette
+
+ palette = property(_get_palette, _set_palette)
+
+ def __destroy_cb(self, icon):
+ self._pulser.stop()
+ if self._palette is not None:
+ self._palette.destroy()
+
+class CanvasPulsingIcon(CanvasIcon):
+ __gtype_name__ = 'SugarCanvasPulsingIcon'
+
+ def __init__(self, **kwargs):
+ self._pulser = Pulser(self)
+ self._base_color = None
+ self._pulse_color = None
+ self._paused = False
+ self._pulsing = False
+
+ CanvasIcon.__init__(self, **kwargs)
+
+ self.connect('destroy', self.__destroy_cb)
+
+ def __destroy_cb(self, box):
+ self._pulser.stop()
+
+ def set_pulse_color(self, pulse_color):
+ self._pulse_color = pulse_color
+ self._pulser.update()
+
+ def get_pulse_color(self):
+ return self._pulse_color
+
+ pulse_color = gobject.property(
+ type=object, getter=get_pulse_color, setter=set_pulse_color)
+
+ def set_base_color(self, base_color):
+ self._base_color = base_color
+ self._pulser.update()
+
+ def get_base_color(self):
+ return self._base_color
+
+ base_color = gobject.property(
+ type=object, getter=get_base_color, setter=set_base_color)
+
+ def set_paused(self, paused):
+ self._paused = paused
+
+ if self._paused:
+ self._pulser.stop()
+ elif self._pulsing:
+ self._pulser.start(restart=False)
+
+ def get_paused(self):
+ return self._paused
+
+ paused = gobject.property(
+ type=bool, default=False, getter=get_paused, setter=set_paused)
+
+ def set_pulsing(self, pulsing):
+ self._pulsing = pulsing
+ if self._paused:
+ return
+
+ if self._pulsing:
+ self._pulser.start(restart=True)
+ else:
+ self._pulser.stop()
+
+ def get_pulsing(self):
+ return self._pulsing
+
+ pulsing = gobject.property(
+ type=bool, default=False, getter=get_pulsing, setter=set_pulsing)
diff --git a/shell/src/jarabe/view/service.py b/shell/src/jarabe/view/service.py
new file mode 100644
index 0000000..7af778a
--- /dev/null
+++ b/shell/src/jarabe/view/service.py
@@ -0,0 +1,89 @@
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""D-bus service providing access to the shell's functionality"""
+
+import dbus
+import gtk
+
+from jarabe.model import shell
+from jarabe.model import bundleregistry
+
+_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
+
+ Uses a shell_model object to observe events such as changes to:
+
+ * nickname
+ * colour
+ * icon
+ * currently active activity
+
+ and pass the event off to the methods in the dbus signature.
+
+ Key method here at the moment is add_bundle, which is used to
+ do a run-time registration of a bundle using it's application path.
+
+ XXX At the moment the d-bus service methods do not appear to do
+ anything other than add_bundle
+ """
+
+ def __init__(self):
+ bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, _DBUS_PATH)
+
+ self._shell_model = shell.get_model()
+
+ @dbus.service.method(_DBUS_SHELL_IFACE,
+ in_signature="s", out_signature="s")
+ def GetBundlePath(self, bundle_id):
+ bundle = bundleregistry.get_registry().get_bundle(bundle_id)
+ if bundle:
+ return bundle.get_path()
+ else:
+ return ''
+
+ @dbus.service.method(_DBUS_SHELL_IFACE,
+ 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.
+ """
+ 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 self._shell_model.get_launcher(activity_id) is None
+
+ return False
+
+ @dbus.service.method(_DBUS_SHELL_IFACE,
+ 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="")
+ def NotifyLaunchFailure(self, activity_id):
+ shell.get_model().notify_launch_failed(activity_id)
+
diff --git a/shell/src/jarabe/view/tabbinghandler.py b/shell/src/jarabe/view/tabbinghandler.py
new file mode 100644
index 0000000..f52bda3
--- /dev/null
+++ b/shell/src/jarabe/view/tabbinghandler.py
@@ -0,0 +1,148 @@
+# Copyright (C) 2008, Benjamin Berg <benjamin@sipsolutions.net>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import gobject
+import gtk
+
+from jarabe.model import shell
+
+_RAISE_DELAY = 250
+
+class TabbingHandler(object):
+ def __init__(self, frame, modifier):
+ self._frame = frame
+ self._tabbing = False
+ self._modifier = modifier
+ self._timeout = None
+
+ def _start_tabbing(self):
+ if not self._tabbing:
+ logging.debug('Grabing the input.')
+
+ screen = gtk.gdk.screen_get_default()
+ window = screen.get_root_window()
+ keyboard_grab_result = gtk.gdk.keyboard_grab(window)
+ pointer_grab_result = gtk.gdk.pointer_grab(window)
+
+ self._tabbing = (keyboard_grab_result == gtk.gdk.GRAB_SUCCESS and
+ pointer_grab_result == gtk.gdk.GRAB_SUCCESS)
+
+ # Now test that the modifier is still active to prevent race
+ # conditions. We also test if one of the grabs failed.
+ mask = window.get_pointer()[2]
+ if not self._tabbing or not (mask & self._modifier):
+ logging.debug('Releasing grabs again.')
+
+ # ungrab keyboard/pointer if the grab was successfull.
+ if keyboard_grab_result == gtk.gdk.GRAB_SUCCESS:
+ gtk.gdk.keyboard_ungrab()
+ if pointer_grab_result == gtk.gdk.GRAB_SUCCESS:
+ gtk.gdk.pointer_ungrab()
+
+ self._tabbing = False
+ else:
+ self._frame.show(self._frame.MODE_NON_INTERACTIVE)
+
+ def __timeout_cb(self, event_time):
+ self._activate_current(event_time)
+ self._timeout = None
+ return False
+
+ def _start_timeout(self, event_time):
+ self._cancel_timeout()
+ self._timeout = gobject.timeout_add(_RAISE_DELAY,
+ lambda: self.__timeout_cb(event_time))
+
+ def _cancel_timeout(self):
+ if self._timeout:
+ gobject.source_remove(self._timeout)
+ self._timeout = None
+
+ def _activate_current(self, event_time):
+ home_model = shell.get_model()
+ activity = home_model.get_tabbing_activity()
+ if activity and activity.get_window():
+ activity.get_window().activate(event_time)
+
+ def next_activity(self, event_time):
+ if not self._tabbing:
+ first_switch = True
+ self._start_tabbing()
+ else:
+ first_switch = False
+
+ if self._tabbing:
+ shell_model = shell.get_model()
+ zoom_level = shell_model.zoom_level
+ zoom_activity = (zoom_level == shell.ShellModel.ZOOM_ACTIVITY)
+
+ if not zoom_activity and first_switch:
+ activity = shell_model.get_active_activity()
+ else:
+ activity = shell_model.get_tabbing_activity()
+ activity = shell_model.get_next_activity(current=activity)
+
+ shell_model.set_tabbing_activity(activity)
+ self._start_timeout(event_time)
+ else:
+ self._activate_next_activity(event_time)
+
+ def previous_activity(self, event_time):
+ if not self._tabbing:
+ first_switch = True
+ self._start_tabbing()
+ else:
+ first_switch = False
+
+ if self._tabbing:
+ shell_model = shell.get_model()
+ zoom_level = shell_model.zoom_level
+ zoom_activity = (zoom_level == shell.ShellModel.ZOOM_ACTIVITY)
+
+ if not zoom_activity and first_switch:
+ activity = shell_model.get_active_activity()
+ else:
+ activity = shell_model.get_tabbing_activity()
+ activity = shell_model.get_previous_activity(current=activity)
+
+ shell_model.set_tabbing_activity(activity)
+ self._start_timeout(event_time)
+ else:
+ self._activate_next_activity(event_time)
+
+ def _activate_next_activity(self, event_time):
+ next_activity = shell.get_model().get_next_activity()
+ if next_activity:
+ next_activity.get_window().activate(event_time)
+
+ def stop(self, event_time):
+ gtk.gdk.keyboard_ungrab()
+ gtk.gdk.pointer_ungrab()
+ self._tabbing = False
+
+ self._frame.hide()
+
+ self._cancel_timeout()
+ self._activate_current(event_time)
+
+ home_model = shell.get_model()
+ home_model.set_tabbing_activity(None)
+
+ def is_tabbing(self):
+ return self._tabbing
+
diff --git a/shell/src/jarabe/view/viewsource.py b/shell/src/jarabe/view/viewsource.py
new file mode 100644
index 0000000..290df18
--- /dev/null
+++ b/shell/src/jarabe/view/viewsource.py
@@ -0,0 +1,464 @@
+# Copyright (C) 2008 One Laptop Per Child
+# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+import traceback
+from gettext import gettext as _
+
+import gobject
+import pango
+import gtk
+import gtksourceview2
+import dbus
+import gconf
+
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.radiotoolbutton import RadioToolButton
+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:
+ try:
+ service.HandleViewSource()
+ return
+ except dbus.DBusException, e:
+ 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())
+ except Exception:
+ logging.error(traceback.format_exc())
+
+ window_xid = activity.get_xid()
+ if window_xid is None:
+ _logger.error('Activity without a window xid')
+ return
+
+ bundle_path = activity.get_bundle_path()
+
+ if window_xid in map_activity_to_window:
+ _logger.debug('Viewsource window already open for %s %s', window_xid,
+ bundle_path)
+ return
+
+ document_path = None
+ if service is not None:
+ try:
+ document_path = service.GetDocumentPath()
+ except dbus.DBusException, e:
+ 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())
+ except Exception:
+ logging.error(traceback.format_exc())
+
+ if bundle_path is None and document_path is None:
+ _logger.debug('Activity without bundle_path nor document_path')
+ return
+
+ view_source = ViewSource(window_xid, bundle_path, document_path,
+ activity.get_title())
+ map_activity_to_window[window_xid] = view_source
+ view_source.show()
+
+class ViewSource(gtk.Window):
+ __gtype_name__ = 'SugarViewSource'
+
+ def __init__(self, window_xid, bundle_path, document_path, title):
+ gtk.Window.__init__(self)
+
+ logging.debug('ViewSource paths: %r %r', bundle_path, document_path)
+
+ self.set_decorated(False)
+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+ self.set_border_width(style.LINE_WIDTH)
+
+ width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2
+ height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2
+ self.set_size_request(width, height)
+
+ self._parent_window_xid = window_xid
+
+ self.connect('realize', self.__realize_cb)
+ self.connect('destroy', self.__destroy_cb, document_path)
+ self.connect('key-press-event', self.__key_press_event_cb)
+
+ vbox = gtk.VBox()
+ self.add(vbox)
+ vbox.show()
+
+ toolbar = Toolbar(title, bundle_path, document_path)
+ vbox.pack_start(toolbar, expand=False)
+ toolbar.connect('stop-clicked', self.__stop_clicked_cb)
+ toolbar.connect('source-selected', self.__source_selected_cb)
+ toolbar.show()
+
+ pane = gtk.HPaned()
+ vbox.pack_start(pane)
+ pane.show()
+
+ self._selected_file = None
+ file_name = ''
+
+ activity_bundle = ActivityBundle(bundle_path)
+ command = activity_bundle.get_command()
+ if len(command.split(' ')) > 1:
+ name = command.split(' ')[1].split('.')[0]
+ file_name = name + '.py'
+ path = os.path.join(activity_bundle.get_path(), file_name)
+ self._selected_file = path
+
+ self._file_viewer = FileViewer(bundle_path, file_name)
+ self._file_viewer.connect('file-selected', self.__file_selected_cb)
+ pane.add1(self._file_viewer)
+ self._file_viewer.show()
+
+ self._source_display = SourceDisplay()
+ pane.add2(self._source_display)
+ self._source_display.show()
+ self._source_display.file_path = self._selected_file
+
+ if document_path is not None:
+ self._select_source(document_path)
+
+ def _calculate_char_width(self, char_count):
+ widget = gtk.Label('')
+ context = widget.get_pango_context()
+ pango_font = context.load_font(_SOURCE_FONT)
+ metrics = pango_font.get_metrics()
+ return pango.PIXELS(metrics.get_approximate_char_width()) * char_count
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+
+ parent = gtk.gdk.window_foreign_new(self._parent_window_xid)
+ self.window.set_transient_for(parent)
+
+ def __stop_clicked_cb(self, widget):
+ self.destroy()
+
+ def __source_selected_cb(self, widget, path):
+ self._select_source(path)
+
+ def _select_source(self, path):
+ if os.path.isfile(path):
+ self._source_display.file_path = path
+ self._file_viewer.hide()
+ else:
+ self._file_viewer.set_path(path)
+ self._source_display.file_path = self._selected_file
+ self._file_viewer.show()
+
+ def __destroy_cb(self, window, document_path):
+ del map_activity_to_window[self._parent_window_xid]
+ if document_path is not None and os.path.exists(document_path):
+ os.unlink(document_path)
+
+ def __key_press_event_cb(self, window, event):
+ keyname = gtk.gdk.keyval_name(event.keyval)
+ if keyname == 'Escape':
+ self.destroy()
+
+ def __file_selected_cb(self, file_viewer, file_path):
+ if file_path is not None and os.path.isfile(file_path):
+ self._source_display.file_path = file_path
+ self._selected_file = file_path
+ else:
+ self._source_display.file_path = None
+
+class DocumentButton(RadioToolButton):
+ __gtype_name__ = 'SugarDocumentButton'
+
+ def __init__(self, file_name, document_path, title):
+ RadioToolButton.__init__(self)
+
+ self._document_path = document_path
+ self._title = title
+ self._jobject = None
+
+ self.props.tooltip = _('Instance Source')
+
+ client = gconf.client_get_default()
+ self._color = client.get_string('/desktop/sugar/user/color')
+ icon = Icon(file=file_name,
+ icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR,
+ xo_color=XoColor(self._color))
+ self.set_icon_widget(icon)
+ icon.show()
+
+ menu_item = MenuItem(_('Keep'))
+ icon = Icon(icon_name='document-save', icon_size=gtk.ICON_SIZE_MENU,
+ xo_color=XoColor(self._color))
+ menu_item.set_image(icon)
+
+ menu_item.connect('activate', self.__keep_in_journal_cb)
+ self.props.palette.menu.append(menu_item)
+ menu_item.show()
+
+ def __keep_in_journal_cb(self, menu_item):
+ mime_type = mime.get_from_file_name(self._document_path)
+ if mime_type == 'application/octet-stream':
+ mime_type = mime.get_for_file(self._document_path)
+
+ self._jobject = datastore.create()
+ title = _('Source') + ': ' + self._title
+ self._jobject.metadata['title'] = title
+ self._jobject.metadata['keep'] = '0'
+ self._jobject.metadata['buddies'] = ''
+ self._jobject.metadata['preview'] = ''
+ self._jobject.metadata['icon-color'] = self._color
+ self._jobject.metadata['mime_type'] = mime_type
+ self._jobject.metadata['source'] = '1'
+ self._jobject.file_path = self._document_path
+ datastore.write(self._jobject, transfer_ownership=True,
+ reply_handler=self.__internal_save_cb,
+ error_handler=self.__internal_save_error_cb)
+
+ def __internal_save_cb(self):
+ 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,
+ ([str])),
+ }
+
+ def __init__(self, title, bundle_path, document_path):
+ gtk.Toolbar.__init__(self)
+
+ document_button = None
+
+ self._add_separator()
+
+ activity_bundle = ActivityBundle(bundle_path)
+ file_name = activity_bundle.get_icon()
+
+ if document_path is not None and os.path.exists(document_path):
+ document_button = DocumentButton(file_name, document_path, title)
+ document_button.connect('toggled', self.__button_toggled_cb,
+ document_path)
+ self.insert(document_button, -1)
+ document_button.show()
+ self._add_separator()
+
+ if bundle_path is not None and os.path.exists(bundle_path):
+ activity_button = RadioToolButton()
+ icon = Icon(file=file_name,
+ icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR,
+ fill_color=style.COLOR_TRANSPARENT.get_svg(),
+ stroke_color=style.COLOR_WHITE.get_svg())
+ activity_button.set_icon_widget(icon)
+ icon.show()
+ if document_button is not None:
+ activity_button.props.group = document_button
+ activity_button.props.tooltip = _('Activity Bundle Source')
+ activity_button.connect('toggled', self.__button_toggled_cb,
+ bundle_path)
+ self.insert(activity_button, -1)
+ activity_button.show()
+ self._add_separator()
+
+ text = _('View source: %r') % title
+ label = gtk.Label()
+ label.set_markup('<b>%s</b>' % text)
+ label.set_alignment(0, 0.5)
+ self._add_widget(label)
+
+ self._add_separator(True)
+
+ stop = ToolButton(icon_name='dialog-cancel')
+ stop.set_tooltip(_('Close'))
+ stop.connect('clicked', self.__stop_clicked_cb)
+ self.insert(stop, -1)
+ stop.show()
+
+ def _add_separator(self, expand=False):
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ if expand:
+ separator.set_expand(True)
+ else:
+ separator.set_size_request(style.DEFAULT_SPACING, -1)
+ self.insert(separator, -1)
+ separator.show()
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+ def __stop_clicked_cb(self, button):
+ self.emit('stop-clicked')
+
+ def __button_toggled_cb(self, button, path):
+ if button.props.active:
+ self.emit('source-selected', path)
+
+class FileViewer(gtk.ScrolledWindow):
+ __gtype_name__ = 'SugarFileViewer'
+
+ __gsignals__ = {
+ 'file-selected': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str])),
+ }
+
+ def __init__(self, path, initial_filename):
+ gtk.ScrolledWindow.__init__(self)
+
+ self.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
+ self.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
+ self.set_size_request(style.GRID_CELL_SIZE * 3, -1)
+
+ self._path = None
+ self._initial_filename = initial_filename
+
+ self._tree_view = gtk.TreeView()
+ self.add(self._tree_view)
+ self._tree_view.show()
+
+ self._tree_view.props.headers_visible = False
+ selection = self._tree_view.get_selection()
+ selection.connect('changed', self.__selection_changed_cb)
+
+ cell = gtk.CellRendererText()
+ column = gtk.TreeViewColumn()
+ column.pack_start(cell, True)
+ column.add_attribute(cell, 'text', 0)
+ self._tree_view.append_column(column)
+ self._tree_view.set_search_column(0)
+
+ self.set_path(path)
+
+ def set_path(self, path):
+ self.emit('file-selected', None)
+ if self._path == path:
+ return
+ self._path = path
+ self._tree_view.set_model(gtk.TreeStore(str, str))
+ self._add_dir_to_model(path)
+
+ def _add_dir_to_model(self, dir_path, parent=None):
+ model = self._tree_view.get_model()
+ for f in os.listdir(dir_path):
+ if not f.endswith('.pyc'):
+ full_path = os.path.join(dir_path, f)
+ if os.path.isdir(full_path):
+ new_iter = model.append(parent, [f, full_path])
+ self._add_dir_to_model(full_path, new_iter)
+ else:
+ current_iter = model.append(parent, [f, full_path])
+ if f == self._initial_filename:
+ selection = self._tree_view.get_selection()
+ selection.select_iter(current_iter)
+
+ def __selection_changed_cb(self, selection):
+ model, tree_iter = selection.get_selected()
+ if tree_iter is None:
+ file_path = None
+ else:
+ file_path = model.get_value(tree_iter, 1)
+ self.emit('file-selected', file_path)
+
+class SourceDisplay(gtk.ScrolledWindow):
+ __gtype_name__ = 'SugarSourceDisplay'
+
+ def __init__(self):
+ gtk.ScrolledWindow.__init__(self)
+
+ self.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
+ self.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
+
+ self._buffer = gtksourceview2.Buffer()
+ self._buffer.set_highlight_syntax(True)
+
+ self._source_view = gtksourceview2.View(self._buffer)
+ self._source_view.set_editable(False)
+ self._source_view.set_cursor_visible(True)
+ self._source_view.set_show_line_numbers(True)
+ self._source_view.set_show_right_margin(True)
+ self._source_view.set_right_margin_position(80)
+ #self._source_view.set_highlight_current_line(True) #FIXME: Ugly color
+ self._source_view.modify_font(_SOURCE_FONT)
+ self.add(self._source_view)
+ self._source_view.show()
+
+ self._file_path = None
+
+ def _set_file_path(self, file_path):
+ if file_path == self._file_path:
+ return
+ self._file_path = file_path
+
+ if self._file_path is None:
+ self._buffer.set_text('')
+ return
+
+ mime_type = mime.get_for_file(self._file_path)
+ logging.debug('Detected mime type: %r', mime_type)
+
+ language_manager = gtksourceview2.language_manager_get_default()
+ detected_language = None
+ for language_id in language_manager.get_language_ids():
+ language = language_manager.get_language(language_id)
+ if mime_type in language.get_mime_types():
+ detected_language = language
+ break
+
+ if detected_language is not None:
+ logging.debug('Detected language: %r',
+ detected_language.get_name())
+
+ self._buffer.set_language(detected_language)
+ self._buffer.set_text(open(self._file_path, 'r').read())
+
+ def _get_file_path(self):
+ return self._file_path
+
+ file_path = property(_get_file_path, _set_file_path)
+