diff options
author | Julio Reyes <nemesiscodex@gmail.com> | 2013-07-05 14:13:11 (GMT) |
---|---|---|
committer | Julio Reyes <nemesiscodex@gmail.com> | 2013-07-05 14:13:11 (GMT) |
commit | b6db5b8af0af7ebfdd44cb53b09d63b819989338 (patch) | |
tree | ca439018bfa1c34c97bdaed4ec0f20bc36bd678f /src/jarabe/desktop |
Diffstat (limited to 'src/jarabe/desktop')
-rw-r--r-- | src/jarabe/desktop/Makefile.am | 18 | ||||
-rw-r--r-- | src/jarabe/desktop/Makefile.in | 455 | ||||
-rw-r--r-- | src/jarabe/desktop/__init__.py | 15 | ||||
-rw-r--r-- | src/jarabe/desktop/activitieslist.py | 461 | ||||
-rw-r--r-- | src/jarabe/desktop/favoriteslayout.py | 560 | ||||
-rw-r--r-- | src/jarabe/desktop/favoritesview.py | 702 | ||||
-rw-r--r-- | src/jarabe/desktop/friendview.py | 84 | ||||
-rw-r--r-- | src/jarabe/desktop/grid.py | 204 | ||||
-rw-r--r-- | src/jarabe/desktop/groupbox.py | 94 | ||||
-rw-r--r-- | src/jarabe/desktop/homebox.py | 295 | ||||
-rw-r--r-- | src/jarabe/desktop/homewindow.py | 209 | ||||
-rw-r--r-- | src/jarabe/desktop/keydialog.py | 317 | ||||
-rw-r--r-- | src/jarabe/desktop/meshbox.py | 679 | ||||
-rw-r--r-- | src/jarabe/desktop/networkviews.py | 708 | ||||
-rw-r--r-- | src/jarabe/desktop/schoolserver.py | 173 | ||||
-rw-r--r-- | src/jarabe/desktop/snowflakelayout.py | 111 | ||||
-rw-r--r-- | src/jarabe/desktop/spreadlayout.py | 89 | ||||
-rw-r--r-- | src/jarabe/desktop/transitionbox.py | 99 |
18 files changed, 5273 insertions, 0 deletions
diff --git a/src/jarabe/desktop/Makefile.am b/src/jarabe/desktop/Makefile.am new file mode 100644 index 0000000..25fb0b4 --- /dev/null +++ b/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/src/jarabe/desktop/Makefile.in b/src/jarabe/desktop/Makefile.in new file mode 100644 index 0000000..ef0f449 --- /dev/null +++ b/src/jarabe/desktop/Makefile.in @@ -0,0 +1,455 @@ +# Makefile.in generated by automake 1.11.3 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, +# 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Free Software +# Foundation, Inc. +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ +VPATH = @srcdir@ +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +subdir = src/jarabe/desktop +DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in \ + $(sugar_PYTHON) +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(install_sh) -d +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +SOURCES = +DIST_SOURCES = +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +am__py_compile = PYTHON=$(PYTHON) $(SHELL) $(py_compile) +am__installdirs = "$(DESTDIR)$(sugardir)" +py_compile = $(top_srcdir)/py-compile +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +ALL_LINGUAS = @ALL_LINGUAS@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +CATALOGS = @CATALOGS@ +CATOBJEXT = @CATOBJEXT@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DATADIRNAME = @DATADIRNAME@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +GCONFTOOL = @GCONFTOOL@ +GCONF_SCHEMA_CONFIG_SOURCE = @GCONF_SCHEMA_CONFIG_SOURCE@ +GCONF_SCHEMA_FILE_DIR = @GCONF_SCHEMA_FILE_DIR@ +GETTEXT_PACKAGE = @GETTEXT_PACKAGE@ +GMOFILES = @GMOFILES@ +GMSGFMT = @GMSGFMT@ +GREP = @GREP@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +INSTOBJEXT = @INSTOBJEXT@ +INTLLIBS = @INTLLIBS@ +INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@ +INTLTOOL_MERGE = @INTLTOOL_MERGE@ +INTLTOOL_PERL = @INTLTOOL_PERL@ +INTLTOOL_UPDATE = @INTLTOOL_UPDATE@ +INTLTOOL_V_MERGE = @INTLTOOL_V_MERGE@ +INTLTOOL_V_MERGE_OPTIONS = @INTLTOOL_V_MERGE_OPTIONS@ +INTLTOOL__v_MERGE_ = @INTLTOOL__v_MERGE_@ +INTLTOOL__v_MERGE_0 = @INTLTOOL__v_MERGE_0@ +LDFLAGS = @LDFLAGS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LTLIBOBJS = @LTLIBOBJS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MKDIR_P = @MKDIR_P@ +MKINSTALLDIRS = @MKINSTALLDIRS@ +MSGFMT = @MSGFMT@ +MSGFMT_OPTS = @MSGFMT_OPTS@ +MSGMERGE = @MSGMERGE@ +OBJEXT = @OBJEXT@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ +PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +POFILES = @POFILES@ +POSUB = @POSUB@ +PO_IN_DATADIR_FALSE = @PO_IN_DATADIR_FALSE@ +PO_IN_DATADIR_TRUE = @PO_IN_DATADIR_TRUE@ +PYTHON = @PYTHON@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_VERSION = @PYTHON_VERSION@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +SHELL_CFLAGS = @SHELL_CFLAGS@ +SHELL_LIBS = @SHELL_LIBS@ +STRIP = @STRIP@ +SUCROSE_VERSION = @SUCROSE_VERSION@ +USE_NLS = @USE_NLS@ +VERSION = @VERSION@ +XGETTEXT = @XGETTEXT@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_CC = @ac_ct_CC@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build_alias = @build_alias@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host_alias = @host_alias@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +intltool__v_merge_options_ = @intltool__v_merge_options_@ +intltool__v_merge_options_0 = @intltool__v_merge_options_0@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +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 + +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/jarabe/desktop/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/desktop/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): +install-sugarPYTHON: $(sugar_PYTHON) + @$(NORMAL_INSTALL) + test -z "$(sugardir)" || $(MKDIR_P) "$(DESTDIR)$(sugardir)" + @list='$(sugar_PYTHON)'; dlist=; list2=; test -n "$(sugardir)" || list=; \ + for p in $$list; do \ + if test -f "$$p"; then b=; else b="$(srcdir)/"; fi; \ + if test -f $$b$$p; then \ + $(am__strip_dir) \ + dlist="$$dlist $$f"; \ + list2="$$list2 $$b$$p"; \ + else :; fi; \ + done; \ + for file in $$list2; do echo $$file; done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(sugardir)'"; \ + $(INSTALL_DATA) $$files "$(DESTDIR)$(sugardir)" || exit $$?; \ + done || exit $$?; \ + if test -n "$$dlist"; then \ + $(am__py_compile) --destdir "$(DESTDIR)" \ + --basedir "$(sugardir)" $$dlist; \ + else :; fi + +uninstall-sugarPYTHON: + @$(NORMAL_UNINSTALL) + @list='$(sugar_PYTHON)'; test -n "$(sugardir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + test -n "$$files" || exit 0; \ + dir='$(DESTDIR)$(sugardir)'; \ + filesc=`echo "$$files" | sed 's|$$|c|'`; \ + fileso=`echo "$$files" | sed 's|$$|o|'`; \ + st=0; \ + for files in "$$files" "$$filesc" "$$fileso"; do \ + $(am__uninstall_files_from_dir) || st=$$?; \ + done; \ + exit $$st +tags: TAGS +TAGS: + +ctags: CTAGS +CTAGS: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile +installdirs: + for dir in "$(DESTDIR)$(sugardir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-sugarPYTHON + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-sugarPYTHON + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-generic distclean \ + distclean-generic distdir dvi dvi-am html html-am info info-am \ + install install-am install-data install-data-am install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-pdf install-pdf-am install-ps install-ps-am \ + install-strip install-sugarPYTHON installcheck installcheck-am \ + installdirs maintainer-clean maintainer-clean-generic \ + mostlyclean mostlyclean-generic pdf pdf-am ps ps-am uninstall \ + uninstall-am uninstall-sugarPYTHON + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/src/jarabe/desktop/__init__.py b/src/jarabe/desktop/__init__.py new file mode 100644 index 0000000..85f6a24 --- /dev/null +++ b/src/jarabe/desktop/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/jarabe/desktop/activitieslist.py b/src/jarabe/desktop/activitieslist.py new file mode 100644 index 0000000..7bf0960 --- /dev/null +++ b/src/jarabe/desktop/activitieslist.py @@ -0,0 +1,461 @@ +# 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 jarabe.model import bundleregistry +from jarabe.view.palettes import ActivityPalette +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) + self.set_enable_search(False) + + 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, str, 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 + 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() + + +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, delete_profile=True) + + +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(): + self._add_erase_option(registry, activity_info) + + 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 _add_erase_option(self, registry, activity_info): + menu_item = MenuItem(_('Erase'), 'list-remove') + menu_item.connect('activate', self.__erase_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + if not os.access(activity_info.get_path(), os.W_OK) or \ + registry.is_activity_protected(self._bundle_id): + menu_item.props.sensitive = False + + def __destroy_cb(self, palette): + registry = bundleregistry.get_registry() + registry.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/src/jarabe/desktop/favoriteslayout.py b/src/jarabe/desktop/favoriteslayout.py new file mode 100644 index 0000000..360c147 --- /dev/null +++ b/src/jarabe/desktop/favoriteslayout.py @@ -0,0 +1,560 @@ +# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2010 Sugar Labs +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# 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 +_INTERMEDIATE_B = (style.STANDARD_ICON_SIZE + style.SMALL_ICON_SIZE) / 2 +_INTERMEDIATE_A = (style.STANDARD_ICON_SIZE + _INTERMEDIATE_B) / 2 +_INTERMEDIATE_C = (_INTERMEDIATE_B + style.SMALL_ICON_SIZE) / 2 +_ICON_SIZES = [style.MEDIUM_ICON_SIZE, style.STANDARD_ICON_SIZE, + _INTERMEDIATE_A, _INTERMEDIATE_B, _INTERMEDIATE_C, + style.SMALL_ICON_SIZE] + + +class FavoritesLayout(gobject.GObject, hippo.CanvasLayout): + """Base class of the different layout types.""" + + __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 +_ICON_SPACING_FACTORS = [1.5, 1.4, 1.3, 1.2, 1.1, 1.0] +_SPIRAL_SPACING_FACTORS = [1.5, 1.5, 1.5, 1.4, 1.3, 1.2] +_MIMIMUM_RADIUS_ENCROACHMENT = 0.75 +_INITIAL_ANGLE = math.pi + + +class RingLayout(FavoritesLayout): + """Lay out icons in a ring or spiral 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 = {} + self._spiral_mode = False + + 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): + """ Adjust the ring or spiral radius and icon size as needed. """ + self._spiral_mode = False + distance = style.MEDIUM_ICON_SIZE + style.DEFAULT_SPACING * \ + _ICON_SPACING_FACTORS[_ICON_SIZES.index(style.MEDIUM_ICON_SIZE)] + radius = max(children_count * distance / (2 * math.pi), + _MINIMUM_RADIUS) + if radius < _MAXIMUM_RADIUS: + return radius, style.MEDIUM_ICON_SIZE + + distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING * \ + _ICON_SPACING_FACTORS[_ICON_SIZES.index(style.STANDARD_ICON_SIZE)] + radius = max(children_count * distance / (2 * math.pi), + _MINIMUM_RADIUS) + if radius < _MAXIMUM_RADIUS: + return radius, style.STANDARD_ICON_SIZE + + self._spiral_mode = True + icon_size = style.STANDARD_ICON_SIZE + angle_, radius = self._calculate_angle_and_radius(children_count, + icon_size) + while radius > _MAXIMUM_RADIUS: + i = _ICON_SIZES.index(icon_size) + if i < len(_ICON_SIZES) - 1: + icon_size = _ICON_SIZES[i + 1] + angle_, radius = self._calculate_angle_and_radius( + children_count, icon_size) + else: + break + return radius, icon_size + + def _calculate_position(self, radius, icon_size, icon_index, + children_count, sin=math.sin, cos=math.cos): + """ Calculate an icon position on a circle or a spiral. """ + width, height = self.box.get_allocation() + if self._spiral_mode: + min_width_, box_width = self.box.get_width_request() + min_height_, box_height = self.box.get_height_request(box_width) + angle, radius = self._calculate_angle_and_radius(icon_index, + icon_size) + x, y = self._convert_from_polar_to_cartesian(angle, radius, + icon_size, + width, height) + else: + angle = icon_index * (2 * math.pi / children_count) - math.pi / 2 + x = radius * cos(angle) + (width - icon_size) / 2 + y = radius * sin(angle) + (height - icon_size - \ + (style.GRID_CELL_SIZE / 2)) / 2 + return x, y + + def _convert_from_polar_to_cartesian(self, angle, radius, icon_size, width, + height): + """ Convert angle, radius to x, y """ + x = int(math.sin(angle) * radius) + y = int(math.cos(angle) * radius) + x = - x + (width - icon_size) / 2 + y = y + (height - icon_size - (style.GRID_CELL_SIZE / 2)) / 2 + return x, y + + def _calculate_angle_and_radius(self, icon_count, icon_size): + """ Based on icon_count and icon_size, calculate radius and angle. """ + spiral_spacing = _SPIRAL_SPACING_FACTORS[_ICON_SIZES.index(icon_size)] + icon_spacing = icon_size + style.DEFAULT_SPACING * \ + _ICON_SPACING_FACTORS[_ICON_SIZES.index(icon_size)] + angle = _INITIAL_ANGLE + radius = _MINIMUM_RADIUS - (icon_size * _MIMIMUM_RADIUS_ENCROACHMENT) + for i_ in range(icon_count): + circumference = radius * 2 * math.pi + n = circumference / icon_spacing + angle += (2 * math.pi / n) + radius += (float(icon_spacing) * spiral_spacing / n) + return angle, radius + + def _get_children_in_ring(self): + children_in_ring = [child for child in self.box.get_layout_children() \ + if child not in self._locked_children] + 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) + # try again + continue + + 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 + # mirror around 180 + return cos_d(360 - d) + + cos = lambda r: cos_d(math.degrees(r)) + sin = lambda r: cos_d(math.degrees(r) - 90) + + return RingLayout._calculate_position(self, radius, icon_size, index, + children_count, sin=sin, + cos=cos) + + +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. + # mirror around 90 + return -cos_d(180 - d) + + 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 + # mirror around 90 + return sin_d(180 - d) + + cos = lambda r: cos_d(math.degrees(r)) + sin = lambda r: sin_d(math.degrees(r)) + + return RingLayout._calculate_position(self, radius, icon_size, index, + children_count, sin=sin, + cos=cos) diff --git a/src/jarabe/desktop/favoritesview.py b/src/jarabe/desktop/favoritesview.py new file mode 100644 index 0000000..654f400 --- /dev/null +++ b/src/jarabe/desktop/favoritesview.py @@ -0,0 +1,702 @@ +# 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 glib +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 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.model.buddy import get_owner_instance +from jarabe.model import shell +from jarabe.model import bundleregistry +from jarabe.journal import misc + +from jarabe.desktop import schoolserver +from jarabe.desktop.schoolserver import RegisterError +from jarabe.desktop 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.""" + +_favorites_settings = None + + +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 set_filter(self, query): + query = query.strip() + for icon in self._box.get_children(): + if icon not in [self._my_icon, self._current_activity]: + activity_name = icon.get_activity_name().lower() + if activity_name.find(query) > -1: + icon.alpha = 1.0 + else: + icon.alpha = 0.33 + + 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.set_registered() + + 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_activity_name(self): + return self._activity_info.get_name() + + 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: + title = journal_entries[0]['title'] + self.props.secondary_text = glib.markup_escape_text(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_invoker.cache_palette = True + + self._palette_enabled = False + self._register_menu = None + + def create_palette(self): + if not self._palette_enabled: + self._palette_enabled = True + return + + palette = BuddyMenu(get_owner_instance()) + + client = gconf.client_get_default() + backup_url = client.get_string('/desktop/sugar/backup_url') + + if not backup_url: + self._register_menu = MenuItem(_('Register'), 'media-record') + else: + self._register_menu = MenuItem(_('Register again'), + 'media-record') + + self._register_menu.connect('activate', self.__register_activate_cb) + palette.menu.append(self._register_menu) + self._register_menu.show() + + return palette + + def get_toplevel(self): + return hippo.get_canvas_for_item(self).get_toplevel() + + def __register_activate_cb(self, menuitem): + self.emit('register-activate') + + def set_registered(self): + self.palette.menu.remove(self._register_menu) + self._register_menu = MenuItem(_('Register again'), 'media-record') + self._register_menu.connect('activate', self.__register_activate_cb) + self.palette.menu.append(self._register_menu) + self._register_menu.show() + + +class FavoritesSetting(object): + + _FAVORITES_KEY = '/desktop/sugar/desktop/favorites_layout' + + 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) + + +def get_settings(): + global _favorites_settings + if _favorites_settings is None: + _favorites_settings = FavoritesSetting() + return _favorites_settings diff --git a/src/jarabe/desktop/friendview.py b/src/jarabe/desktop/friendview.py new file mode 100644 index 0000000..8dab35f --- /dev/null +++ b/src/jarabe/desktop/friendview.py @@ -0,0 +1,84 @@ +# 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 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._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/src/jarabe/desktop/grid.py b/src/jarabe/desktop/grid.py new file mode 100644 index 0000000..eab4033 --- /dev/null +++ b/src/jarabe/desktop/grid.py @@ -0,0 +1,204 @@ +# Copyright (C) 2007 Red Hat, Inc. +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import random + +import gobject +import gtk + +from sugar import _sugarext + + +_PLACE_TRIALS = 20 +_MAX_WEIGHT = 255 +_REFRESH_RATE = 200 +_MAX_COLLISIONS_PER_REFRESH = 20 + + +class Grid(_sugarext.Grid): + __gsignals__ = { + 'child-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, width, height): + gobject.GObject.__init__(self) + + self.width = width + self.height = height + self._children = [] + self._child_rects = {} + self._locked_children = set() + self._collisions = [] + self._collisions_sid = 0 + + self.setup(self.width, self.height) + + def add(self, child, width, height, x=None, y=None, locked=False): + if x is not None and y is not None: + rect = gtk.gdk.Rectangle(x, y, width, height) + weight = self.compute_weight(rect) + else: + trials = _PLACE_TRIALS + weight = _MAX_WEIGHT + while trials > 0 and weight: + x = int(random.random() * (self.width - width)) + y = int(random.random() * (self.height - height)) + + rect = gtk.gdk.Rectangle(x, y, width, height) + new_weight = self.compute_weight(rect) + if weight > new_weight: + weight = new_weight + + trials -= 1 + + self._child_rects[child] = rect + self._children.append(child) + self.add_weight(self._child_rects[child]) + if locked: + self._locked_children.add(child) + + if weight > 0: + self._detect_collisions(child) + + def remove(self, child): + self._children.remove(child) + self.remove_weight(self._child_rects[child]) + self._locked_children.discard(child) + del self._child_rects[child] + + if child in self._collisions: + self._collisions.remove(child) + + def move(self, child, x, y, locked=False): + self.remove_weight(self._child_rects[child]) + + rect = self._child_rects[child] + rect.x = x + rect.y = y + + weight = self.compute_weight(rect) + self.add_weight(self._child_rects[child]) + + if locked: + self._locked_children.add(child) + else: + self._locked_children.discard(child) + + if weight > 0: + self._detect_collisions(child) + + def _shift_child(self, child, weight): + rect = self._child_rects[child] + + new_rects = [] + + # Get rects right, left, bottom and top + if (rect.x + rect.width < self.width - 1): + new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y, + rect.width, rect.height)) + + if (rect.x - 1 > 0): + new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y, + rect.width, rect.height)) + + if (rect.y + rect.height < self.height - 1): + new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y + 1, + rect.width, rect.height)) + + if (rect.y - 1 > 0): + new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y - 1, + rect.width, rect.height)) + + # Get diagonal rects + if rect.x + rect.width < self.width - 1 and \ + rect.y + rect.height < self.height - 1: + new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y + 1, + rect.width, rect.height)) + + if rect.x - 1 > 0 and rect.y + rect.height < self.height - 1: + new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y + 1, + rect.width, rect.height)) + + if rect.x + rect.width < self.width - 1 and rect.y - 1 > 0: + new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y - 1, + rect.width, rect.height)) + + if rect.x - 1 > 0 and rect.y - 1 > 0: + new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y - 1, + rect.width, rect.height)) + + random.shuffle(new_rects) + + best_rect = None + for new_rect in new_rects: + new_weight = self.compute_weight(new_rect) + if new_weight < weight: + best_rect = new_rect + weight = new_weight + + if best_rect: + self._child_rects[child] = best_rect + weight = self._shift_child(child, weight) + + return weight + + def __solve_collisions_cb(self): + for i_ in range(_MAX_COLLISIONS_PER_REFRESH): + collision = self._collisions.pop(0) + + old_rect = self._child_rects[collision] + self.remove_weight(old_rect) + weight = self.compute_weight(old_rect) + weight = self._shift_child(collision, weight) + self.add_weight(self._child_rects[collision]) + + # TODO: we shouldn't give up the first time we failed to find a + # better position. + if old_rect != self._child_rects[collision]: + self._detect_collisions(collision) + self.emit('child-changed', collision) + if weight > 0: + self._collisions.append(collision) + + if not self._collisions: + self._collisions_sid = 0 + return False + + return True + + def _detect_collisions(self, child): + collision_found = False + child_rect = self._child_rects[child] + for c in self._children: + intersection = child_rect.intersect(self._child_rects[c]) + if c != child and intersection.width > 0: + if (c not in self._locked_children and + c not in self._collisions): + collision_found = True + self._collisions.append(c) + + if collision_found: + if child not in self._collisions: + self._collisions.append(child) + + if self._collisions and not self._collisions_sid: + self._collisions_sid = gobject.timeout_add(_REFRESH_RATE, + self.__solve_collisions_cb, priority=gobject.PRIORITY_LOW) + + def get_child_rect(self, child): + return self._child_rects[child] diff --git a/src/jarabe/desktop/groupbox.py b/src/jarabe/desktop/groupbox.py new file mode 100644 index 0000000..ed8f8ae --- /dev/null +++ b/src/jarabe/desktop/groupbox.py @@ -0,0 +1,94 @@ +# 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/src/jarabe/desktop/homebox.py b/src/jarabe/desktop/homebox.py new file mode 100644 index 0000000..2ee6ae7 --- /dev/null +++ b/src/jarabe/desktop/homebox.py @@ -0,0 +1,295 @@ +# 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) + self._query = '' + + 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): + self._query = query.lower() + self._list_view.set_filter(self._query) + self._favorites_view.set_filter(self._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) + if resume_mode and self._query != '': + self._list_view.set_filter(self._query) + self._favorites_view.set_filter(self._query) + + +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.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: + 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 + + 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/src/jarabe/desktop/homewindow.py b/src/jarabe/desktop/homewindow.py new file mode 100644 index 0000000..07deff7 --- /dev/null +++ b/src/jarabe/desktop/homewindow.py @@ -0,0 +1,209 @@ +# 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 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 + +_instance = None + + +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 + + def busy_during_delayed_action(self, action): + """Use busy cursor during execution of action, scheduled via idle_add. + """ + def action_wrapper(old_cursor): + try: + action() + finally: + self.get_window().set_cursor(old_cursor) + + old_cursor = self.get_window().get_cursor() + self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + gobject.idle_add(action_wrapper, old_cursor) + + +def get_instance(): + global _instance + if not _instance: + _instance = HomeWindow() + return _instance diff --git a/src/jarabe/desktop/keydialog.py b/src/jarabe/desktop/keydialog.py new file mode 100644 index 0000000..41c2a51 --- /dev/null +++ b/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 + + +IW_AUTH_ALG_OPEN_SYSTEM = 'open' +IW_AUTH_ALG_SHARED_KEY = 'shared' + +WEP_PASSPHRASE = 1 +WEP_HEX = 2 +WEP_ASCII = 3 + + +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, response): + gtk.Dialog.__init__(self, flags=gtk.DIALOG_MODAL) + self.set_title('Wireless Key Required') + + 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) + + display_name = network.ssid_to_display_name(ssid) + label = gtk.Label(_("A wireless encryption key is required for\n" + " the wireless network '%s'.") % (display_name, )) + 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 + + +class WEPKeyDialog(KeyDialog): + def __init__(self, ssid, flags, wpa_flags, rsn_flags, dev_caps, response): + KeyDialog.__init__(self, ssid, flags, wpa_flags, rsn_flags, + dev_caps, 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() + wsec = {'wep-key0': key, 'auth-alg': auth_alg} + return {'802-11-wireless-security': wsec} + + 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, response): + KeyDialog.__init__(self, ssid, flags, wpa_flags, rsn_flags, + dev_caps, 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): + wsec = {'psk': self._get_security()} + return {'802-11-wireless-security': wsec} + + 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, 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, response) + else: + key_dialog = WPAKeyDialog(ssid, flags, wpa_flags, rsn_flags, + dev_caps, response) + + key_dialog.connect('response', _key_dialog_response_cb) + key_dialog.show_all() + + +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, + gtk.RESPONSE_DELETE_EVENT]: + # 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/src/jarabe/desktop/meshbox.py b/src/jarabe/desktop/meshbox.py new file mode 100644 index 0000000..20dc413 --- /dev/null +++ b/src/jarabe/desktop/meshbox.py @@ -0,0 +1,679 @@ +# 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 + + +_AP_ICON_NAME = 'network-wireless' +_OLPC_MESH_ICON_NAME = 'network-mesh' + +_AUTOSEARCH_TIMEOUT = 1000 +_FILTERED_ALPHA = 0.33 + + +class _ActivityIcon(CanvasIcon): + def __init__(self, model, file_name, xo_color, + size=style.STANDARD_ICON_SIZE): + CanvasIcon.__init__(self, file_name=file_name, + xo_color=xo_color, + size=size) + self._model = model + self.connect('activated', self._clicked_cb) + + def create_palette(self): + primary_text = glib.markup_escape_text(self._model.bundle.get_name()) + secondary_text = glib.markup_escape_text(self._model.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=primary_text, + secondary_text=secondary_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 _clicked_cb(self, item): + bundle = self._model.get_bundle() + misc.launch(bundle, activity_id=self._model.activity_id, + color=self._model.get_color()) + + +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._layout = SnowflakeLayout() + self.set_layout(self._layout) + + self._icon = self._create_icon() + self._layout.add(self._icon, center=True) + + self._icon.palette_invoker.cache_palette = False + + for buddy in self._model.props.current_buddies: + self._add_buddy(buddy) + + def _create_icon(self): + icon = _ActivityIcon(self._model, + file_name=self._model.bundle.get_icon(), + xo_color=self._model.get_color(), + size=style.STANDARD_ICON_SIZE) + return icon + + def has_buddy_icon(self, key): + return key in self._icons + + 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 set_filter(self, query): + text_to_check = self._model.bundle.get_name().lower() + \ + self._model.bundle.get_bundle_id().lower() + self._icon.props.xo_color = self._model.get_color() + if text_to_check.find(query) == -1: + self._icon.alpha = _FILTERED_ALPHA + else: + self._icon.alpha = 1.0 + for icon in self._icons.itervalues(): + if hasattr(icon, 'set_filter'): + icon.set_filter(query) + + +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, network.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=network.NM_WIRELESS_IFACE) + self._bus.add_signal_receiver(self.__access_point_removed_cb, + signal_name='AccessPointRemoved', + path=device.object_path, + dbus_interface=network.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(network.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(network.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=network.NM_WIRELESS_IFACE) + self._bus.remove_signal_receiver(self.__access_point_removed_cb, + signal_name='AccessPointRemoved', + path=self.device.object_path, + dbus_interface=network.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() + self._netmgr = network.get_manager() + except dbus.DBusException: + logging.debug('NetworkManager not available') + 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=network.NM_IFACE) + self._bus.add_signal_receiver(self.__device_removed_cb, + signal_name='DeviceRemoved', + dbus_interface=network.NM_IFACE) + self._bus.add_signal_receiver(self.__properties_changed_cb, + signal_name='PropertiesChanged', + dbus_interface=network.NM_IFACE) + + secret_agent = network.get_secret_agent() + if secret_agent is not None: + secret_agent.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, dbus.PROPERTIES_IFACE) + active_connections_o = netmgr_props.Get(network.NM_IFACE, 'ActiveConnections') + + for conn_o in active_connections_o: + obj = self._bus.get_object(network.NM_IFACE, conn_o) + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) + state = props.Get(network.NM_ACTIVE_CONN_IFACE, 'State') + if state == network.NM_ACTIVE_CONNECTION_STATE_ACTIVATING: + ap_o = props.Get(network.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 + net.create_keydialog(kwargs['response']) + if not found: + raise Exception('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(network.NM_SERVICE, device_o) + props = dbus.Interface(device, dbus.PROPERTIES_IFACE) + + device_type = props.Get(network.NM_DEVICE_IFACE, 'DeviceType') + if device_type == network.NM_DEVICE_TYPE_WIFI: + if device_o in self._devices: + return + 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.NM_DEVICE_TYPE_OLPC_MESH: + if device_o == self._olpc_mesh_device_o: + return + 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) + self._olpc_mesh_device_o = None + + 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): + 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_value = ap.network_hash() + if hash_value in self.wireless_networks: + self.wireless_networks[hash_value].add_ap(ap) + else: + # this is a new network + icon = WirelessNetworkView(ap) + self.wireless_networks[hash_value] = icon + self._layout.add(icon) + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + def _remove_net_if_empty(self, net, hash_value): + # remove a network if it has no APs left + if net.num_aps() == 0: + net.disconnect() + self._layout.remove(net) + del self.wireless_networks[hash_value] + + def _ap_props_changed_cb(self, ap, old_hash_value): + # if we have mesh hardware, ignore OLPC mesh networks that appear as + # normal wifi networks + if len(self._mesh) > 0 and ap.mode == network.NM_802_11_MODE_ADHOC \ + and ap.ssid == '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.ssid) and \ + ap.mode == network.NM_802_11_MODE_ADHOC: + if old_hash_value is None: + # new Ad-hoc network finished initializing + self._adhoc_manager.add_access_point(ap) + # we are called as well in other cases but we do not need to + # act here as we don't display signal strength for Ad-hoc networks + return + + if old_hash_value is None: + # new AP finished initializing + self._add_ap_to_network(ap) + return + + hash_value = ap.network_hash() + if old_hash_value == hash_value: + # no change in network identity, so just update signal strengths + self.wireless_networks[hash_value].update_strength() + return + + # properties change includes a change of the identity of the network + # that it is on. so create this as a new network. + self.wireless_networks[old_hash_value].remove_ap(ap) + self._remove_net_if_empty(self.wireless_networks[old_hash_value], + old_hash_value) + self._add_ap_to_network(ap) + + def add_access_point(self, device, ap_o): + 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 = [] + self._adhoc_manager.stop_listening() + + 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_value, 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_value] + + 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/src/jarabe/desktop/networkviews.py b/src/jarabe/desktop/networkviews.py new file mode 100644 index 0000000..f42bfed --- /dev/null +++ b/src/jarabe/desktop/networkviews.py @@ -0,0 +1,708 @@ +# 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 + + +_AP_ICON_NAME = 'network-wireless' +_OLPC_MESH_ICON_NAME = 'network-mesh' + +_FILTERED_ALPHA = 0.33 + + +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._filtered = False + self._ssid = initial_ap.ssid + self._display_name = network.ssid_to_display_name(self._ssid) + self._mode = initial_ap.mode + self._strength = initial_ap.strength + self._flags = initial_ap.flags + self._wpa_flags = initial_ap.wpa_flags + self._rsn_flags = initial_ap.rsn_flags + self._device_caps = 0 + self._device_state = None + self._color = None + + if self._mode == network.NM_802_11_MODE_ADHOC and \ + network.is_sugar_adhoc_network(self._ssid): + self._color = profile.get_color() + else: + sha_hash = hashlib.sha1() + data = self._ssid + hex(self._flags) + sha_hash.update(data) + digest = hash(sha_hash.digest()) + index = digest % len(xocolor.colors) + + self._color = xocolor.XoColor('%s,%s' % + (xocolor.colors[index][0], + xocolor.colors[index][1])) + + self.connect('button-release-event', self.__button_release_event_cb) + + pulse_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self.props.pulse_color = pulse_color + + self._palette = self._create_palette() + self.set_palette(self._palette) + self._palette_icon.props.xo_color = self._color + self._update_badge() + + interface_props = dbus.Interface(self._device, dbus.PROPERTIES_IFACE) + interface_props.Get(network.NM_WIRELESS_IFACE, 'WirelessCapabilities', + reply_handler=self.__get_device_caps_reply_cb, + error_handler=self.__get_device_caps_error_cb) + interface_props.Get(network.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=network.NM_DEVICE_IFACE) + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=network.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) + + label = glib.markup_escape_text(self._display_name) + p = palette.Palette(primary_text=label, icon=self._palette_icon) + + self._connect_item = MenuItem(_('Connect'), 'dialog-ok') + self._connect_item.connect('activate', self.__connect_activate_cb) + p.menu.append(self._connect_item) + + self._disconnect_item = MenuItem(_('Disconnect'), 'media-eject') + self._disconnect_item.connect('activate', + self._disconnect_activate_cb) + p.menu.append(self._disconnect_item) + + return p + + def __device_state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update_state() + self._update_icon() + self._update_badge() + self._update_color() + + def __update_active_ap(self, ap_path): + if ap_path in self._access_points: + # save reference to active AP, so that we always display the + # strength of that one + self._active_ap = self._access_points[ap_path] + self.update_strength() + elif self._active_ap is not None: + # revert to showing state of strongest AP again + self._active_ap = None + self.update_strength() + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveAccessPoint' in properties: + self.__update_active_ap(properties['ActiveAccessPoint']) + + def __get_active_ap_reply_cb(self, ap_path): + self.__update_active_ap(ap_path) + interface_props = dbus.Interface(self._device, dbus.PROPERTIES_IFACE) + interface_props.Get(network.NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_device_state_reply_cb, + error_handler=self.__get_device_state_error_cb) + + def __get_active_ap_error_cb(self, err): + logging.error('Error getting the active access point: %s', err) + + def __get_device_caps_reply_cb(self, caps): + self._device_caps = caps + + def __get_device_caps_error_cb(self, err): + logging.error('Error getting the wireless device properties: %s', err) + + def __get_device_state_reply_cb(self, state): + self._device_state = state + self._update_state() + self._update_color() + self._update_icon() + self._update_badge() + + def __get_device_state_error_cb(self, err): + logging.error('Error getting the device state: %s', err) + + def _update_icon(self): + if self._mode == network.NM_802_11_MODE_ADHOC and \ + network.is_sugar_adhoc_network(self._ssid): + channel = max([1] + [ap.channel for ap in + self._access_points.values()]) + if self._device_state == network.NM_DEVICE_STATE_ACTIVATED and \ + self._active_ap is not None: + icon_name = 'network-adhoc-%s-connected' % channel + else: + icon_name = 'network-adhoc-%s' % channel + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + else: + if self._device_state == network.NM_DEVICE_STATE_ACTIVATED and \ + self._active_ap is not None: + icon_name = '%s-connected' % _AP_ICON_NAME + else: + icon_name = _AP_ICON_NAME + + icon_name = get_icon_state(icon_name, self._strength) + if icon_name: + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + + def _update_badge(self): + if self._mode != network.NM_802_11_MODE_ADHOC: + if network.find_connection_by_ssid(self._ssid) is not None: + self.props.badge_name = 'emblem-favorite' + self._palette_icon.props.badge_name = 'emblem-favorite' + elif self._flags == network.NM_802_11_AP_FLAGS_PRIVACY: + self.props.badge_name = 'emblem-locked' + self._palette_icon.props.badge_name = 'emblem-locked' + else: + self.props.badge_name = None + self._palette_icon.props.badge_name = None + else: + self.props.badge_name = None + self._palette_icon.props.badge_name = None + + def _update_state(self): + if self._active_ap is not None: + state = self._device_state + else: + state = network.NM_DEVICE_STATE_UNKNOWN + + if state == network.NM_DEVICE_STATE_PREPARE or \ + state == network.NM_DEVICE_STATE_CONFIG or \ + state == network.NM_DEVICE_STATE_NEED_AUTH or \ + state == network.NM_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.NM_DEVICE_STATE_ACTIVATED: + network.set_connected() + if self._disconnect_item: + self._disconnect_item.show() + self._connect_item.hide() + self._palette.props.secondary_text = _('Connected') + self.props.pulsing = False + else: + if self._disconnect_item: + self._disconnect_item.hide() + self._connect_item.show() + self._palette.props.secondary_text = None + self.props.pulsing = False + + def _update_color(self): + self.props.base_color = self._color + if self._filtered: + self.props.pulsing = False + self.alpha = _FILTERED_ALPHA + else: + self.alpha = 1.0 + + def _disconnect_activate_cb(self, item): + ap_paths = self._access_points.keys() + network.disconnect_access_points(ap_paths) + + def _add_ciphers_from_flags(self, flags, pairwise): + ciphers = [] + if pairwise: + if flags & network.NM_802_11_AP_SEC_PAIR_TKIP: + ciphers.append('tkip') + if flags & network.NM_802_11_AP_SEC_PAIR_CCMP: + ciphers.append('ccmp') + else: + if flags & network.NM_802_11_AP_SEC_GROUP_WEP40: + ciphers.append('wep40') + if flags & network.NM_802_11_AP_SEC_GROUP_WEP104: + ciphers.append('wep104') + if flags & network.NM_802_11_AP_SEC_GROUP_TKIP: + ciphers.append('tkip') + if flags & network.NM_802_11_AP_SEC_GROUP_CCMP: + ciphers.append('ccmp') + return ciphers + + def _get_security(self): + if not (self._flags & network.NM_802_11_AP_FLAGS_PRIVACY) and \ + (self._wpa_flags == network.NM_802_11_AP_SEC_NONE) and \ + (self._rsn_flags == network.NM_802_11_AP_SEC_NONE): + # No security + return None + + if (self._flags & network.NM_802_11_AP_FLAGS_PRIVACY) and \ + (self._wpa_flags == network.NM_802_11_AP_SEC_NONE) and \ + (self._rsn_flags == network.NM_802_11_AP_SEC_NONE): + # Static WEP, Dynamic WEP, or LEAP + wireless_security = WirelessSecurity() + wireless_security.key_mgmt = 'none' + return wireless_security + + if (self._mode != network.NM_802_11_MODE_INFRA): + # Stuff after this point requires infrastructure + logging.error('The infrastructure mode is not supoorted' + ' by your wireless device.') + return None + + if (self._rsn_flags & network.NM_802_11_AP_SEC_KEY_MGMT_PSK) and \ + (self._device_caps & network.NM_WIFI_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_WIFI_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): + # Activate existing connection, if there is one + connection = network.find_connection_by_ssid(self._ssid) + if connection: + logging.debug('Activating existing connection for SSID %r', + self._ssid) + connection.activate(self._device) + return + + # Otherwise, create new connection and activate it + logging.debug('Creating new connection for SSID %r', self._ssid) + settings = Settings() + settings.connection.id = self._display_name + settings.connection.uuid = unique_id() + settings.connection.type = '802-11-wireless' + settings.wireless.ssid = self._ssid + + if self._mode == network.NM_802_11_MODE_INFRA: + settings.wireless.mode = 'infrastructure' + settings.connection.autoconnect = True + 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' + + network.add_and_activate_connection(self._device, settings, + self.get_first_ap().model) + + def set_filter(self, query): + self._filtered = self._display_name.lower().find(query) == -1 + self._update_icon() + self._update_color() + + def create_keydialog(self, response): + keydialog.create(self._ssid, self._flags, self._wpa_flags, + self._rsn_flags, self._device_caps, response) + + def update_strength(self): + if self._active_ap is not None: + # display strength of AP that we are connected to + new_strength = self._active_ap.strength + else: + # display the strength of the strongest AP that makes up this + # network, also considering that there may be no APs + new_strength = max([0] + [ap.strength for ap in + self._access_points.values()]) + + if new_strength != self._strength: + self._strength = new_strength + self._update_icon() + + def add_ap(self, ap): + self._access_points[ap.model.object_path] = ap + self.update_strength() + + def remove_ap(self, ap): + path = ap.model.object_path + if path not in self._access_points: + return + del self._access_points[path] + if self._active_ap == ap: + self._active_ap = None + self.update_strength() + + def num_aps(self): + return len(self._access_points) + + def find_ap(self, ap_path): + if ap_path not in self._access_points: + return None + return self._access_points[ap_path] + + def get_first_ap(self): + return self._access_points.values()[0] + + def is_olpc_mesh(self): + return self._mode == network.NM_802_11_MODE_ADHOC \ + and self._ssid == '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=network.NM_DEVICE_IFACE) + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=network.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._filtered = 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) + + text = _('Ad-hoc Network %d') % (self._channel, ) + palette_ = palette.Palette(glib.markup_escape_text(text), + 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.NM_DEVICE_STATE_UNKNOWN + + if state == network.NM_DEVICE_STATE_ACTIVATED: + icon_name = '%s-connected' % (self._ICON_NAME + str(self._channel)) + else: + icon_name = self._ICON_NAME + str(self._channel) + + if icon_name is not None: + self.props.icon_name = icon_name + icon = self._palette.props.icon + icon.props.icon_name = icon_name + + if (state >= network.NM_DEVICE_STATE_PREPARE) and \ + (state <= network.NM_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.NM_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 + self._update_color() + + def _update_color(self): + self.props.base_color = self._state_color + if self._filtered: + self.props.pulsing = False + self.alpha = _FILTERED_ALPHA + else: + self.alpha = 1.0 + + def __members_changed_cb(self, adhoc_manager, channel, has_members): + if channel == self._channel: + if has_members == True: + self._state_color = profile.get_color() + else: + color = '%s,%s' % (profile.get_color().get_stroke_color(), + style.COLOR_TRANSPARENT.get_svg()) + self._state_color = XoColor(color) + + if not self._filtered: + self.props.base_color = self._state_color + self._palette_icon.props.xo_color = self._state_color + self.alpha = 1.0 + else: + self.alpha = _FILTERED_ALPHA + + def set_filter(self, query): + name = self._NAME + str(self._channel) + self._filtered = 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._filtered = False + self._device_state = None + self._active = False + device = mesh_mgr.mesh_device + + self.connect('button-release-event', self.__button_release_event_cb) + + interface_props = dbus.Interface(device, dbus.PROPERTIES_IFACE) + interface_props.Get(network.NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_device_state_reply_cb, + error_handler=self.__get_device_state_error_cb) + interface_props.Get(network.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=network.NM_DEVICE_IFACE) + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=device.object_path, + dbus_interface=network.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): + text = _('Mesh Network %d') % (self._channel, ) + _palette = palette.Palette(glib.markup_escape_text(text)) + + 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() + self._update_color() + + 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.NM_DEVICE_STATE_UNKNOWN + + if state in [network.NM_DEVICE_STATE_PREPARE, + network.NM_DEVICE_STATE_CONFIG, + network.NM_DEVICE_STATE_NEED_AUTH, + network.NM_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.NM_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): + self.props.base_color = profile.get_color() + if self._filtered: + self.alpha = _FILTERED_ALPHA + else: + self.alpha = 1.0 + + 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 set_filter(self, query): + self._filtered = (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=network.NM_DEVICE_IFACE) + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=device_object_path, + dbus_interface=network.NM_OLPC_MESH_IFACE) diff --git a/src/jarabe/desktop/schoolserver.py b/src/jarabe/desktop/schoolserver.py new file mode 100644 index 0000000..403897b --- /dev/null +++ b/src/jarabe/desktop/schoolserver.py @@ -0,0 +1,173 @@ +# 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 _ +import xmlrpclib +import socket +import httplib +import os +from string import ascii_uppercase +import random +import time +import uuid +import sys + +import gconf + +from sugar import env +from sugar.profile import get_profile + +_REGISTER_URL = 'http://schoolserver:8080/' +_REGISTER_TIMEOUT = 8 +_OFW_TREE = '/ofw' +_PROC_TREE = '/proc/device-tree' +_MFG_SN = 'mfg-data/SN' +_MFG_UUID = 'mfg-data/U#' + + +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 + + +class _TimeoutHTTP(httplib.HTTP): + + def __init__(self, host='', port=None, strict=None, timeout=None): + if port == 0: + port = None + # FIXME: Depending on undocumented internals that can break between + # Python releases. Please have a look at SL #2350 + self._setup(self._connection_class(host, + port, strict, timeout=_REGISTER_TIMEOUT)) + + +class _TimeoutTransport(xmlrpclib.Transport): + + def make_connection(self, host): + host, extra_headers, x509_ = self.get_host_info(host) + return _TimeoutHTTP(host, timeout=_REGISTER_TIMEOUT) + + +def register_laptop(url=_REGISTER_URL): + + profile = get_profile() + client = gconf.client_get_default() + + if _have_ofw_tree(): + sn = _read_mfg_data(os.path.join(_OFW_TREE, _MFG_SN)) + uuid_ = _read_mfg_data(os.path.join(_OFW_TREE, _MFG_UUID)) + elif _have_proc_device_tree(): + sn = _read_mfg_data(os.path.join(_PROC_TREE, _MFG_SN)) + uuid_ = _read_mfg_data(os.path.join(_PROC_TREE, _MFG_UUID)) + else: + sn = _generate_serial_number() + uuid_ = str(uuid.uuid1()) + sn = sn or 'SHF00000000' + uuid_ = uuid_ or '00000000-0000-0000-0000-000000000000' + + setting_name = '/desktop/sugar/collaboration/jabber_server' + jabber_server = client.get_string(setting_name) + _store_identifiers(sn, uuid_, jabber_server) + + if jabber_server: + url = 'http://' + jabber_server + ':8080/' + + nick = client.get_string('/desktop/sugar/user/nick') + + if sys.hexversion < 0x2070000: + server = xmlrpclib.ServerProxy(url, _TimeoutTransport()) + else: + socket.setdefaulttimeout(_REGISTER_TIMEOUT) + server = xmlrpclib.ServerProxy(url) + try: + data = server.register(sn, nick, uuid_, profile.pubkey) + except (xmlrpclib.Error, TypeError, socket.error): + logging.exception('Registration: cannot connect to server') + raise RegisterError(_('Cannot connect to the server.')) + finally: + socket.setdefaulttimeout(None) + + 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_TREE) + + +def _have_proc_device_tree(): + return os.path.exists(_PROC_TREE) + + +def _read_mfg_data(path): + if not os.path.exists(path): + return None + fh = open(path, 'r') + data = fh.read().rstrip('\0\n') + fh.close() + return data diff --git a/src/jarabe/desktop/snowflakelayout.py b/src/jarabe/desktop/snowflakelayout.py new file mode 100644 index 0000000..e4963ba --- /dev/null +++ b/src/jarabe/desktop/snowflakelayout.py @@ -0,0 +1,111 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import math + +import gobject +import hippo + +from sugar.graphics import style + + +_BASE_DISTANCE = style.zoom(25) +_CHILDREN_FACTOR = style.zoom(3) + + +class SnowflakeLayout(gobject.GObject, hippo.CanvasLayout): + __gtype_name__ = 'SugarSnowflakeLayout' + + def __init__(self): + gobject.GObject.__init__(self) + self._nflakes = 0 + self._box = None + + def add(self, child, center=False): + if not center: + self._nflakes += 1 + + self._box.append(child) + + box_child = self._box.find_box_child(child) + box_child.is_center = center + + def remove(self, child): + box_child = self._box.find_box_child(child) + if not box_child.is_center: + self._nflakes -= 1 + + self._box.remove(child) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + size = self._calculate_size() + return (size, size) + + def do_get_width_request(self): + size = self._calculate_size() + return (size, size) + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + r = self._get_radius() + index = 0 + + for child in self._box.get_layout_children(): + min_width, child_width = child.get_width_request() + min_height, child_height = child.get_height_request(child_width) + + if child.is_center: + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + child_width, child_height, origin_changed) + else: + angle = 2 * math.pi * index / self._nflakes + + if self._nflakes != 2: + angle -= math.pi / 2 + + dx = math.cos(angle) * r + dy = math.sin(angle) * r + + child_x = int(x + (width - child_width) / 2 + dx) + child_y = int(y + (height - child_height) / 2 + dy) + + child.allocate(child_x, child_y, child_width, + child_height, origin_changed) + + index += 1 + + def _get_radius(self): + radius = int(_BASE_DISTANCE + _CHILDREN_FACTOR * self._nflakes) + for child in self._box.get_layout_children(): + if child.is_center: + [min_w, child_w] = child.get_width_request() + [min_h, child_h] = child.get_height_request(child_w) + radius += max(child_w, child_h) / 2 + + return radius + + def _calculate_size(self): + thickness = 0 + for child in self._box.get_layout_children(): + [min_width, child_width] = child.get_width_request() + [min_height, child_height] = child.get_height_request(child_width) + thickness = max(thickness, max(child_width, child_height)) + + return self._get_radius() * 2 + thickness diff --git a/src/jarabe/desktop/spreadlayout.py b/src/jarabe/desktop/spreadlayout.py new file mode 100644 index 0000000..b5c623e --- /dev/null +++ b/src/jarabe/desktop/spreadlayout.py @@ -0,0 +1,89 @@ +# 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 math + +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) + width = math.ceil(width / _CELL_SIZE) + height = math.ceil(height / _CELL_SIZE) + + return int(width), int(height) + + def _grid_child_changed_cb(self, grid, child): + child.emit_request_changed() diff --git a/src/jarabe/desktop/transitionbox.py b/src/jarabe/desktop/transitionbox.py new file mode 100644 index 0000000..fd2112c --- /dev/null +++ b/src/jarabe/desktop/transitionbox.py @@ -0,0 +1,99 @@ +# 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 = int(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() |