From b6db5b8af0af7ebfdd44cb53b09d63b819989338 Mon Sep 17 00:00:00 2001 From: Julio Reyes Date: Fri, 05 Jul 2013 14:13:11 +0000 Subject: Initial Commit --- (limited to 'src') diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..83571a4 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = jarabe diff --git a/src/Makefile.in b/src/Makefile.in new file mode 100644 index 0000000..b9e24ba --- /dev/null +++ b/src/Makefile.in @@ -0,0 +1,569 @@ +# 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 +DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in +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 = +RECURSIVE_TARGETS = all-recursive check-recursive dvi-recursive \ + html-recursive info-recursive install-data-recursive \ + install-dvi-recursive install-exec-recursive \ + install-html-recursive install-info-recursive \ + install-pdf-recursive install-ps-recursive install-recursive \ + installcheck-recursive installdirs-recursive pdf-recursive \ + ps-recursive uninstall-recursive +RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \ + distclean-recursive maintainer-clean-recursive +AM_RECURSIVE_TARGETS = $(RECURSIVE_TARGETS:-recursive=) \ + $(RECURSIVE_CLEAN_TARGETS:-recursive=) tags TAGS ctags CTAGS \ + distdir +ETAGS = etags +CTAGS = ctags +DIST_SUBDIRS = $(SUBDIRS) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +am__relativize = \ + dir0=`pwd`; \ + sed_first='s,^\([^/]*\)/.*$$,\1,'; \ + sed_rest='s,^[^/]*/*,,'; \ + sed_last='s,^.*/\([^/]*\)$$,\1,'; \ + sed_butlast='s,/*[^/]*$$,,'; \ + while test -n "$$dir1"; do \ + first=`echo "$$dir1" | sed -e "$$sed_first"`; \ + if test "$$first" != "."; then \ + if test "$$first" = ".."; then \ + dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \ + dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \ + else \ + first2=`echo "$$dir2" | sed -e "$$sed_first"`; \ + if test "$$first2" = "$$first"; then \ + dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \ + else \ + dir2="../$$dir2"; \ + fi; \ + dir0="$$dir0"/"$$first"; \ + fi; \ + fi; \ + dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \ + done; \ + reldir="$$dir2" +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@ +SUBDIRS = jarabe +all: all-recursive + +.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/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/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): + +# This directory's subdirectories are mostly independent; you can cd +# into them and run `make' without going through this Makefile. +# To change the values of `make' variables: instead of editing Makefiles, +# (1) if the variable is set in `config.status', edit `config.status' +# (which will cause the Makefiles to be regenerated when you run `make'); +# (2) otherwise, pass the desired values on the `make' command line. +$(RECURSIVE_TARGETS): + @fail= failcom='exit 1'; \ + for f in x $$MAKEFLAGS; do \ + case $$f in \ + *=* | --[!k]*);; \ + *k*) failcom='fail=yes';; \ + esac; \ + done; \ + dot_seen=no; \ + target=`echo $@ | sed s/-recursive//`; \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + echo "Making $$target in $$subdir"; \ + if test "$$subdir" = "."; then \ + dot_seen=yes; \ + local_target="$$target-am"; \ + else \ + local_target="$$target"; \ + fi; \ + ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ + || eval $$failcom; \ + done; \ + if test "$$dot_seen" = "no"; then \ + $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \ + fi; test -z "$$fail" + +$(RECURSIVE_CLEAN_TARGETS): + @fail= failcom='exit 1'; \ + for f in x $$MAKEFLAGS; do \ + case $$f in \ + *=* | --[!k]*);; \ + *k*) failcom='fail=yes';; \ + esac; \ + done; \ + dot_seen=no; \ + case "$@" in \ + distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \ + *) list='$(SUBDIRS)' ;; \ + esac; \ + rev=''; for subdir in $$list; do \ + if test "$$subdir" = "."; then :; else \ + rev="$$subdir $$rev"; \ + fi; \ + done; \ + rev="$$rev ."; \ + target=`echo $@ | sed s/-recursive//`; \ + for subdir in $$rev; do \ + echo "Making $$target in $$subdir"; \ + if test "$$subdir" = "."; then \ + local_target="$$target-am"; \ + else \ + local_target="$$target"; \ + fi; \ + ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ + || eval $$failcom; \ + done && test -z "$$fail" +tags-recursive: + list='$(SUBDIRS)'; for subdir in $$list; do \ + test "$$subdir" = . || ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) tags); \ + done +ctags-recursive: + list='$(SUBDIRS)'; for subdir in $$list; do \ + test "$$subdir" = . || ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) ctags); \ + done + +ID: $(HEADERS) $(SOURCES) $(LISP) $(TAGS_FILES) + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + mkid -fID $$unique +tags: TAGS + +TAGS: tags-recursive $(HEADERS) $(SOURCES) $(TAGS_DEPENDENCIES) \ + $(TAGS_FILES) $(LISP) + set x; \ + here=`pwd`; \ + if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \ + include_option=--etags-include; \ + empty_fix=.; \ + else \ + include_option=--include; \ + empty_fix=; \ + fi; \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + test ! -f $$subdir/TAGS || \ + set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \ + fi; \ + done; \ + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: CTAGS +CTAGS: ctags-recursive $(HEADERS) $(SOURCES) $(TAGS_DEPENDENCIES) \ + $(TAGS_FILES) $(LISP) + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +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 + @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + test -d "$(distdir)/$$subdir" \ + || $(MKDIR_P) "$(distdir)/$$subdir" \ + || exit 1; \ + fi; \ + done + @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + dir1=$$subdir; dir2="$(distdir)/$$subdir"; \ + $(am__relativize); \ + new_distdir=$$reldir; \ + dir1=$$subdir; dir2="$(top_distdir)"; \ + $(am__relativize); \ + new_top_distdir=$$reldir; \ + echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \ + echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \ + ($(am__cd) $$subdir && \ + $(MAKE) $(AM_MAKEFLAGS) \ + top_distdir="$$new_top_distdir" \ + distdir="$$new_distdir" \ + am__remove_distdir=: \ + am__skip_length_check=: \ + am__skip_mode_fix=: \ + distdir) \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-recursive +all-am: Makefile +installdirs: installdirs-recursive +installdirs-am: +install: install-recursive +install-exec: install-exec-recursive +install-data: install-data-recursive +uninstall: uninstall-recursive + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-recursive +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-recursive + +clean-am: clean-generic mostlyclean-am + +distclean: distclean-recursive + -rm -f Makefile +distclean-am: clean-am distclean-generic distclean-tags + +dvi: dvi-recursive + +dvi-am: + +html: html-recursive + +html-am: + +info: info-recursive + +info-am: + +install-data-am: + +install-dvi: install-dvi-recursive + +install-dvi-am: + +install-exec-am: + +install-html: install-html-recursive + +install-html-am: + +install-info: install-info-recursive + +install-info-am: + +install-man: + +install-pdf: install-pdf-recursive + +install-pdf-am: + +install-ps: install-ps-recursive + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-recursive + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-recursive + +mostlyclean-am: mostlyclean-generic + +pdf: pdf-recursive + +pdf-am: + +ps: ps-recursive + +ps-am: + +uninstall-am: + +.MAKE: $(RECURSIVE_CLEAN_TARGETS) $(RECURSIVE_TARGETS) ctags-recursive \ + install-am install-strip tags-recursive + +.PHONY: $(RECURSIVE_CLEAN_TARGETS) $(RECURSIVE_TARGETS) CTAGS GTAGS \ + all all-am check check-am clean clean-generic ctags \ + ctags-recursive distclean distclean-generic distclean-tags \ + 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 installcheck installcheck-am installdirs \ + installdirs-am maintainer-clean maintainer-clean-generic \ + mostlyclean mostlyclean-generic pdf pdf-am ps ps-am tags \ + tags-recursive uninstall uninstall-am + + +# 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/Makefile.am b/src/jarabe/Makefile.am new file mode 100644 index 0000000..84bb213 --- /dev/null +++ b/src/jarabe/Makefile.am @@ -0,0 +1,16 @@ +SUBDIRS = \ + controlpanel \ + desktop \ + frame \ + journal \ + model \ + view \ + intro \ + util + +sugardir = $(pythondir)/jarabe +sugar_PYTHON = \ + __init__.py + +nodist_sugar_PYTHON = config.py + diff --git a/src/jarabe/Makefile.in b/src/jarabe/Makefile.in new file mode 100644 index 0000000..9d1c674 --- /dev/null +++ b/src/jarabe/Makefile.in @@ -0,0 +1,691 @@ +# 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 +DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in \ + $(srcdir)/config.py.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.py +CONFIG_CLEAN_VPATH_FILES = +SOURCES = +DIST_SOURCES = +RECURSIVE_TARGETS = all-recursive check-recursive dvi-recursive \ + html-recursive info-recursive install-data-recursive \ + install-dvi-recursive install-exec-recursive \ + install-html-recursive install-info-recursive \ + install-pdf-recursive install-ps-recursive install-recursive \ + installcheck-recursive installdirs-recursive pdf-recursive \ + ps-recursive uninstall-recursive +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)" "$(DESTDIR)$(sugardir)" +py_compile = $(top_srcdir)/py-compile +RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \ + distclean-recursive maintainer-clean-recursive +AM_RECURSIVE_TARGETS = $(RECURSIVE_TARGETS:-recursive=) \ + $(RECURSIVE_CLEAN_TARGETS:-recursive=) tags TAGS ctags CTAGS \ + distdir +ETAGS = etags +CTAGS = ctags +DIST_SUBDIRS = $(SUBDIRS) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +am__relativize = \ + dir0=`pwd`; \ + sed_first='s,^\([^/]*\)/.*$$,\1,'; \ + sed_rest='s,^[^/]*/*,,'; \ + sed_last='s,^.*/\([^/]*\)$$,\1,'; \ + sed_butlast='s,/*[^/]*$$,,'; \ + while test -n "$$dir1"; do \ + first=`echo "$$dir1" | sed -e "$$sed_first"`; \ + if test "$$first" != "."; then \ + if test "$$first" = ".."; then \ + dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \ + dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \ + else \ + first2=`echo "$$dir2" | sed -e "$$sed_first"`; \ + if test "$$first2" = "$$first"; then \ + dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \ + else \ + dir2="../$$dir2"; \ + fi; \ + dir0="$$dir0"/"$$first"; \ + fi; \ + fi; \ + dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \ + done; \ + reldir="$$dir2" +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@ +SUBDIRS = \ + controlpanel \ + desktop \ + frame \ + journal \ + model \ + view \ + intro \ + util + +sugardir = $(pythondir)/jarabe +sugar_PYTHON = \ + __init__.py + +nodist_sugar_PYTHON = config.py +all: all-recursive + +.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/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/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): +config.py: $(top_builddir)/config.status $(srcdir)/config.py.in + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ +install-nodist_sugarPYTHON: $(nodist_sugar_PYTHON) + @$(NORMAL_INSTALL) + test -z "$(sugardir)" || $(MKDIR_P) "$(DESTDIR)$(sugardir)" + @list='$(nodist_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-nodist_sugarPYTHON: + @$(NORMAL_UNINSTALL) + @list='$(nodist_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 +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 + +# This directory's subdirectories are mostly independent; you can cd +# into them and run `make' without going through this Makefile. +# To change the values of `make' variables: instead of editing Makefiles, +# (1) if the variable is set in `config.status', edit `config.status' +# (which will cause the Makefiles to be regenerated when you run `make'); +# (2) otherwise, pass the desired values on the `make' command line. +$(RECURSIVE_TARGETS): + @fail= failcom='exit 1'; \ + for f in x $$MAKEFLAGS; do \ + case $$f in \ + *=* | --[!k]*);; \ + *k*) failcom='fail=yes';; \ + esac; \ + done; \ + dot_seen=no; \ + target=`echo $@ | sed s/-recursive//`; \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + echo "Making $$target in $$subdir"; \ + if test "$$subdir" = "."; then \ + dot_seen=yes; \ + local_target="$$target-am"; \ + else \ + local_target="$$target"; \ + fi; \ + ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ + || eval $$failcom; \ + done; \ + if test "$$dot_seen" = "no"; then \ + $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \ + fi; test -z "$$fail" + +$(RECURSIVE_CLEAN_TARGETS): + @fail= failcom='exit 1'; \ + for f in x $$MAKEFLAGS; do \ + case $$f in \ + *=* | --[!k]*);; \ + *k*) failcom='fail=yes';; \ + esac; \ + done; \ + dot_seen=no; \ + case "$@" in \ + distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \ + *) list='$(SUBDIRS)' ;; \ + esac; \ + rev=''; for subdir in $$list; do \ + if test "$$subdir" = "."; then :; else \ + rev="$$subdir $$rev"; \ + fi; \ + done; \ + rev="$$rev ."; \ + target=`echo $@ | sed s/-recursive//`; \ + for subdir in $$rev; do \ + echo "Making $$target in $$subdir"; \ + if test "$$subdir" = "."; then \ + local_target="$$target-am"; \ + else \ + local_target="$$target"; \ + fi; \ + ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ + || eval $$failcom; \ + done && test -z "$$fail" +tags-recursive: + list='$(SUBDIRS)'; for subdir in $$list; do \ + test "$$subdir" = . || ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) tags); \ + done +ctags-recursive: + list='$(SUBDIRS)'; for subdir in $$list; do \ + test "$$subdir" = . || ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) ctags); \ + done + +ID: $(HEADERS) $(SOURCES) $(LISP) $(TAGS_FILES) + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + mkid -fID $$unique +tags: TAGS + +TAGS: tags-recursive $(HEADERS) $(SOURCES) $(TAGS_DEPENDENCIES) \ + $(TAGS_FILES) $(LISP) + set x; \ + here=`pwd`; \ + if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \ + include_option=--etags-include; \ + empty_fix=.; \ + else \ + include_option=--include; \ + empty_fix=; \ + fi; \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + test ! -f $$subdir/TAGS || \ + set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \ + fi; \ + done; \ + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: CTAGS +CTAGS: ctags-recursive $(HEADERS) $(SOURCES) $(TAGS_DEPENDENCIES) \ + $(TAGS_FILES) $(LISP) + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +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 + @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + test -d "$(distdir)/$$subdir" \ + || $(MKDIR_P) "$(distdir)/$$subdir" \ + || exit 1; \ + fi; \ + done + @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + dir1=$$subdir; dir2="$(distdir)/$$subdir"; \ + $(am__relativize); \ + new_distdir=$$reldir; \ + dir1=$$subdir; dir2="$(top_distdir)"; \ + $(am__relativize); \ + new_top_distdir=$$reldir; \ + echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \ + echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \ + ($(am__cd) $$subdir && \ + $(MAKE) $(AM_MAKEFLAGS) \ + top_distdir="$$new_top_distdir" \ + distdir="$$new_distdir" \ + am__remove_distdir=: \ + am__skip_length_check=: \ + am__skip_mode_fix=: \ + distdir) \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-recursive +all-am: Makefile +installdirs: installdirs-recursive +installdirs-am: + for dir in "$(DESTDIR)$(sugardir)" "$(DESTDIR)$(sugardir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-recursive +install-exec: install-exec-recursive +install-data: install-data-recursive +uninstall: uninstall-recursive + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-recursive +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-recursive + +clean-am: clean-generic mostlyclean-am + +distclean: distclean-recursive + -rm -f Makefile +distclean-am: clean-am distclean-generic distclean-tags + +dvi: dvi-recursive + +dvi-am: + +html: html-recursive + +html-am: + +info: info-recursive + +info-am: + +install-data-am: install-nodist_sugarPYTHON install-sugarPYTHON + +install-dvi: install-dvi-recursive + +install-dvi-am: + +install-exec-am: + +install-html: install-html-recursive + +install-html-am: + +install-info: install-info-recursive + +install-info-am: + +install-man: + +install-pdf: install-pdf-recursive + +install-pdf-am: + +install-ps: install-ps-recursive + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-recursive + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-recursive + +mostlyclean-am: mostlyclean-generic + +pdf: pdf-recursive + +pdf-am: + +ps: ps-recursive + +ps-am: + +uninstall-am: uninstall-nodist_sugarPYTHON uninstall-sugarPYTHON + +.MAKE: $(RECURSIVE_CLEAN_TARGETS) $(RECURSIVE_TARGETS) ctags-recursive \ + install-am install-strip tags-recursive + +.PHONY: $(RECURSIVE_CLEAN_TARGETS) $(RECURSIVE_TARGETS) CTAGS GTAGS \ + all all-am check check-am clean clean-generic ctags \ + ctags-recursive distclean distclean-generic distclean-tags \ + 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-nodist_sugarPYTHON install-pdf install-pdf-am \ + install-ps install-ps-am install-strip install-sugarPYTHON \ + installcheck installcheck-am installdirs installdirs-am \ + maintainer-clean maintainer-clean-generic mostlyclean \ + mostlyclean-generic pdf pdf-am ps ps-am tags tags-recursive \ + uninstall uninstall-am uninstall-nodist_sugarPYTHON \ + 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/__init__.py b/src/jarabe/__init__.py new file mode 100644 index 0000000..ed2f639 --- /dev/null +++ b/src/jarabe/__init__.py @@ -0,0 +1,25 @@ +"""OLPC Sugar Graphical "Shell" Interface + +Provides the shell-level operations for managing +the OLPC laptop computers. It interacts heavily +with (and depends upon) the Sugar UI libraries. + +This is a "graphical" shell, the name does not +refer to a command-line "shell" interface. +""" + +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/jarabe/config.py.in b/src/jarabe/config.py.in new file mode 100644 index 0000000..d22ee9a --- /dev/null +++ b/src/jarabe/config.py.in @@ -0,0 +1,26 @@ +# Copyright (C) 2008 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# pylint: disable=C0301 + +prefix = '@prefix@' +data_path = '@prefix@/share/sugar/data' +shell_path = '@prefix@/share/sugar/shell' +locale_path = '@prefix@/share/locale' +ext_path = '@prefix@/share/sugar/extensions' +activities_path = "@prefix@/share/sugar/activities" +version = '@SUCROSE_VERSION@' + diff --git a/src/jarabe/controlpanel/Makefile.am b/src/jarabe/controlpanel/Makefile.am new file mode 100644 index 0000000..1de2961 --- /dev/null +++ b/src/jarabe/controlpanel/Makefile.am @@ -0,0 +1,10 @@ +sugardir = $(pythondir)/jarabe/controlpanel +sugar_PYTHON = \ + __init__.py \ + cmd.py \ + gui.py \ + inlinealert.py \ + sectionview.py \ + toolbar.py + + diff --git a/src/jarabe/controlpanel/Makefile.in b/src/jarabe/controlpanel/Makefile.in new file mode 100644 index 0000000..11a0798 --- /dev/null +++ b/src/jarabe/controlpanel/Makefile.in @@ -0,0 +1,445 @@ +# 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/controlpanel +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/controlpanel +sugar_PYTHON = \ + __init__.py \ + cmd.py \ + gui.py \ + inlinealert.py \ + sectionview.py \ + toolbar.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/controlpanel/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/controlpanel/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/controlpanel/__init__.py b/src/jarabe/controlpanel/__init__.py new file mode 100644 index 0000000..85f6a24 --- /dev/null +++ b/src/jarabe/controlpanel/__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/controlpanel/cmd.py b/src/jarabe/controlpanel/cmd.py new file mode 100644 index 0000000..c4870c9 --- /dev/null +++ b/src/jarabe/controlpanel/cmd.py @@ -0,0 +1,161 @@ +# Copyright (C) 2007, 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import sys +import getopt +import os +from gettext import gettext as _ +import logging + +from jarabe import config + + +_RESTART = 1 + +_same_option_warning = _('sugar-control-panel: WARNING, found more than one' + ' option with the same name: %s module: %r') +_no_option_error = _('sugar-control-panel: key=%s not an available option') +_general_error = _('sugar-control-panel: %s') + + +def cmd_help(): + """Print the help to the screen""" + # TRANS: Translators, there's a empty line at the end of this string, + # which must appear in the translated string (msgstr) as well. + print _('Usage: sugar-control-panel [ option ] key [ args ... ] \n\ + Control for the sugar environment. \n\ + Options: \n\ + -h show this help message and exit \n\ + -l list all the available options \n\ + -h key show information about this key \n\ + -g key get the current value of the key \n\ + -s key set the current value for the key \n\ + -c key clear the current value for the key \n\ + ') + + +def note_restart(): + """Instructions how to restart sugar""" + print _('To apply your changes you have to restart Sugar.\n' + + 'Hit ctrl+alt+erase on the keyboard to trigger a restart.') + + +def load_modules(): + """Build a list of pointers to available modules and import them. + """ + modules = [] + + path = os.path.join(config.ext_path, 'cpsection') + folder = os.listdir(path) + + for item in folder: + if os.path.isdir(os.path.join(path, item)) and \ + os.path.exists(os.path.join(path, item, 'model.py')): + try: + module = __import__('.'.join(('cpsection', item, 'model')), + globals(), locals(), ['model']) + except Exception: + logging.exception('Exception while loading extension:') + else: + modules.append(module) + + return modules + + +def main(): + try: + options, args = getopt.getopt(sys.argv[1:], 'h:s:g:c:l', []) + except getopt.GetoptError: + cmd_help() + sys.exit(2) + + if not options: + cmd_help() + sys.exit(2) + + modules = load_modules() + + for option, key in options: + found = 0 + if option in ('-h'): + for module in modules: + method = getattr(module, 'set_' + key, None) + if method: + found += 1 + if found == 1: + print method.__doc__ + else: + print _(_same_option_warning % (key, module)) + if found == 0: + print _(_no_option_error % key) + if option in ('-l'): + for module in modules: + methods = dir(module) + print '%s:' % module.__name__.split('.')[1] + for method in methods: + if method.startswith('get_'): + print ' %s' % method[4:] + elif method.startswith('clear_'): + print ' %s (use the -c argument with this option)' \ + % method[6:] + if option in ('-g'): + for module in modules: + method = getattr(module, 'print_' + key, None) + if method: + found += 1 + if found == 1: + try: + method() + except Exception, detail: + print _(_general_error % detail) + else: + print _(_same_option_warning % (key, module)) + if found == 0: + print _(_no_option_error % key) + if option in ('-s'): + for module in modules: + method = getattr(module, 'set_' + key, None) + if method: + note = 0 + found += 1 + if found == 1: + try: + note = method(*args) + except Exception, detail: + print _(_general_error % detail) + if note == _RESTART: + note_restart() + else: + print _(_same_option_warning % (key, module)) + if found == 0: + print _(_no_option_error % key) + if option in ('-c'): + for module in modules: + method = getattr(module, 'clear_' + key, None) + if method: + note = 0 + found += 1 + if found == 1: + try: + note = method(*args) + except Exception, detail: + print _(_general_error % detail) + if note == _RESTART: + note_restart() + else: + print _(_same_option_warning % (key, module)) + if found == 0: + print _(_no_option_error % key) diff --git a/src/jarabe/controlpanel/gui.py b/src/jarabe/controlpanel/gui.py new file mode 100644 index 0000000..46810aa --- /dev/null +++ b/src/jarabe/controlpanel/gui.py @@ -0,0 +1,445 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +from gettext import gettext as _ + +import gobject +import gtk + +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar.graphics.alert import Alert + +from jarabe.model.session import get_session_manager +from jarabe.controlpanel.toolbar import MainToolbar +from jarabe.controlpanel.toolbar import SectionToolbar +from jarabe import config + +POWERD_FLAG_DIR = '/etc/powerd/flags' + +_logger = logging.getLogger('ControlPanel') + + +class ControlPanel(gtk.Window): + __gtype_name__ = 'SugarControlPanel' + + def __init__(self): + gtk.Window.__init__(self) + + self._max_columns = int(0.285 * (float(gtk.gdk.screen_width()) / + style.GRID_CELL_SIZE - 3)) + + self.set_border_width(style.LINE_WIDTH) + offset = style.GRID_CELL_SIZE + width = gtk.gdk.screen_width() - offset * 2 + height = gtk.gdk.screen_height() - offset * 2 + self.set_size_request(width, height) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_decorated(False) + self.set_resizable(False) + self.set_modal(True) + + self._toolbar = None + self._canvas = None + self._table = None + self._scrolledwindow = None + self._separator = None + self._section_view = None + self._section_toolbar = None + self._main_toolbar = None + + self._vbox = gtk.VBox() + self._hbox = gtk.HBox() + self._vbox.pack_start(self._hbox) + self._hbox.show() + + self._main_view = gtk.EventBox() + self._hbox.pack_start(self._main_view) + self._main_view.modify_bg(gtk.STATE_NORMAL, + style.COLOR_BLACK.get_gdk_color()) + self._main_view.show() + + self.add(self._vbox) + self._vbox.show() + + self.connect('realize', self.__realize_cb) + + self._options = self._get_options() + self._current_option = None + self._setup_main() + self._setup_section() + self._show_main_view() + + def __realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + + def _set_canvas(self, canvas): + if self._canvas: + self._main_view.remove(self._canvas) + if canvas: + self._main_view.add(canvas) + self._canvas = canvas + + def _set_toolbar(self, toolbar): + if self._toolbar: + self._vbox.remove(self._toolbar) + self._vbox.pack_start(toolbar, False) + self._vbox.reorder_child(toolbar, 0) + self._toolbar = toolbar + if not self._separator: + self._separator = gtk.HSeparator() + self._vbox.pack_start(self._separator, False) + self._vbox.reorder_child(self._separator, 1) + self._separator.show() + + def _setup_main(self): + self._main_toolbar = MainToolbar() + + self._table = gtk.Table() + self._table.set_col_spacings(style.GRID_CELL_SIZE) + self._table.set_row_spacings(style.GRID_CELL_SIZE) + self._table.set_border_width(style.GRID_CELL_SIZE) + + self._scrolledwindow = gtk.ScrolledWindow() + self._scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, + gtk.POLICY_AUTOMATIC) + self._scrolledwindow.add_with_viewport(self._table) + child = self._scrolledwindow.get_child() + child.modify_bg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color()) + + self._setup_options() + self._main_toolbar.connect('stop-clicked', + self.__stop_clicked_cb) + self._main_toolbar.connect('search-changed', + self.__search_changed_cb) + + def _setup_options(self): + if not os.access(POWERD_FLAG_DIR, os.W_OK): + del self._options['power'] + + try: + import xklavier + except ImportError: + del self._options['keyboard'] + + # If the screen width only supports two columns, start + # placing from the second row. + if self._max_columns == 2: + row = 1 + column = 0 + else: + # About Me and About my computer are hardcoded below to use the + # first two slots so we need to leave them free. + row = 0 + column = 2 + + options = self._options.keys() + options.sort() + + for option in options: + sectionicon = _SectionIcon(icon_name=self._options[option]['icon'], + title=self._options[option]['title'], + xo_color=self._options[option]['color'], + pixel_size=style.GRID_CELL_SIZE) + sectionicon.connect('button_press_event', + self.__select_option_cb, option) + sectionicon.show() + + if option == 'aboutme': + self._table.attach(sectionicon, 0, 1, 0, 1) + elif option == 'aboutcomputer': + self._table.attach(sectionicon, 1, 2, 0, 1) + else: + self._table.attach(sectionicon, + column, column + 1, + row, row + 1) + column += 1 + if column == self._max_columns: + column = 0 + row += 1 + + self._options[option]['button'] = sectionicon + + def _show_main_view(self): + self._set_toolbar(self._main_toolbar) + self._main_toolbar.show() + self._set_canvas(self._scrolledwindow) + self._main_view.modify_bg(gtk.STATE_NORMAL, + style.COLOR_BLACK.get_gdk_color()) + self._table.show() + self._scrolledwindow.show() + entry = self._main_toolbar.get_entry() + entry.grab_focus() + entry.set_text('') + + def _update(self, query): + for option in self._options: + found = False + for key in self._options[option]['keywords']: + if query.lower() in key.lower(): + self._options[option]['button'].set_sensitive(True) + found = True + break + if not found: + self._options[option]['button'].set_sensitive(False) + + def _setup_section(self): + self._section_toolbar = SectionToolbar() + self._section_toolbar.connect('cancel-clicked', + self.__cancel_clicked_cb) + self._section_toolbar.connect('accept-clicked', + self.__accept_clicked_cb) + + def show_section_view(self, option): + self._set_toolbar(self._section_toolbar) + + icon = self._section_toolbar.get_icon() + icon.set_from_icon_name(self._options[option]['icon'], + gtk.ICON_SIZE_LARGE_TOOLBAR) + icon.props.xo_color = self._options[option]['color'] + title = self._section_toolbar.get_title() + title.set_text(self._options[option]['title']) + self._section_toolbar.show() + + self._current_option = option + + mod = __import__('.'.join(('cpsection', option, 'view')), + globals(), locals(), ['view']) + view_class = getattr(mod, self._options[option]['view'], None) + + mod = __import__('.'.join(('cpsection', option, 'model')), + globals(), locals(), ['model']) + model = ModelWrapper(mod) + + try: + self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + self._section_view = view_class(model, + self._options[option]['alerts']) + + self._set_canvas(self._section_view) + self._section_view.show() + finally: + self.get_window().set_cursor(None) + + self._section_view.connect('notify::is-valid', + self.__valid_section_cb) + self._section_view.connect('request-close', + self.__close_request_cb) + self._main_view.modify_bg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + + def set_section_view_auto_close(self): + """Automatically close the control panel if there is "nothing to do" + """ + self._section_view.auto_close = True + + def _get_options(self): + """Get the available option information from the extensions + """ + options = {} + + path = os.path.join(config.ext_path, 'cpsection') + folder = os.listdir(path) + + for item in folder: + if os.path.isdir(os.path.join(path, item)) and \ + os.path.exists(os.path.join(path, item, '__init__.py')): + try: + mod = __import__('.'.join(('cpsection', item)), + globals(), locals(), [item]) + view_class = getattr(mod, 'CLASS', None) + if view_class is not None: + options[item] = {} + options[item]['alerts'] = [] + options[item]['view'] = view_class + options[item]['icon'] = getattr(mod, 'ICON', item) + options[item]['title'] = getattr(mod, 'TITLE', item) + options[item]['color'] = getattr(mod, 'COLOR', None) + keywords = getattr(mod, 'KEYWORDS', []) + keywords.append(options[item]['title'].lower()) + if item not in keywords: + keywords.append(item) + options[item]['keywords'] = keywords + else: + _logger.error('no CLASS attribute in %r', item) + except Exception: + logging.exception('Exception while loading extension:') + + return options + + def __cancel_clicked_cb(self, widget): + self._section_view.undo() + self._options[self._current_option]['alerts'] = [] + self._section_toolbar.accept_button.set_sensitive(True) + self._show_main_view() + + def __accept_clicked_cb(self, widget): + if self._section_view.needs_restart: + self._section_toolbar.accept_button.set_sensitive(False) + self._section_toolbar.cancel_button.set_sensitive(False) + alert = Alert() + alert.props.title = _('Warning') + alert.props.msg = _('Changes require restart') + + icon = Icon(icon_name='dialog-cancel') + alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel changes'), icon) + icon.show() + + if self._current_option != 'aboutme': + icon = Icon(icon_name='dialog-ok') + alert.add_button(gtk.RESPONSE_ACCEPT, _('Later'), icon) + icon.show() + + icon = Icon(icon_name='system-restart') + alert.add_button(gtk.RESPONSE_APPLY, _('Restart now'), icon) + icon.show() + + self._vbox.pack_start(alert, False) + self._vbox.reorder_child(alert, 2) + alert.connect('response', self.__response_cb) + alert.show() + else: + self._show_main_view() + + def __response_cb(self, alert, response_id): + self._vbox.remove(alert) + self._section_toolbar.accept_button.set_sensitive(True) + self._section_toolbar.cancel_button.set_sensitive(True) + if response_id is gtk.RESPONSE_CANCEL: + self._section_view.undo() + self._section_view.setup() + self._options[self._current_option]['alerts'] = [] + elif response_id is gtk.RESPONSE_ACCEPT: + self._options[self._current_option]['alerts'] = \ + self._section_view.restart_alerts + self._show_main_view() + elif response_id is gtk.RESPONSE_APPLY: + session_manager = get_session_manager() + session_manager.logout() + + def __select_option_cb(self, button, event, option): + self.show_section_view(option) + + def __search_changed_cb(self, maintoolbar, query): + self._update(query) + + def __stop_clicked_cb(self, widget): + self.destroy() + + def __close_request_cb(self, widget, event=None): + self.destroy() + + def __valid_section_cb(self, section_view, pspec): + section_is_valid = section_view.props.is_valid + self._section_toolbar.accept_button.set_sensitive(section_is_valid) + + +class ModelWrapper(object): + def __init__(self, module): + self._module = module + self._options = {} + self._setup() + + def _setup(self): + methods = dir(self._module) + for method in methods: + if method.startswith('get_') and method[4:] != 'color': + try: + self._options[method[4:]] = getattr(self._module, method)() + except Exception: + self._options[method[4:]] = None + + def __getattr__(self, name): + return getattr(self._module, name) + + def undo(self): + for key in self._options.keys(): + method = getattr(self._module, 'set_' + key, None) + if method and self._options[key] is not None: + try: + method(self._options[key]) + except Exception, detail: + _logger.debug('Error undo option: %s', detail) + + +class _SectionIcon(gtk.EventBox): + __gtype_name__ = 'SugarSectionIcon' + + __gproperties__ = { + 'icon-name': (str, None, None, None, gobject.PARAM_READWRITE), + 'pixel-size': (object, None, None, gobject.PARAM_READWRITE), + 'xo-color': (object, None, None, gobject.PARAM_READWRITE), + 'title': (str, None, None, None, gobject.PARAM_READWRITE), + } + + def __init__(self, **kwargs): + self._icon_name = None + self._pixel_size = style.GRID_CELL_SIZE + self._xo_color = None + self._title = 'No Title' + + gobject.GObject.__init__(self, **kwargs) + + self._vbox = gtk.VBox() + self._icon = Icon(icon_name=self._icon_name, + pixel_size=self._pixel_size, + xo_color=self._xo_color) + self._vbox.pack_start(self._icon, expand=False, fill=False) + + self._label = gtk.Label(self._title) + self._label.modify_fg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + self._vbox.pack_start(self._label, expand=False, fill=False) + + self._vbox.set_spacing(style.DEFAULT_SPACING) + self.set_visible_window(False) + self.set_app_paintable(True) + self.set_events(gtk.gdk.BUTTON_PRESS_MASK) + + self.add(self._vbox) + self._vbox.show() + self._label.show() + self._icon.show() + + def get_icon(self): + return self._icon + + def do_set_property(self, pspec, value): + if pspec.name == 'icon-name': + if self._icon_name != value: + self._icon_name = value + elif pspec.name == 'pixel-size': + if self._pixel_size != value: + self._pixel_size = value + elif pspec.name == 'xo-color': + if self._xo_color != value: + self._xo_color = value + elif pspec.name == 'title': + if self._title != value: + self._title = value + + def do_get_property(self, pspec): + if pspec.name == 'icon-name': + return self._icon_name + elif pspec.name == 'pixel-size': + return self._pixel_size + elif pspec.name == 'xo-color': + return self._xo_color + elif pspec.name == 'title': + return self._title diff --git a/src/jarabe/controlpanel/inlinealert.py b/src/jarabe/controlpanel/inlinealert.py new file mode 100644 index 0000000..f970af4 --- /dev/null +++ b/src/jarabe/controlpanel/inlinealert.py @@ -0,0 +1,81 @@ +# Copyright (C) 2008, OLPC +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +import pango + +from sugar.graphics import style +from sugar.graphics.icon import Icon + + +class InlineAlert(gtk.HBox): + """UI interface for Inline alerts + + Inline alerts are different from the other alerts beause they are + no dialogs, they only inform about a current event. + + Properties: + 'msg': the message of the alert, + 'icon': the icon that appears at the far left + See __gproperties__ + """ + + __gtype_name__ = 'SugarInlineAlert' + + __gproperties__ = { + 'msg': (str, None, None, None, gobject.PARAM_READWRITE), + 'icon': (object, None, None, gobject.PARAM_WRITABLE), + } + + def __init__(self, **kwargs): + + self._msg = None + self._msg_color = None + self._icon = Icon(icon_name='emblem-warning', + fill_color=style.COLOR_SELECTION_GREY.get_svg(), + stroke_color=style.COLOR_WHITE.get_svg()) + + self._msg_label = gtk.Label() + self._msg_label.set_max_width_chars(50) + self._msg_label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) + self._msg_label.set_alignment(0, 0.5) + self._msg_label.modify_fg(gtk.STATE_NORMAL, + style.COLOR_SELECTION_GREY.get_gdk_color()) + + gobject.GObject.__init__(self, **kwargs) + + self.set_spacing(style.DEFAULT_SPACING) + self.modify_bg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + + self.pack_start(self._icon, False) + self.pack_start(self._msg_label, False) + self._msg_label.show() + self._icon.show() + + def do_set_property(self, pspec, value): + if pspec.name == 'msg': + if self._msg != value: + self._msg = value + self._msg_label.set_markup(self._msg) + elif pspec.name == 'icon': + if self._icon != value: + self._icon = value + + def do_get_property(self, pspec): + if pspec.name == 'msg': + return self._msg diff --git a/src/jarabe/controlpanel/sectionview.py b/src/jarabe/controlpanel/sectionview.py new file mode 100644 index 0000000..4b5751f --- /dev/null +++ b/src/jarabe/controlpanel/sectionview.py @@ -0,0 +1,54 @@ +# Copyright (C) 2008, OLPC +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk +from gettext import gettext as _ + + +class SectionView(gtk.VBox): + __gtype_name__ = 'SugarSectionView' + + __gsignals__ = { + 'request-close': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + __gproperties__ = { + 'is_valid': (bool, None, None, True, gobject.PARAM_READWRITE), + } + + _APPLY_TIMEOUT = 1000 + + def __init__(self): + gtk.VBox.__init__(self) + self._is_valid = True + self.auto_close = False + self.needs_restart = False + self.restart_alerts = [] + self.restart_msg = _('Changes require restart') + + def do_set_property(self, pspec, value): + if pspec.name == 'is-valid': + if self._is_valid != value: + self._is_valid = value + + def do_get_property(self, pspec): + if pspec.name == 'is-valid': + return self._is_valid + + def undo(self): + """Undo here the changes that have been made in this section.""" + pass diff --git a/src/jarabe/controlpanel/toolbar.py b/src/jarabe/controlpanel/toolbar.py new file mode 100644 index 0000000..fca34a0 --- /dev/null +++ b/src/jarabe/controlpanel/toolbar.py @@ -0,0 +1,160 @@ +# Copyright (C) 2007, 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gettext +import gobject + +_ = lambda msg: gettext.dgettext('sugar', msg) + +from sugar.graphics.icon import Icon +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics import iconentry +from sugar.graphics import style + + +class MainToolbar(gtk.Toolbar): + """ Main toolbar of the control panel + """ + __gtype_name__ = 'MainToolbar' + + __gsignals__ = { + 'stop-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'search-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._add_separator() + + tool_item = gtk.ToolItem() + self.insert(tool_item, -1) + tool_item.show() + self._search_entry = iconentry.IconEntry() + self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self._search_entry.add_clear_button() + self._search_entry.set_width_chars(25) + self._search_entry.connect('changed', self.__search_entry_changed_cb) + tool_item.add(self._search_entry) + self._search_entry.show() + + self._add_separator(True) + + self.stop = ToolButton(icon_name='dialog-cancel') + self.stop.set_tooltip(_('Done')) + self.stop.connect('clicked', self.__stop_clicked_cb) + self.stop.show() + self.insert(self.stop, -1) + self.stop.show() + + def get_entry(self): + return self._search_entry + + def _add_separator(self, expand=False): + separator = gtk.SeparatorToolItem() + separator.props.draw = False + if expand: + separator.set_expand(True) + else: + separator.set_size_request(style.DEFAULT_SPACING, -1) + self.insert(separator, -1) + separator.show() + + def __search_entry_changed_cb(self, search_entry): + self.emit('search-changed', search_entry.props.text) + + def __stop_clicked_cb(self, button): + self.emit('stop-clicked') + + +class SectionToolbar(gtk.Toolbar): + """ Toolbar of the sections of the control panel + """ + __gtype_name__ = 'SectionToolbar' + + __gsignals__ = { + 'cancel-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'accept-clicked': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._add_separator() + + self._icon = Icon() + self._add_widget(self._icon) + + self._add_separator() + + self._title = gtk.Label() + self._add_widget(self._title) + + self._add_separator(True) + + self.cancel_button = ToolButton('dialog-cancel') + self.cancel_button.set_tooltip(_('Cancel')) + self.cancel_button.connect('clicked', self.__cancel_button_clicked_cb) + self.insert(self.cancel_button, -1) + self.cancel_button.show() + + self.accept_button = ToolButton('dialog-ok') + self.accept_button.set_tooltip(_('Ok')) + self.accept_button.connect('clicked', self.__accept_button_clicked_cb) + self.insert(self.accept_button, -1) + self.accept_button.show() + + def get_icon(self): + return self._icon + + def get_title(self): + return self._title + + def _add_separator(self, expand=False): + separator = gtk.SeparatorToolItem() + separator.props.draw = False + if expand: + separator.set_expand(True) + else: + separator.set_size_request(style.DEFAULT_SPACING, -1) + self.insert(separator, -1) + separator.show() + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + def __cancel_button_clicked_cb(self, widget, data=None): + self.emit('cancel-clicked') + + def __accept_button_clicked_cb(self, widget, data=None): + self.emit('accept-clicked') diff --git a/src/jarabe/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 = '%s' % activity_info.get_name() + else: + tags = ', '.join(tag_list) + title = '%s\n' \ + '%s' % \ + (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. +# +# 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 = _('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 = _('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. +# +# 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() diff --git a/src/jarabe/frame/Makefile.am b/src/jarabe/frame/Makefile.am new file mode 100644 index 0000000..e5c445f --- /dev/null +++ b/src/jarabe/frame/Makefile.am @@ -0,0 +1,18 @@ +sugardir = $(pythondir)/jarabe/frame +sugar_PYTHON = \ + __init__.py \ + activitiestray.py \ + clipboard.py \ + clipboardicon.py \ + clipboardmenu.py \ + clipboardobject.py \ + clipboardpanelwindow.py \ + clipboardtray.py \ + devicestray.py \ + frameinvoker.py \ + friendstray.py \ + eventarea.py \ + frame.py \ + notification.py \ + framewindow.py \ + zoomtoolbar.py diff --git a/src/jarabe/frame/Makefile.in b/src/jarabe/frame/Makefile.in new file mode 100644 index 0000000..8da62b8 --- /dev/null +++ b/src/jarabe/frame/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/frame +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/frame +sugar_PYTHON = \ + __init__.py \ + activitiestray.py \ + clipboard.py \ + clipboardicon.py \ + clipboardmenu.py \ + clipboardobject.py \ + clipboardpanelwindow.py \ + clipboardtray.py \ + devicestray.py \ + frameinvoker.py \ + friendstray.py \ + eventarea.py \ + frame.py \ + notification.py \ + framewindow.py \ + zoomtoolbar.py + +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/frame/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/frame/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/frame/__init__.py b/src/jarabe/frame/__init__.py new file mode 100644 index 0000000..b3e4b80 --- /dev/null +++ b/src/jarabe/frame/__init__.py @@ -0,0 +1,27 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from jarabe.frame.frame import Frame + + +_view = None + + +def get_view(): + global _view + if not _view: + _view = Frame() + return _view diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py new file mode 100644 index 0000000..9590bce --- /dev/null +++ b/src/jarabe/frame/activitiestray.py @@ -0,0 +1,769 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2010 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import tempfile +import os + +import gobject +import gconf +import gio +import glib +import gtk + +from sugar.graphics import style +from sugar.graphics.tray import HTray +from sugar.graphics.xocolor import XoColor +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.icon import Icon, get_icon_file_name +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.datastore import datastore +from sugar import mime +from sugar import env + +from jarabe.model import shell +from jarabe.model import invites +from jarabe.model import bundleregistry +from jarabe.model import filetransfer +from jarabe.view.palettes import JournalPalette, CurrentActivityPalette +from jarabe.view.pulsingicon import PulsingIcon +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.frame.notification import NotificationIcon +import jarabe.frame + + +class ActivityButton(RadioToolButton): + def __init__(self, home_activity, group): + RadioToolButton.__init__(self, group=group) + + self.set_palette_invoker(FrameWidgetInvoker(self)) + self.palette_invoker.cache_palette = False + + self._home_activity = home_activity + self._notify_launch_hid = None + + self._icon = PulsingIcon() + self._icon.props.base_color = home_activity.get_icon_color() + self._icon.props.pulse_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TOOLBAR_GREY.get_svg())) + if home_activity.get_icon_path(): + self._icon.props.file = home_activity.get_icon_path() + else: + self._icon.props.icon_name = 'image-missing' + self.set_icon_widget(self._icon) + self._icon.show() + + if home_activity.props.launch_status == shell.Activity.LAUNCHING: + self._icon.props.pulsing = True + self._notify_launch_hid = home_activity.connect( \ + 'notify::launch-status', self.__notify_launch_status_cb) + elif home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() + + def create_palette(self): + if self._home_activity.is_journal(): + palette = JournalPalette(self._home_activity) + else: + palette = CurrentActivityPalette(self._home_activity) + palette.set_group_id('frame') + self.set_palette(palette) + + def _on_failed_launch(self): + # TODO http://bugs.sugarlabs.org/ticket/2007 + pass + + def __notify_launch_status_cb(self, home_activity, pspec): + home_activity.disconnect(self._notify_launch_hid) + self._notify_launch_hid = None + if home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() + else: + self._icon.props.pulsing = False + + +class InviteButton(ToolButton): + """Invite to shared activity""" + + __gsignals__ = { + 'remove-invite': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, invite): + ToolButton.__init__(self) + + self._invite = invite + + self.connect('clicked', self.__clicked_cb) + self.connect('destroy', self.__destroy_cb) + + bundle_registry = bundleregistry.get_registry() + bundle = bundle_registry.get_bundle(invite.get_bundle_id()) + + self._icon = Icon() + self._icon.props.xo_color = invite.get_color() + if bundle is not None: + self._icon.props.file = bundle.get_icon() + else: + self._icon.props.icon_name = 'image-missing' + self.set_icon_widget(self._icon) + self._icon.show() + + palette = InvitePalette(invite) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + palette.connect('remove-invite', self.__remove_invite_cb) + self.set_palette(palette) + + self._notif_icon = NotificationIcon() + self._notif_icon.connect('button-release-event', + self.__button_release_event_cb) + + self._notif_icon.props.xo_color = invite.get_color() + if bundle is not None: + self._notif_icon.props.icon_filename = bundle.get_icon() + else: + self._notif_icon.props.icon_name = 'image-missing' + + frame = jarabe.frame.get_view() + frame.add_notification(self._notif_icon, gtk.CORNER_TOP_LEFT) + + def __button_release_event_cb(self, icon, event): + if self._notif_icon is not None: + frame = jarabe.frame.get_view() + frame.remove_notification(self._notif_icon) + self._notif_icon = None + self._invite.join() + self.emit('remove-invite') + + def __clicked_cb(self, button): + self.palette.popup(immediate=True, state=Palette.SECONDARY) + + def __remove_invite_cb(self, palette): + self.emit('remove-invite') + + def __destroy_cb(self, button): + if self._notif_icon is not None: + frame = jarabe.frame.get_view() + frame.remove_notification(self._notif_icon) + self._notif_icon = None + + +class InvitePalette(Palette): + """Palette for frame or notification icon for invites.""" + + __gsignals__ = { + 'remove-invite': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, invite): + Palette.__init__(self, '') + + self._invite = invite + + menu_item = MenuItem(_('Join'), icon_name='dialog-ok') + menu_item.connect('activate', self.__join_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__decline_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + bundle_id = invite.get_bundle_id() + + registry = bundleregistry.get_registry() + self._bundle = registry.get_bundle(bundle_id) + if self._bundle: + name = self._bundle.get_name() + else: + name = bundle_id + + self.set_primary_text(glib.markup_escape_text(name)) + + def __join_activate_cb(self, menu_item): + self._invite.join() + self.emit('remove-invite') + + def __decline_activate_cb(self, menu_item): + self.emit('remove-invite') + + +class ActivitiesTray(HTray): + def __init__(self): + HTray.__init__(self) + + self._buttons = {} + self._invite_to_item = {} + self._freeze_button_clicks = False + + self._home_model = shell.get_model() + self._home_model.connect('activity-added', self.__activity_added_cb) + self._home_model.connect('activity-removed', + self.__activity_removed_cb) + self._home_model.connect('active-activity-changed', + self.__activity_changed_cb) + self._home_model.connect('tabbing-activity-changed', + self.__tabbing_activity_changed_cb) + + self._invites = invites.get_instance() + for invite in self._invites: + self._add_invite(invite) + self._invites.connect('invite-added', self.__invite_added_cb) + self._invites.connect('invite-removed', self.__invite_removed_cb) + + filetransfer.new_file_transfer.connect(self.__new_file_transfer_cb) + + def __activity_added_cb(self, home_model, home_activity): + logging.debug('__activity_added_cb: %r', home_activity) + if self.get_children(): + group = self.get_children()[0] + else: + group = None + + button = ActivityButton(home_activity, group) + self.add_item(button) + self._buttons[home_activity] = button + button.connect('clicked', self.__activity_clicked_cb, home_activity) + button.show() + + def __activity_removed_cb(self, home_model, home_activity): + logging.debug('__activity_removed_cb: %r', home_activity) + button = self._buttons[home_activity] + self.remove_item(button) + del self._buttons[home_activity] + + def _activate_activity(self, home_activity): + button = self._buttons[home_activity] + self._freeze_button_clicks = True + button.props.active = True + self._freeze_button_clicks = False + + self.scroll_to_item(button) + # Redraw immediately. + # The widget may not be realized yet, and then there is no window. + if self.window: + self.window.process_updates(True) + + def __activity_changed_cb(self, home_model, home_activity): + logging.debug('__activity_changed_cb: %r', home_activity) + + # Only select the new activity, if there is no tabbing activity. + if home_model.get_tabbing_activity() is None: + self._activate_activity(home_activity) + + def __tabbing_activity_changed_cb(self, home_model, home_activity): + logging.debug('__tabbing_activity_changed_cb: %r', home_activity) + # If the tabbing_activity is set to None just do nothing. + # The active activity will be updated a bit later (and it will + # be set to the activity that is currently selected). + if home_activity is None: + return + + self._activate_activity(home_activity) + + def __activity_clicked_cb(self, button, home_activity): + if not self._freeze_button_clicks and button.props.active: + logging.debug('ActivitiesTray.__activity_clicked_cb') + window = home_activity.get_window() + if window: + window.activate(gtk.get_current_event_time()) + + def __remove_invite_cb(self, icon, invite): + self._invites.remove_invite(invite) + + def __invite_added_cb(self, invites_model, invite): + self._add_invite(invite) + + def __invite_removed_cb(self, invites_model, invite): + self._remove_invite(invite) + + def _add_invite(self, invite): + """Add an invite""" + item = InviteButton(invite) + item.connect('remove-invite', self.__remove_invite_cb, invite) + self.add_item(item) + item.show() + self._invite_to_item[invite] = item + + def _remove_invite(self, invite): + self.remove_item(self._invite_to_item[invite]) + self._invite_to_item[invite].destroy() + del self._invite_to_item[invite] + + def __new_file_transfer_cb(self, **kwargs): + file_transfer = kwargs['file_transfer'] + logging.debug('__new_file_transfer_cb %r', file_transfer) + + if isinstance(file_transfer, filetransfer.IncomingFileTransfer): + button = IncomingTransferButton(file_transfer) + elif isinstance(file_transfer, filetransfer.OutgoingFileTransfer): + button = OutgoingTransferButton(file_transfer) + + self.add_item(button) + button.show() + + +class BaseTransferButton(ToolButton): + """Button with a notification attached + """ + def __init__(self, file_transfer): + ToolButton.__init__(self) + + self.file_transfer = file_transfer + file_transfer.connect('notify::state', self.__notify_state_cb) + + icon = Icon() + self.props.icon_widget = icon + icon.show() + + self.notif_icon = NotificationIcon() + self.notif_icon.connect('button-release-event', + self.__button_release_event_cb) + + self.connect('clicked', self.__button_clicked_cb) + + def __button_release_event_cb(self, icon, event): + if self.notif_icon is not None: + frame = jarabe.frame.get_view() + frame.remove_notification(self.notif_icon) + self.notif_icon = None + + def __button_clicked_cb(self, button): + self.palette.popup(immediate=True, state=Palette.SECONDARY) + + def remove(self): + frame = jarabe.frame.get_view() + frame.remove_notification(self.notif_icon) + self.props.parent.remove(self) + + def __notify_state_cb(self, file_transfer, pspec): + logging.debug('_update state: %r %r', file_transfer.props.state, + file_transfer.reason_last_change) + if file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + if file_transfer.reason_last_change == \ + filetransfer.FT_REASON_LOCAL_STOPPED: + self.remove() + + +class IncomingTransferButton(BaseTransferButton): + """UI element representing an ongoing incoming file transfer + """ + def __init__(self, file_transfer): + BaseTransferButton.__init__(self, file_transfer) + + self._ds_object = datastore.create() + + file_transfer.connect('notify::state', self.__notify_state_cb) + file_transfer.connect('notify::transferred-bytes', + self.__notify_transferred_bytes_cb) + + icons = gio.content_type_get_icon(file_transfer.mime_type).props.names + icons.append('application-octet-stream') + for icon_name in icons: + icon_name = 'transfer-from-%s' % icon_name + file_name = get_icon_file_name(icon_name) + if file_name is not None: + self.props.icon_widget.props.icon_name = icon_name + self.notif_icon.props.icon_name = icon_name + break + + icon_color = file_transfer.buddy.props.color + self.props.icon_widget.props.xo_color = icon_color + self.notif_icon.props.xo_color = icon_color + + frame = jarabe.frame.get_view() + frame.add_notification(self.notif_icon, + gtk.CORNER_TOP_LEFT) + + def create_palette(self): + palette = IncomingTransferPalette(self.file_transfer) + palette.connect('dismiss-clicked', self.__dismiss_clicked_cb) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + return palette + + def __notify_state_cb(self, file_transfer, pspec): + if file_transfer.props.state == filetransfer.FT_STATE_OPEN: + logging.debug('__notify_state_cb OPEN') + self._ds_object.metadata['title'] = file_transfer.title + self._ds_object.metadata['description'] = file_transfer.description + self._ds_object.metadata['progress'] = '0' + self._ds_object.metadata['keep'] = '0' + self._ds_object.metadata['buddies'] = '' + self._ds_object.metadata['preview'] = '' + self._ds_object.metadata['icon-color'] = \ + file_transfer.buddy.props.color.to_string() + self._ds_object.metadata['mime_type'] = file_transfer.mime_type + elif file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: + logging.debug('__notify_state_cb COMPLETED') + self._ds_object.metadata['progress'] = '100' + self._ds_object.file_path = file_transfer.destination_path + datastore.write(self._ds_object, transfer_ownership=True, + reply_handler=self.__reply_handler_cb, + error_handler=self.__error_handler_cb) + elif file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + logging.debug('__notify_state_cb CANCELLED') + object_id = self._ds_object.object_id + if object_id is not None: + self._ds_object.destroy() + datastore.delete(object_id) + self._ds_object = None + + def __notify_transferred_bytes_cb(self, file_transfer, pspec): + progress = file_transfer.props.transferred_bytes / \ + file_transfer.file_size + self._ds_object.metadata['progress'] = str(progress * 100) + datastore.write(self._ds_object, update_mtime=False) + + def __reply_handler_cb(self): + logging.debug('__reply_handler_cb %r', self._ds_object.object_id) + + def __error_handler_cb(self, error): + logging.debug('__error_handler_cb %r %s', self._ds_object.object_id, + error) + + def __dismiss_clicked_cb(self, palette): + self.remove() + + +class OutgoingTransferButton(BaseTransferButton): + """UI element representing an ongoing outgoing file transfer + """ + def __init__(self, file_transfer): + BaseTransferButton.__init__(self, file_transfer) + + icons = gio.content_type_get_icon(file_transfer.mime_type).props.names + icons.append('application-octet-stream') + for icon_name in icons: + icon_name = 'transfer-to-%s' % icon_name + file_name = get_icon_file_name(icon_name) + if file_name is not None: + self.props.icon_widget.props.icon_name = icon_name + self.notif_icon.props.icon_name = icon_name + break + + client = gconf.client_get_default() + icon_color = XoColor(client.get_string('/desktop/sugar/user/color')) + self.props.icon_widget.props.xo_color = icon_color + self.notif_icon.props.xo_color = icon_color + + frame = jarabe.frame.get_view() + frame.add_notification(self.notif_icon, + gtk.CORNER_TOP_LEFT) + + def create_palette(self): + palette = OutgoingTransferPalette(self.file_transfer) + palette.connect('dismiss-clicked', self.__dismiss_clicked_cb) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + return palette + + def __dismiss_clicked_cb(self, palette): + self.remove() + + +class BaseTransferPalette(Palette): + """Base palette class for frame or notification icon for file transfers + """ + __gtype_name__ = 'SugarBaseTransferPalette' + + __gsignals__ = { + 'dismiss-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, file_transfer): + Palette.__init__(self, glib.markup_escape_text(file_transfer.title)) + + self.file_transfer = file_transfer + + self.progress_bar = None + self.progress_label = None + self._notify_transferred_bytes_handler = None + + self.connect('popup', self.__popup_cb) + self.connect('popdown', self.__popdown_cb) + + def __popup_cb(self, palette): + self.update_progress() + self._notify_transferred_bytes_handler = \ + self.file_transfer.connect('notify::transferred_bytes', + self.__notify_transferred_bytes_cb) + + def __popdown_cb(self, palette): + if self._notify_transferred_bytes_handler is not None: + self.file_transfer.disconnect( + self._notify_transferred_bytes_handler) + self._notify_transferred_bytes_handler = None + + def __notify_transferred_bytes_cb(self, file_transfer, pspec): + self.update_progress() + + def _format_size(self, size): + if size < 1024: + return _('%dB') % size + elif size < 1048576: + return _('%dKB') % (size / 1024) + else: + return _('%dMB') % (size / 1048576) + + def update_progress(self): + logging.debug('update_progress: %r', + self.file_transfer.props.transferred_bytes) + + if self.progress_bar is None: + return + + self.progress_bar.props.fraction = \ + self.file_transfer.props.transferred_bytes / \ + float(self.file_transfer.file_size) + logging.debug('update_progress: %r', self.progress_bar.props.fraction) + + transferred = self._format_size( + self.file_transfer.props.transferred_bytes) + total = self._format_size(self.file_transfer.file_size) + # TRANS: file transfer, bytes transferred, e.g. 128 of 1024 + self.progress_label.props.label = _('%s of %s') % (transferred, total) + + +class IncomingTransferPalette(BaseTransferPalette): + """Palette for frame or notification icon for incoming file transfers + """ + __gtype_name__ = 'SugarIncomingTransferPalette' + + def __init__(self, file_transfer): + BaseTransferPalette.__init__(self, file_transfer) + + self.file_transfer.connect('notify::state', self.__notify_state_cb) + + nick = str(self.file_transfer.buddy.props.nick) + label = glib.markup_escape_text(_('Transfer from %s') % (nick,)) + self.props.secondary_text = label + + self._update() + + def __notify_state_cb(self, file_transfer, pspec): + self._update() + + def _update(self): + logging.debug('_update state: %r', self.file_transfer.props.state) + if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING: + menu_item = MenuItem(_('Accept'), icon_name='dialog-ok') + menu_item.connect('activate', self.__accept_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__decline_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + if self.file_transfer.description: + label = gtk.Label(self.file_transfer.description) + vbox.add(label) + label.show() + + mime_type = self.file_transfer.mime_type + type_description = mime.get_mime_description(mime_type) + + size = self._format_size(self.file_transfer.file_size) + label = gtk.Label('%s (%s)' % (size, type_description)) + vbox.add(label) + label.show() + + elif self.file_transfer.props.state in \ + [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self.progress_bar = gtk.ProgressBar() + vbox.add(self.progress_bar) + self.progress_bar.show() + + self.progress_label = gtk.Label('') + vbox.add(self.progress_label) + self.progress_label.show() + + self.update_progress() + + elif self.file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Dismiss'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__dismiss_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + self.update_progress() + elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + + for item in self.menu.get_children(): + self.menu.remove(item) + + if self.file_transfer.reason_last_change == \ + filetransfer.FT_REASON_REMOTE_STOPPED: + menu_item = MenuItem(_('Dismiss'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__dismiss_activate_cb) + self.menu.append(menu_item) + menu_item.show() + text = _('The other participant canceled the file transfer') + label = gtk.Label(text) + self.set_content(label) + label.show() + + def __accept_activate_cb(self, menu_item): + #TODO: figure out the best place to get rid of that temp file + extension = mime.get_primary_extension(self.file_transfer.mime_type) + if extension is None: + extension = '.bin' + fd, file_path = tempfile.mkstemp(suffix=extension, + prefix=self._sanitize(self.file_transfer.title), + dir=os.path.join(env.get_profile_path(), 'data')) + os.close(fd) + os.unlink(file_path) + + self.file_transfer.accept(file_path) + + def _sanitize(self, file_name): + file_name = file_name.replace('/', '_') + file_name = file_name.replace('.', '_') + file_name = file_name.replace('?', '_') + return file_name + + def __decline_activate_cb(self, menu_item): + self.file_transfer.cancel() + + def __cancel_activate_cb(self, menu_item): + self.file_transfer.cancel() + + def __dismiss_activate_cb(self, menu_item): + self.emit('dismiss-clicked') + + +class OutgoingTransferPalette(BaseTransferPalette): + """Palette for frame or notification icon for outgoing file transfers + """ + __gtype_name__ = 'SugarOutgoingTransferPalette' + + def __init__(self, file_transfer): + BaseTransferPalette.__init__(self, file_transfer) + + self.progress_bar = None + self.progress_label = None + + self.file_transfer.connect('notify::state', self.__notify_state_cb) + + nick = str(file_transfer.buddy.props.nick) + label = glib.markup_escape_text(_('Transfer to %s') % (nick,)) + self.props.secondary_text = label + + self._update() + + def __notify_state_cb(self, file_transfer, pspec): + self._update() + + def _update(self): + new_state = self.file_transfer.props.state + logging.debug('_update state: %r', new_state) + if new_state == filetransfer.FT_STATE_PENDING: + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + if self.file_transfer.description: + label = gtk.Label(self.file_transfer.description) + vbox.add(label) + label.show() + + mime_type = self.file_transfer.mime_type + type_description = mime.get_mime_description(mime_type) + + size = self._format_size(self.file_transfer.file_size) + label = gtk.Label('%s (%s)' % (size, type_description)) + vbox.add(label) + label.show() + + elif new_state in [filetransfer.FT_STATE_ACCEPTED, + filetransfer.FT_STATE_OPEN]: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self.progress_bar = gtk.ProgressBar() + vbox.add(self.progress_bar) + self.progress_bar.show() + + self.progress_label = gtk.Label('') + vbox.add(self.progress_label) + self.progress_label.show() + + self.update_progress() + + elif new_state in [filetransfer.FT_STATE_COMPLETED, + filetransfer.FT_STATE_CANCELLED]: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Dismiss'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__dismiss_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + self.update_progress() + + def __cancel_activate_cb(self, menu_item): + self.file_transfer.cancel() + + def __dismiss_activate_cb(self, menu_item): + self.emit('dismiss-clicked') diff --git a/src/jarabe/frame/clipboard.py b/src/jarabe/frame/clipboard.py new file mode 100644 index 0000000..a09ac5b --- /dev/null +++ b/src/jarabe/frame/clipboard.py @@ -0,0 +1,178 @@ +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import os +import shutil +import urlparse +import tempfile + +import gobject +import gtk + +from sugar import mime + +from jarabe.frame.clipboardobject import ClipboardObject, Format + + +_instance = None + + +class Clipboard(gobject.GObject): + + __gsignals__ = { + 'object-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'object-deleted': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([long])), + 'object-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([long])), + 'object-state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._objects = {} + self._next_id = 0 + + def _get_next_object_id(self): + self._next_id += 1 + return self._next_id + + def add_object(self, name, data_hash=None): + """ Add a object to the clipboard + + Keyword arguments: + name -- object name + data_hash -- hash to check if the object is already + in the clipboard, generated with hash() + over the data to be added + + Return: object_id or None if the object is not added + + """ + logging.debug('Clipboard.add_object: hash %s', data_hash) + if data_hash is None: + object_id = self._get_next_object_id() + else: + object_id = data_hash + if object_id in self._objects: + logging.debug('Clipboard.add_object: object already in clipboard,' + ' selecting previous entry instead') + self.emit('object-selected', object_id) + return None + self._objects[object_id] = ClipboardObject(object_id, name) + self.emit('object-added', self._objects[object_id]) + return object_id + + def add_object_format(self, object_id, format_type, data, on_disk): + logging.debug('Clipboard.add_object_format') + cb_object = self._objects[object_id] + + if format_type == 'XdndDirectSave0': + format_ = Format('text/uri-list', data + '\r\n', on_disk) + format_.owns_disk_data = True + cb_object.add_format(format_) + elif on_disk and cb_object.get_percent() == 100: + new_uri = self._copy_file(data) + cb_object.add_format(Format(format_type, new_uri, on_disk)) + logging.debug('Added format of type ' + format_type + + ' with path at ' + new_uri) + else: + cb_object.add_format(Format(format_type, data, on_disk)) + logging.debug('Added in-memory format of type %s.', format_type) + + self.emit('object-state-changed', cb_object) + + def delete_object(self, object_id): + cb_object = self._objects.pop(object_id) + cb_object.destroy() + if not self._objects: + gtk_clipboard = gtk.Clipboard() + gtk_clipboard.clear() + self.emit('object-deleted', object_id) + logging.debug('Deleted object with object_id %r', object_id) + + def set_object_percent(self, object_id, percent): + cb_object = self._objects[object_id] + if percent < 0 or percent > 100: + raise ValueError('invalid percentage') + if cb_object.get_percent() > percent: + raise ValueError('invalid percentage; less than current percent') + if cb_object.get_percent() == percent: + # ignore setting same percentage + return + + cb_object.set_percent(percent) + + if percent == 100: + self._process_object(cb_object) + + self.emit('object-state-changed', cb_object) + + def _process_object(self, cb_object): + formats = cb_object.get_formats() + for format_name, format_ in formats.iteritems(): + if format_.is_on_disk() and not format_.owns_disk_data: + new_uri = self._copy_file(format_.get_data()) + format_.set_data(new_uri) + + # Add a text/plain format to objects that are text but lack it + if 'text/plain' not in formats.keys(): + if 'UTF8_STRING' in formats.keys(): + self.add_object_format( + cb_object.get_id(), 'text/plain', + data=formats['UTF8_STRING'].get_data(), on_disk=False) + elif 'text/unicode' in formats.keys(): + self.add_object_format( + cb_object.get_id(), 'text/plain', + data=formats['UTF8_STRING'].get_data(), on_disk=False) + + def get_object(self, object_id): + logging.debug('Clipboard.get_object') + return self._objects[object_id] + + def get_object_data(self, object_id, format_type): + logging.debug('Clipboard.get_object_data') + cb_object = self._objects[object_id] + format_ = cb_object.get_formats()[format_type] + return format_ + + def _copy_file(self, original_uri): + uri = urlparse.urlparse(original_uri) + path = uri.path # pylint: disable=E1101 + directory_, file_name = os.path.split(path) + + root, ext = os.path.splitext(file_name) + if not ext or ext == '.': + mime_type = mime.get_for_file(path) + ext = '.' + mime.get_primary_extension(mime_type) + + f_, new_file_path = tempfile.mkstemp(ext, root) + del f_ + shutil.copyfile(path, new_file_path) + os.chmod(new_file_path, 0644) + + return 'file://' + new_file_path + + +def get_instance(): + global _instance + if not _instance: + _instance = Clipboard() + return _instance diff --git a/src/jarabe/frame/clipboardicon.py b/src/jarabe/frame/clipboardicon.py new file mode 100644 index 0000000..315cdaa --- /dev/null +++ b/src/jarabe/frame/clipboardicon.py @@ -0,0 +1,170 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import gconf + +import gtk + +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.graphics import style + +from jarabe.frame import clipboard +from jarabe.frame.clipboardmenu import ClipboardMenu +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.frame.notification import NotificationIcon +import jarabe.frame + + +class ClipboardIcon(RadioToolButton): + __gtype_name__ = 'SugarClipboardIcon' + + def __init__(self, cb_object, group): + RadioToolButton.__init__(self, group=group) + + self.props.palette_invoker = FrameWidgetInvoker(self) + + self._cb_object = cb_object + self.owns_clipboard = False + self.props.sensitive = False + self.props.active = False + self._notif_icon = None + self._current_percent = None + + self._icon = Icon() + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + self._icon.props.xo_color = color + self.set_icon_widget(self._icon) + self._icon.show() + + cb_service = clipboard.get_instance() + cb_service.connect('object-state-changed', + self._object_state_changed_cb) + cb_service.connect('object-selected', self._object_selected_cb) + + child = self.get_child() + child.connect('drag_data_get', self._drag_data_get_cb) + self.connect('notify::active', self._notify_active_cb) + + def create_palette(self): + palette = ClipboardMenu(self._cb_object) + palette.set_group_id('frame') + return palette + + def get_object_id(self): + return self._cb_object.get_id() + + def _drag_data_get_cb(self, widget, context, selection, target_type, + event_time): + logging.debug('_drag_data_get_cb: requested target %s', + selection.target) + data = self._cb_object.get_formats()[selection.target].get_data() + selection.set(selection.target, 8, data) + + def _put_in_clipboard(self): + logging.debug('ClipboardIcon._put_in_clipboard') + + if self._cb_object.get_percent() < 100: + raise ValueError('Object is not complete, cannot be put into the' + ' clipboard.') + + targets = self._get_targets() + if targets: + x_clipboard = gtk.Clipboard() + if not x_clipboard.set_with_data(targets, + self._clipboard_data_get_cb, + self._clipboard_clear_cb, + targets): + logging.error('GtkClipboard.set_with_data failed!') + else: + self.owns_clipboard = True + + def _clipboard_data_get_cb(self, x_clipboard, selection, info, targets): + if not selection.target in [target[0] for target in targets]: + logging.warning('ClipboardIcon._clipboard_data_get_cb: asked %s' \ + ' but only have %r.', selection.target, targets) + return + data = self._cb_object.get_formats()[selection.target].get_data() + selection.set(selection.target, 8, data) + + def _clipboard_clear_cb(self, x_clipboard, targets): + logging.debug('ClipboardIcon._clipboard_clear_cb') + self.owns_clipboard = False + + def _object_state_changed_cb(self, cb_service, cb_object): + if cb_object != self._cb_object: + return + + if cb_object.get_icon(): + self._icon.props.icon_name = cb_object.get_icon() + else: + self._icon.props.icon_name = 'application-octet-stream' + + child = self.get_child() + child.connect('drag-begin', self._drag_begin_cb) + child.drag_source_set(gtk.gdk.BUTTON1_MASK, + self._get_targets(), + gtk.gdk.ACTION_COPY) + + if cb_object.get_percent() == 100: + self.props.sensitive = True + + # Clipboard object became complete. Make it the active one. + if self._current_percent < 100 and cb_object.get_percent() == 100: + self.props.active = True + self.show_notification() + + self._current_percent = cb_object.get_percent() + + def _object_selected_cb(self, cb_service, object_id): + if object_id != self._cb_object.get_id(): + return + self.props.active = True + self.show_notification() + logging.debug('ClipboardIcon: %r was selected', object_id) + + def show_notification(self): + self._notif_icon = NotificationIcon() + self._notif_icon.props.icon_name = self._icon.props.icon_name + self._notif_icon.props.xo_color = \ + XoColor('%s,%s' % (self._icon.props.stroke_color, + self._icon.props.fill_color)) + frame = jarabe.frame.get_view() + frame.add_notification(self._notif_icon, gtk.CORNER_BOTTOM_LEFT) + + def _drag_begin_cb(self, widget, context): + # TODO: We should get the pixbuf from the icon, with colors, etc. + icon_theme = gtk.icon_theme_get_default() + pixbuf = icon_theme.load_icon(self._icon.props.icon_name, + style.STANDARD_ICON_SIZE, 0) + context.set_icon_pixbuf(pixbuf, hot_x=pixbuf.props.width / 2, + hot_y=pixbuf.props.height / 2) + + def _notify_active_cb(self, widget, pspec): + if self.props.active: + self._put_in_clipboard() + else: + self.owns_clipboard = False + + def _get_targets(self): + targets = [] + for format_type in self._cb_object.get_formats().keys(): + targets.append((format_type, 0, 0)) + return targets diff --git a/src/jarabe/frame/clipboardmenu.py b/src/jarabe/frame/clipboardmenu.py new file mode 100644 index 0000000..4c077d9 --- /dev/null +++ b/src/jarabe/frame/clipboardmenu.py @@ -0,0 +1,256 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import tempfile +import urlparse +import os +import logging +import gconf +import glib + +import gtk + +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.datastore import datastore +from sugar import mime +from sugar import env +from sugar.activity.i18n import pgettext + +from jarabe.frame import clipboard +from jarabe.journal import misc +from jarabe.model import bundleregistry + + +class ClipboardMenu(Palette): + + def __init__(self, cb_object): + Palette.__init__(self, text_maxlen=100) + + self._cb_object = cb_object + + self.set_group_id('frame') + + cb_service = clipboard.get_instance() + cb_service.connect('object-state-changed', + self._object_state_changed_cb) + + self._progress_bar = None + + self._remove_item = MenuItem(pgettext('Clipboard', 'Remove'), + 'list-remove') + self._remove_item.connect('activate', self._remove_item_activate_cb) + self.menu.append(self._remove_item) + self._remove_item.show() + + self._open_item = MenuItem(_('Open'), 'zoom-activity') + self._open_item.connect('activate', self._open_item_activate_cb) + self.menu.append(self._open_item) + self._open_item.show() + + self._journal_item = MenuItem(_('Keep')) + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + icon = Icon(icon_name='document-save', icon_size=gtk.ICON_SIZE_MENU, + xo_color=color) + self._journal_item.set_image(icon) + + self._journal_item.connect('activate', self._journal_item_activate_cb) + self.menu.append(self._journal_item) + self._journal_item.show() + + self._update() + + def _update_open_submenu(self): + activities = self._get_activities() + logging.debug('_update_open_submenu: %r', activities) + child = self._open_item.get_child() + if activities is None or len(activities) <= 1: + child.set_text(_('Open')) + if self._open_item.get_submenu() is not None: + self._open_item.remove_submenu() + return + + child.set_text(_('Open with')) + submenu = self._open_item.get_submenu() + if submenu is None: + submenu = gtk.Menu() + self._open_item.set_submenu(submenu) + submenu.show() + else: + for item in submenu.get_children(): + submenu.remove(item) + + for service_name in activities: + registry = bundleregistry.get_registry() + activity_info = registry.get_bundle(service_name) + + if not activity_info: + logging.warning('Activity %s is unknown.', service_name) + + item = gtk.MenuItem(activity_info.get_name()) + item.connect('activate', self._open_submenu_item_activate_cb, + service_name) + submenu.append(item) + item.show() + + def _update_items_visibility(self): + activities = self._get_activities() + installable = self._cb_object.is_bundle() + percent = self._cb_object.get_percent() + + if percent == 100 and (activities or installable): + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = True + self._journal_item.props.sensitive = True + elif percent == 100 and (not activities and not installable): + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = False + self._journal_item.props.sensitive = True + else: + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = False + self._journal_item.props.sensitive = False + + self._update_progress_bar() + + def _get_activities(self): + mime_type = self._cb_object.get_mime_type() + if not mime_type: + return '' + + registry = bundleregistry.get_registry() + activities = registry.get_activities_for_type(mime_type) + if activities: + return [info.get_bundle_id() for info in activities] + else: + return '' + + def _update_progress_bar(self): + percent = self._cb_object.get_percent() + if percent == 100.0: + if self._progress_bar: + self._progress_bar = None + self.set_content(None) + else: + if self._progress_bar is None: + self._progress_bar = gtk.ProgressBar() + self._progress_bar.show() + self.set_content(self._progress_bar) + + self._progress_bar.props.fraction = percent / 100.0 + self._progress_bar.props.text = '%.2f %%' % percent + + def _object_state_changed_cb(self, cb_service, cb_object): + if cb_object != self._cb_object: + return + self._update() + + def _update(self): + name = self._cb_object.get_name() + self.props.primary_text = glib.markup_escape_text(name) + preview = self._cb_object.get_preview() + if preview: + self.props.secondary_text = glib.markup_escape_text(preview) + self._update_progress_bar() + self._update_items_visibility() + self._update_open_submenu() + + def _open_item_activate_cb(self, menu_item): + logging.debug('_open_item_activate_cb') + percent = self._cb_object.get_percent() + if percent < 100 or menu_item.get_submenu() is not None: + return + jobject = self._copy_to_journal() + misc.resume(jobject.metadata, self._get_activities()[0]) + jobject.destroy() + + def _open_submenu_item_activate_cb(self, menu_item, service_name): + logging.debug('_open_submenu_item_activate_cb') + percent = self._cb_object.get_percent() + if percent < 100: + return + jobject = self._copy_to_journal() + misc.resume(jobject.metadata, service_name) + jobject.destroy() + + def _remove_item_activate_cb(self, menu_item): + cb_service = clipboard.get_instance() + cb_service.delete_object(self._cb_object.get_id()) + + def _journal_item_activate_cb(self, menu_item): + logging.debug('_journal_item_activate_cb') + jobject = self._copy_to_journal() + jobject.destroy() + + def _write_to_temp_file(self, data): + tmp_dir = os.path.join(env.get_profile_path(), 'data') + f, file_path = tempfile.mkstemp(dir=tmp_dir) + try: + os.write(f, data) + finally: + os.close(f) + return file_path + + def _copy_to_journal(self): + formats = self._cb_object.get_formats().keys() + most_significant_mime_type = mime.choose_most_significant(formats) + format_ = self._cb_object.get_formats()[most_significant_mime_type] + + transfer_ownership = False + if most_significant_mime_type == 'text/uri-list': + uris = mime.split_uri_list(format_.get_data()) + if len(uris) == 1 and uris[0].startswith('file://'): + parsed_url = urlparse.urlparse(uris[0]) + file_path = parsed_url.path # pylint: disable=E1101 + transfer_ownership = False + mime_type = mime.get_for_file(file_path) + else: + file_path = self._write_to_temp_file(format_.get_data()) + transfer_ownership = True + mime_type = 'text/uri-list' + else: + if format_.is_on_disk(): + parsed_url = urlparse.urlparse(format_.get_data()) + file_path = parsed_url.path # pylint: disable=E1101 + transfer_ownership = False + mime_type = mime.get_for_file(file_path) + else: + file_path = self._write_to_temp_file(format_.get_data()) + transfer_ownership = True + sniffed_mime_type = mime.get_for_file(file_path) + if sniffed_mime_type == 'application/octet-stream': + mime_type = most_significant_mime_type + else: + mime_type = sniffed_mime_type + + jobject = datastore.create() + jobject.metadata['title'] = self._cb_object.get_name() + jobject.metadata['keep'] = '0' + jobject.metadata['buddies'] = '' + jobject.metadata['preview'] = '' + client = gconf.client_get_default() + color = client.get_string('/desktop/sugar/user/color') + jobject.metadata['icon-color'] = color + jobject.metadata['mime_type'] = mime_type + jobject.file_path = file_path + + datastore.write(jobject, transfer_ownership=transfer_ownership) + + return jobject diff --git a/src/jarabe/frame/clipboardobject.py b/src/jarabe/frame/clipboardobject.py new file mode 100644 index 0000000..407af2f --- /dev/null +++ b/src/jarabe/frame/clipboardobject.py @@ -0,0 +1,147 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import urlparse +import gio +import gtk + +from gettext import gettext as _ +from sugar import mime +from sugar.bundle.activitybundle import ActivityBundle + + +class ClipboardObject(object): + + def __init__(self, object_path, name): + self._id = object_path + self._name = name + self._percent = 0 + self._formats = {} + + def destroy(self): + for format_ in self._formats.itervalues(): + format_.destroy() + + def get_id(self): + return self._id + + def get_name(self): + name = self._name + if not name: + mime_type = mime.get_mime_description(self.get_mime_type()) + + if not mime_type: + mime_type = 'Data' + name = _('%s clipping') % mime_type + + return name + + def get_icon(self): + mime_type = self.get_mime_type() + + generic_types = mime.get_all_generic_types() + for generic_type in generic_types: + if mime_type in generic_type.mime_types: + return generic_type.icon + + icons = gio.content_type_get_icon(mime_type) + icon_name = None + if icons is not None: + icon_theme = gtk.icon_theme_get_default() + for icon_name in icons.props.names: + icon_info = icon_theme.lookup_icon(icon_name, + gtk.ICON_SIZE_LARGE_TOOLBAR, 0) + if icon_info is not None: + icon_info.free() + return icon_name + + return 'application-octet-stream' + + def get_preview(self): + for mime_type in ['text/plain']: + if mime_type in self._formats: + return self._formats[mime_type].get_data() + return '' + + def is_bundle(self): + # A bundle will have only one format. + if not self._formats: + return False + else: + return self._formats.keys()[0] in [ActivityBundle.MIME_TYPE, + ActivityBundle.DEPRECATED_MIME_TYPE] + + def get_percent(self): + return self._percent + + def set_percent(self, percent): + self._percent = percent + + def add_format(self, format_): + self._formats[format_.get_type()] = format_ + + def get_formats(self): + return self._formats + + def get_mime_type(self): + if not self._formats: + return '' + + format_ = mime.choose_most_significant(self._formats.keys()) + if format_ == 'text/uri-list': + data = self._formats['text/uri-list'].get_data() + uri = urlparse.urlparse(mime.split_uri_list(data)[0], 'file') + scheme = uri.scheme # pylint: disable=E1101 + if scheme == 'file': + path = uri.path # pylint: disable=E1101 + if os.path.exists(path): + format_ = mime.get_for_file(path) + else: + format_ = mime.get_from_file_name(path) + logging.debug('Chose %r!', format_) + + return format_ + + +class Format(object): + + def __init__(self, mime_type, data, on_disk): + self.owns_disk_data = False + + self._type = mime_type + self._data = data + self._on_disk = on_disk + + def destroy(self): + if self._on_disk: + uri = urlparse.urlparse(self._data) + path = uri.path # pylint: disable=E1101 + if os.path.isfile(path): + os.remove(path) + + def get_type(self): + return self._type + + def get_data(self): + return self._data + + def set_data(self, data): + self._data = data + + def is_on_disk(self): + return self._on_disk diff --git a/src/jarabe/frame/clipboardpanelwindow.py b/src/jarabe/frame/clipboardpanelwindow.py new file mode 100644 index 0000000..6811c0d --- /dev/null +++ b/src/jarabe/frame/clipboardpanelwindow.py @@ -0,0 +1,140 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from urlparse import urlparse +import hashlib + +import gtk + +from jarabe.frame.framewindow import FrameWindow +from jarabe.frame.clipboardtray import ClipboardTray + +from jarabe.frame import clipboard + + +class ClipboardPanelWindow(FrameWindow): + def __init__(self, frame, orientation): + FrameWindow.__init__(self, orientation) + + self._frame = frame + + # Listening for new clipboard objects + # NOTE: we need to keep a reference to gtk.Clipboard in order to keep + # listening to it. + self._clipboard = gtk.Clipboard() + self._clipboard.connect('owner-change', self._owner_change_cb) + + self._clipboard_tray = ClipboardTray() + self._clipboard_tray.show() + self.append(self._clipboard_tray) + + # Receiving dnd drops + self.drag_dest_set(0, [], 0) + self.connect('drag_motion', self._clipboard_tray.drag_motion_cb) + self.connect('drag_leave', self._clipboard_tray.drag_leave_cb) + self.connect('drag_drop', self._clipboard_tray.drag_drop_cb) + self.connect('drag_data_received', + self._clipboard_tray.drag_data_received_cb) + + def _owner_change_cb(self, x_clipboard, event): + logging.debug('owner_change_cb') + + if self._clipboard_tray.owns_clipboard(): + return + + cb_service = clipboard.get_instance() + + targets = x_clipboard.wait_for_targets() + cb_selections = [] + if targets is None: + return + + target_is_uri = False + for target in targets: + if target not in ('TIMESTAMP', 'TARGETS', + 'MULTIPLE', 'SAVE_TARGETS'): + logging.debug('Asking for target %s.', target) + if target == 'text/uri-list': + target_is_uri = True + + selection = x_clipboard.wait_for_contents(target) + if not selection: + logging.warning('no data for selection target %s.', target) + continue + cb_selections.append(selection) + + if target_is_uri: + uri = selection.data + filename = uri[len('file://'):].strip() + md5 = self._md5_for_file(filename) + data_hash = hash(md5) + else: + data_hash = hash(selection.data) + + if len(cb_selections) > 0: + key = cb_service.add_object(name="", data_hash=data_hash) + if key is None: + return + cb_service.set_object_percent(key, percent=0) + for selection in cb_selections: + self._add_selection(key, selection) + cb_service.set_object_percent(key, percent=100) + + def _md5_for_file(self, file_name): + '''Calculate md5 for file data + + Calculating block wise to prevent issues with big files in memory + ''' + block_size = 8192 + md5 = hashlib.md5() + f = open(file_name, 'r') + while True: + data = f.read(block_size) + if not data: + break + md5.update(data) + f.close() + return md5.digest() + + def _add_selection(self, key, selection): + if not selection.data: + logging.warning('no data for selection target %s.', selection.type) + return + + logging.debug('adding type ' + selection.type + '.') + + cb_service = clipboard.get_instance() + if selection.type == 'text/uri-list': + uris = selection.get_uris() + + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list' \ + ' still not supported.') + uri = uris[0] + scheme, netloc_, path_, parameters_, query_, fragment_ = \ + urlparse(uri) + on_disk = (scheme == 'file') + + cb_service.add_object_format(key, + selection.type, + uri, + on_disk) + else: + cb_service.add_object_format(key, + selection.type, + selection.data, + on_disk=False) diff --git a/src/jarabe/frame/clipboardtray.py b/src/jarabe/frame/clipboardtray.py new file mode 100644 index 0000000..37d5e1a --- /dev/null +++ b/src/jarabe/frame/clipboardtray.py @@ -0,0 +1,223 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import tempfile + +import gtk + +from sugar import util +from sugar.graphics import tray +from sugar.graphics import style + +from jarabe.frame import clipboard +from jarabe.frame.clipboardicon import ClipboardIcon + + +class _ContextMap(object): + """Maps a drag context to the clipboard object involved in the dragging.""" + def __init__(self): + self._context_map = {} + + def add_context(self, context, object_id, data_types): + """Establishes the mapping. data_types will serve us for reference- + counting this mapping. + """ + self._context_map[context] = [object_id, data_types] + + def get_object_id(self, context): + """Retrieves the object_id associated with context. + Will release the association when this function was called as many + times as the number of data_types that this clipboard object contains. + """ + [object_id, data_types_left] = self._context_map[context] + + data_types_left = data_types_left - 1 + if data_types_left == 0: + del self._context_map[context] + else: + self._context_map[context] = [object_id, data_types_left] + + return object_id + + def has_context(self, context): + return context in self._context_map + + +class ClipboardTray(tray.VTray): + + MAX_ITEMS = gtk.gdk.screen_height() / style.GRID_CELL_SIZE - 2 + + def __init__(self): + tray.VTray.__init__(self, align=tray.ALIGN_TO_END) + self._icons = {} + self._context_map = _ContextMap() + + cb_service = clipboard.get_instance() + cb_service.connect('object-added', self._object_added_cb) + cb_service.connect('object-deleted', self._object_deleted_cb) + + def owns_clipboard(self): + for icon in self._icons.values(): + if icon.owns_clipboard: + return True + return False + + def _add_selection(self, object_id, selection): + if not selection.data: + return + + logging.debug('ClipboardTray: adding type %r', selection.type) + + cb_service = clipboard.get_instance() + if selection.type == 'text/uri-list': + uris = selection.data.split('\n') + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list' \ + ' still not supported.') + + cb_service.add_object_format(object_id, + selection.type, + uris[0], + on_disk=True) + else: + cb_service.add_object_format(object_id, + selection.type, + selection.data, + on_disk=False) + + def _object_added_cb(self, cb_service, cb_object): + if self._icons: + group = self._icons.values()[0] + else: + group = None + + icon = ClipboardIcon(cb_object, group) + self.add_item(icon) + icon.show() + self._icons[cb_object.get_id()] = icon + + objects_to_delete = self.get_children()[:-self.MAX_ITEMS] + for icon in objects_to_delete: + logging.debug('ClipboardTray: deleting surplus object') + cb_service = clipboard.get_instance() + cb_service.delete_object(icon.get_object_id()) + + logging.debug('ClipboardTray: %r was added', cb_object.get_id()) + + def _object_deleted_cb(self, cb_service, object_id): + icon = self._icons[object_id] + self.remove_item(icon) + del self._icons[object_id] + # select the last available icon + if self._icons: + last_icon = self.get_children()[-1] + last_icon.props.active = True + + logging.debug('ClipboardTray: %r was deleted', object_id) + + def drag_motion_cb(self, widget, context, x, y, time): + logging.debug('ClipboardTray._drag_motion_cb') + + if self._internal_drag(context): + context.drag_status(gtk.gdk.ACTION_MOVE, time) + else: + context.drag_status(gtk.gdk.ACTION_COPY, time) + self.props.drag_active = True + + return True + + def drag_leave_cb(self, widget, context, time): + self.props.drag_active = False + + def drag_drop_cb(self, widget, context, x, y, time): + logging.debug('ClipboardTray._drag_drop_cb') + + if self._internal_drag(context): + # TODO: We should move the object within the clipboard here + if not self._context_map.has_context(context): + context.drop_finish(False, gtk.get_current_event_time()) + return False + + cb_service = clipboard.get_instance() + object_id = cb_service.add_object(name="") + + self._context_map.add_context(context, object_id, len(context.targets)) + + if 'XdndDirectSave0' in context.targets: + window = context.source_window + prop_type, format_, filename = \ + window.property_get('XdndDirectSave0', 'text/plain') + + # FIXME query the clipboard service for a filename? + base_dir = tempfile.gettempdir() + dest_filename = util.unique_id() + + name_, dot, extension = filename.rpartition('.') + dest_filename += dot + extension + + dest_uri = 'file://' + os.path.join(base_dir, dest_filename) + + window.property_change('XdndDirectSave0', prop_type, format_, + gtk.gdk.PROP_MODE_REPLACE, dest_uri) + + widget.drag_get_data(context, 'XdndDirectSave0', time) + else: + for target in context.targets: + if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'): + widget.drag_get_data(context, target, time) + + cb_service.set_object_percent(object_id, percent=100) + + return True + + def drag_data_received_cb(self, widget, context, x, y, selection, + targetType, time): + logging.debug('ClipboardTray: got data for target %r', + selection.target) + + object_id = self._context_map.get_object_id(context) + try: + if selection is None: + logging.warn('ClipboardTray: empty selection for target %s', + selection.target) + elif selection.target == 'XdndDirectSave0': + if selection.data == 'S': + window = context.source_window + + prop_type, format_, dest = window.property_get( + 'XdndDirectSave0', 'text/plain') + + clipboardservice = clipboard.get_instance() + clipboardservice.add_object_format(object_id, + 'XdndDirectSave0', + dest, on_disk=True) + else: + self._add_selection(object_id, selection) + + finally: + # If it's the last target to be processed, finish + # the dnd transaction + if not self._context_map.has_context(context): + context.drop_finish(True, gtk.get_current_event_time()) + + def _internal_drag(self, context): + view_ancestor = context.get_source_widget().get_ancestor(gtk.Viewport) + if view_ancestor is self._viewport: + return True + else: + return False diff --git a/src/jarabe/frame/devicestray.py b/src/jarabe/frame/devicestray.py new file mode 100644 index 0000000..c5db639 --- /dev/null +++ b/src/jarabe/frame/devicestray.py @@ -0,0 +1,53 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +from sugar.graphics import tray + +from jarabe import config + + +class DevicesTray(tray.HTray): + def __init__(self): + tray.HTray.__init__(self, align=tray.ALIGN_TO_END) + + for f in os.listdir(os.path.join(config.ext_path, 'deviceicon')): + if f.endswith('.py') and not f.startswith('__'): + module_name = f[:-3] + try: + mod = __import__('deviceicon.' + module_name, globals(), + locals(), [module_name]) + mod.setup(self) + except Exception: + logging.exception('Exception while loading extension:') + + def add_device(self, view): + index = 0 + relative_index = getattr(view, 'FRAME_POSITION_RELATIVE', -1) + for item in self.get_children(): + current_relative_index = getattr(item, 'FRAME_POSITION_RELATIVE', + 0) + if current_relative_index >= relative_index: + index += 1 + else: + break + self.add_item(view, index=index) + view.show() + + def remove_device(self, view): + self.remove_item(view) diff --git a/src/jarabe/frame/eventarea.py b/src/jarabe/frame/eventarea.py new file mode 100644 index 0000000..1b5bf86 --- /dev/null +++ b/src/jarabe/frame/eventarea.py @@ -0,0 +1,153 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +import wnck +import gconf + + +_MAX_DELAY = 1000 + + +class EventArea(gobject.GObject): + __gsignals__ = { + 'enter': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'leave': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._windows = [] + self._hover = False + self._sids = {} + client = gconf.client_get_default() + self._edge_delay = client.get_int('/desktop/sugar/frame/edge_delay') + self._corner_delay = client.get_int('/desktop/sugar/frame' + '/corner_delay') + + right = gtk.gdk.screen_width() - 1 + bottom = gtk.gdk.screen_height() - 1 + width = gtk.gdk.screen_width() - 2 + height = gtk.gdk.screen_height() - 2 + + if self._edge_delay != _MAX_DELAY: + invisible = self._create_invisible(1, 0, width, 1, + self._edge_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(1, bottom, width, 1, + self._edge_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(0, 1, 1, height, + self._edge_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, 1, 1, height, + self._edge_delay) + self._windows.append(invisible) + + if self._corner_delay != _MAX_DELAY: + invisible = self._create_invisible(0, 0, 1, 1, + self._corner_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, 0, 1, 1, + self._corner_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(0, bottom, 1, 1, + self._corner_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, bottom, 1, 1, + self._corner_delay) + self._windows.append(invisible) + + screen = wnck.screen_get_default() + screen.connect('window-stacking-changed', + self._window_stacking_changed_cb) + + def _create_invisible(self, x, y, width, height, delay): + invisible = gtk.Invisible() + if delay >= 0: + invisible.connect('enter-notify-event', self._enter_notify_cb, + delay) + invisible.connect('leave-notify-event', self._leave_notify_cb) + + invisible.drag_dest_set(0, [], 0) + invisible.connect('drag_motion', self._drag_motion_cb) + invisible.connect('drag_leave', self._drag_leave_cb) + + invisible.realize() + # pylint: disable=E1101 + invisible.window.set_events(gtk.gdk.POINTER_MOTION_MASK | + gtk.gdk.ENTER_NOTIFY_MASK | + gtk.gdk.LEAVE_NOTIFY_MASK) + invisible.window.move_resize(x, y, width, height) + + return invisible + + def _notify_enter(self): + if not self._hover: + self._hover = True + self.emit('enter') + + def _notify_leave(self): + if self._hover: + self._hover = False + self.emit('leave') + + def _enter_notify_cb(self, widget, event, delay): + if widget in self._sids: + gobject.source_remove(self._sids[widget]) + self._sids[widget] = gobject.timeout_add(delay, + self.__delay_cb, + widget) + + def __delay_cb(self, widget): + del self._sids[widget] + self._notify_enter() + return False + + def _leave_notify_cb(self, widget, event): + if widget in self._sids: + gobject.source_remove(self._sids[widget]) + del self._sids[widget] + self._notify_leave() + + def _drag_motion_cb(self, widget, drag_context, x, y, timestamp): + drag_context.drag_status(0, timestamp) + self._notify_enter() + return True + + def _drag_leave_cb(self, widget, drag_context, timestamp): + self._notify_leave() + return True + + def show(self): + for window in self._windows: + window.show() + + def hide(self): + for window in self._windows: + window.hide() + + def _window_stacking_changed_cb(self, screen): + for window in self._windows: + window.window.raise_() diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py new file mode 100644 index 0000000..7407e18 --- /dev/null +++ b/src/jarabe/frame/frame.py @@ -0,0 +1,348 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gtk +import gobject + +from sugar.graphics import animator +from sugar.graphics import style +from sugar.graphics import palettegroup +from sugar import profile + +from jarabe.frame.eventarea import EventArea +from jarabe.frame.activitiestray import ActivitiesTray +from jarabe.frame.zoomtoolbar import ZoomToolbar +from jarabe.frame.friendstray import FriendsTray +from jarabe.frame.devicestray import DevicesTray +from jarabe.frame.framewindow import FrameWindow +from jarabe.frame.clipboardpanelwindow import ClipboardPanelWindow +from jarabe.frame.notification import NotificationIcon, NotificationWindow +from jarabe.model import notifications + + +TOP_RIGHT = 0 +TOP_LEFT = 1 +BOTTOM_RIGHT = 2 +BOTTOM_LEFT = 3 + +_FRAME_HIDING_DELAY = 500 +_NOTIFICATION_DURATION = 5000 + + +class _Animation(animator.Animation): + def __init__(self, frame, end): + start = frame.current_position + animator.Animation.__init__(self, start, end) + self._frame = frame + + def next_frame(self, current): + self._frame.move(current) + + +class _MouseListener(object): + def __init__(self, frame): + self._frame = frame + self._hide_sid = 0 + + def mouse_enter(self): + self._show_frame() + + def mouse_leave(self): + if self._frame.mode == Frame.MODE_MOUSE: + self._hide_frame() + + def _show_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._frame.show(Frame.MODE_MOUSE) + + def _hide_frame_timeout_cb(self): + self._frame.hide() + return False + + def _hide_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._hide_sid = gobject.timeout_add( + _FRAME_HIDING_DELAY, self._hide_frame_timeout_cb) + + +class _KeyListener(object): + def __init__(self, frame): + self._frame = frame + + def key_press(self): + if self._frame.visible: + if self._frame.mode == Frame.MODE_KEYBOARD: + self._frame.hide() + else: + self._frame.show(Frame.MODE_KEYBOARD) + + +class Frame(object): + MODE_MOUSE = 0 + MODE_KEYBOARD = 1 + MODE_NON_INTERACTIVE = 2 + + def __init__(self): + logging.debug('STARTUP: Loading the frame') + self.mode = None + + self._palette_group = palettegroup.get_group('frame') + self._palette_group.connect('popdown', self._palette_group_popdown_cb) + + self._left_panel = None + self._right_panel = None + self._top_panel = None + self._bottom_panel = None + + self.current_position = 0.0 + self._animator = None + + self._event_area = EventArea() + self._event_area.connect('enter', self._enter_corner_cb) + self._event_area.show() + + self._top_panel = self._create_top_panel() + self._bottom_panel = self._create_bottom_panel() + self._left_panel = self._create_left_panel() + self._right_panel = self._create_right_panel() + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + self._key_listener = _KeyListener(self) + self._mouse_listener = _MouseListener(self) + + self._notif_by_icon = {} + + notification_service = notifications.get_service() + notification_service.notification_received.connect( + self.__notification_received_cb) + notification_service.notification_cancelled.connect( + self.__notification_cancelled_cb) + + def is_visible(self): + return self.current_position != 0.0 + + visible = property(is_visible, None) + + def hide(self): + if self._animator: + self._animator.stop() + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 0.0)) + self._animator.start() + + self.mode = None + + def show(self, mode): + if self.visible: + return + if self._animator: + self._animator.stop() + + self.mode = mode + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 1.0)) + self._animator.start() + + def move(self, pos): + self.current_position = pos + self._update_position() + + def _is_hover(self): + return (self._top_panel.hover or \ + self._bottom_panel.hover or \ + self._left_panel.hover or \ + self._right_panel.hover) + + def _create_top_panel(self): + panel = self._create_panel(gtk.POS_TOP) + + zoom_toolbar = ZoomToolbar() + panel.append(zoom_toolbar, expand=False) + zoom_toolbar.show() + + activities_tray = ActivitiesTray() + panel.append(activities_tray) + activities_tray.show() + + return panel + + def _create_bottom_panel(self): + panel = self._create_panel(gtk.POS_BOTTOM) + + devices_tray = DevicesTray() + panel.append(devices_tray) + devices_tray.show() + + return panel + + def _create_right_panel(self): + panel = self._create_panel(gtk.POS_RIGHT) + + tray = FriendsTray() + panel.append(tray) + tray.show() + + return panel + + def _create_left_panel(self): + panel = ClipboardPanelWindow(self, gtk.POS_LEFT) + + self._connect_to_panel(panel) + panel.connect('drag-motion', self._drag_motion_cb) + panel.connect('drag-leave', self._drag_leave_cb) + + return panel + + def _create_panel(self, orientation): + panel = FrameWindow(orientation) + self._connect_to_panel(panel) + + return panel + + def _move_panel(self, panel, pos, x1, y1, x2, y2): + x = (x2 - x1) * pos + x1 + y = (y2 - y1) * pos + y1 + + panel.move(int(x), int(y)) + + # FIXME we should hide and show as necessary to free memory + if not panel.props.visible: + panel.show() + + def _connect_to_panel(self, panel): + panel.connect('enter-notify-event', self._enter_notify_cb) + panel.connect('leave-notify-event', self._leave_notify_cb) + + def _update_position(self): + screen_h = gtk.gdk.screen_height() + screen_w = gtk.gdk.screen_width() + + self._move_panel(self._top_panel, self.current_position, + 0, - self._top_panel.size, 0, 0) + + self._move_panel(self._bottom_panel, self.current_position, + 0, screen_h, 0, screen_h - self._bottom_panel.size) + + self._move_panel(self._left_panel, self.current_position, + - self._left_panel.size, 0, 0, 0) + + self._move_panel(self._right_panel, self.current_position, + screen_w, 0, screen_w - self._right_panel.size, 0) + + def _size_changed_cb(self, screen): + self._update_position() + + def _enter_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self._mouse_listener.mouse_enter() + + def _leave_notify_cb(self, window, event): + if event.detail == gtk.gdk.NOTIFY_INFERIOR: + return + + if not self._is_hover() and not self._palette_group.is_up(): + self._mouse_listener.mouse_leave() + + def _palette_group_popdown_cb(self, group): + if not self._is_hover(): + self._mouse_listener.mouse_leave() + + def _drag_motion_cb(self, window, context, x, y, time): + self._mouse_listener.mouse_enter() + + def _drag_leave_cb(self, window, drag_context, timestamp): + self._mouse_listener.mouse_leave() + + def _enter_corner_cb(self, event_area): + self._mouse_listener.mouse_enter() + + def notify_key_press(self): + self._key_listener.key_press() + + def add_notification(self, icon, corner=gtk.CORNER_TOP_LEFT, + duration=_NOTIFICATION_DURATION): + + if not isinstance(icon, NotificationIcon): + raise TypeError('icon must be a NotificationIcon.') + + window = NotificationWindow() + + screen = gtk.gdk.screen_get_default() + if corner == gtk.CORNER_TOP_LEFT: + window.move(0, 0) + elif corner == gtk.CORNER_TOP_RIGHT: + window.move(screen.get_width() - style.GRID_CELL_SIZE, 0) + elif corner == gtk.CORNER_BOTTOM_LEFT: + window.move(0, screen.get_height() - style.GRID_CELL_SIZE) + elif corner == gtk.CORNER_BOTTOM_RIGHT: + window.move(screen.get_width() - style.GRID_CELL_SIZE, + screen.get_height() - style.GRID_CELL_SIZE) + else: + raise ValueError('Inalid corner: %r' % corner) + + window.add(icon) + icon.show() + window.show() + + self._notif_by_icon[icon] = window + + gobject.timeout_add(duration, + lambda: self.remove_notification(icon)) + + def remove_notification(self, icon): + if icon not in self._notif_by_icon: + logging.debug('icon %r not in list of notifications.', icon) + return + + window = self._notif_by_icon[icon] + window.destroy() + del self._notif_by_icon[icon] + + def __notification_received_cb(self, **kwargs): + logging.debug('__notification_received_cb') + icon = NotificationIcon() + + hints = kwargs['hints'] + + icon_file_name = hints.get('x-sugar-icon-file-name', '') + if icon_file_name: + icon.props.icon_filename = icon_file_name + else: + icon.props.icon_name = 'application-octet-stream' + + icon_colors = hints.get('x-sugar-icon-colors', '') + if not icon_colors: + icon_colors = profile.get_color() + icon.props.xo_color = icon_colors + + duration = kwargs.get('expire_timeout', -1) + if duration == -1: + duration = _NOTIFICATION_DURATION + + self.add_notification(icon, gtk.CORNER_TOP_RIGHT, duration) + + def __notification_cancelled_cb(self, **kwargs): + # Do nothing for now. Our notification UI is so simple, there's no + # point yet. + pass diff --git a/src/jarabe/frame/frameinvoker.py b/src/jarabe/frame/frameinvoker.py new file mode 100644 index 0000000..a4abfa8 --- /dev/null +++ b/src/jarabe/frame/frameinvoker.py @@ -0,0 +1,38 @@ +# Copyright (C) 2007, Eduardo Silva +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk + +from sugar.graphics import style +from sugar.graphics.palette import WidgetInvoker + + +def _get_screen_area(): + frame_thickness = style.GRID_CELL_SIZE + + x = y = frame_thickness + width = gtk.gdk.screen_width() - frame_thickness + height = gtk.gdk.screen_height() - frame_thickness + + return gtk.gdk.Rectangle(x, y, width, height) + + +class FrameWidgetInvoker(WidgetInvoker): + def __init__(self, widget): + WidgetInvoker.__init__(self, widget, widget.child) + + self._position_hint = self.ANCHORED + self._screen_area = _get_screen_area() diff --git a/src/jarabe/frame/framewindow.py b/src/jarabe/frame/framewindow.py new file mode 100644 index 0000000..394ba00 --- /dev/null +++ b/src/jarabe/frame/framewindow.py @@ -0,0 +1,153 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +from gtk import gdk +import gobject + +from sugar.graphics import style + + +class FrameContainer(gtk.Bin): + """A container class for frame panel rendering. Hosts a child 'box' where + frame elements can be added. Excludes grid-sized squares at each end + of the frame panel, and a space alongside the inside of the screen where + a border is drawn.""" + + __gtype_name__ = 'SugarFrameContainer' + + def __init__(self, position): + gtk.Bin.__init__(self) + self._position = position + + if self.is_vertical(): + box = gtk.VBox() + else: + box = gtk.HBox() + self.add(box) + box.show() + + def is_vertical(self): + return self._position in (gtk.POS_LEFT, gtk.POS_RIGHT) + + def do_expose_event(self, event): + # Draw the inner border as a rectangle + cr = self.get_parent_window().cairo_create() + r, g, b, a = style.COLOR_BUTTON_GREY.get_rgba() + cr.set_source_rgba (r, g, b, a) + + if self.is_vertical(): + x = style.GRID_CELL_SIZE if self._position == gtk.POS_LEFT else 0 + y = style.GRID_CELL_SIZE + width = style.LINE_WIDTH + height = self.allocation.height - (style.GRID_CELL_SIZE * 2) + else: + x = style.GRID_CELL_SIZE + y = style.GRID_CELL_SIZE if self._position == gtk.POS_TOP else 0 + height = style.LINE_WIDTH + width = self.allocation.width - (style.GRID_CELL_SIZE * 2) + + cr.rectangle(x, y, width, height) + cr.fill() + + gtk.Bin.do_expose_event(self, event) + return False + + def do_size_request(self, req): + if self.is_vertical(): + req.height = gdk.screen_height() + req.width = style.GRID_CELL_SIZE + style.LINE_WIDTH + else: + req.width = gdk.screen_width() + req.height = style.GRID_CELL_SIZE + style.LINE_WIDTH + + self.get_child().size_request() + + def do_size_allocate(self, allocation): + self.allocation = allocation + + # exclude grid squares at two ends of the frame + # allocate remaining space to child box, minus the space needed for + # drawing the border + allocation = gdk.Rectangle() + if self.is_vertical(): + allocation.x = 0 if self._position == gtk.POS_LEFT \ + else style.LINE_WIDTH + allocation.y = style.GRID_CELL_SIZE + allocation.width = self.allocation.width - style.LINE_WIDTH + allocation.height = self.allocation.height \ + - (style.GRID_CELL_SIZE * 2) + else: + allocation.x = style.GRID_CELL_SIZE + allocation.y = 0 if self._position == gtk.POS_TOP \ + else style.LINE_WIDTH + allocation.width = self.allocation.width \ + - (style.GRID_CELL_SIZE * 2) + allocation.height = self.allocation.height - style.LINE_WIDTH + + self.get_child().size_allocate(allocation) + + +class FrameWindow(gtk.Window): + __gtype_name__ = 'SugarFrameWindow' + + def __init__(self, position): + gtk.Window.__init__(self) + self.hover = False + self.size = style.GRID_CELL_SIZE + style.LINE_WIDTH + + accel_group = gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.add_accel_group(accel_group) + + self._position = position + + self.set_decorated(False) + self.connect('realize', self._realize_cb) + self.connect('enter-notify-event', self._enter_notify_cb) + self.connect('leave-notify-event', self._leave_notify_cb) + + self._container = FrameContainer(position) + self.add(self._container) + self._container.show() + self._update_size() + + screen = gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + def append(self, child, expand=True, fill=True): + self._container.get_child().pack_start(child, expand=expand, fill=fill) + + def _update_size(self): + if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM: + self.resize(gdk.screen_width(), self.size) + else: + self.resize(self.size, gdk.screen_height()) + + def _realize_cb(self, widget): + self.window.set_type_hint(gdk.WINDOW_TYPE_HINT_DOCK) + self.window.set_accept_focus(False) + + def _enter_notify_cb(self, window, event): + if event.detail != gdk.NOTIFY_INFERIOR: + self.hover = True + + def _leave_notify_cb(self, window, event): + if event.detail != gdk.NOTIFY_INFERIOR: + self.hover = False + + def _size_changed_cb(self, screen): + self._update_size() diff --git a/src/jarabe/frame/friendstray.py b/src/jarabe/frame/friendstray.py new file mode 100644 index 0000000..26a279b --- /dev/null +++ b/src/jarabe/frame/friendstray.py @@ -0,0 +1,129 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +from sugar.graphics.tray import VTray, TrayIcon + +from jarabe.view.buddymenu import BuddyMenu +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.model import shell +from jarabe.model.buddy import get_owner_instance +from jarabe.model import neighborhood + + +class FriendIcon(TrayIcon): + def __init__(self, buddy): + TrayIcon.__init__(self, icon_name='computer-xo', + xo_color=buddy.get_color()) + + self._buddy = buddy + self.set_palette_invoker(FrameWidgetInvoker(self)) + self.palette_invoker.cache_palette = False + + def create_palette(self): + palette = BuddyMenu(self._buddy) + palette.props.icon_visible = False + palette.set_group_id('frame') + return palette + + +class FriendsTray(VTray): + def __init__(self): + VTray.__init__(self) + + self._shared_activity = None + self._buddies = {} + + shell.get_model().connect('active-activity-changed', + self.__active_activity_changed_cb) + + neighborhood.get_model().connect('activity-added', + self.__neighborhood_activity_added_cb) + + def add_buddy(self, buddy): + if buddy.props.key in self._buddies: + return + + icon = FriendIcon(buddy) + self.add_item(icon) + icon.show() + + self._buddies[buddy.props.key] = icon + + def remove_buddy(self, buddy): + if buddy.props.key not in self._buddies: + return + + self.remove_item(self._buddies[buddy.props.key]) + del self._buddies[buddy.props.key] + + def clear(self): + for item in self.get_children(): + self.remove_item(item) + item.destroy() + self._buddies = {} + + def __neighborhood_activity_added_cb(self, neighborhood_model, + shared_activity): + logging.debug('FriendsTray.__neighborhood_activity_added_cb') + active_activity = shell.get_model().get_active_activity() + if active_activity.get_activity_id() != shared_activity.activity_id: + return + + self.clear() + + # always display ourselves + self.add_buddy(get_owner_instance()) + + self._set_current_activity(shared_activity.activity_id) + + def __active_activity_changed_cb(self, home_model, home_activity): + logging.debug('FriendsTray.__active_activity_changed_cb') + self.clear() + + # always display ourselves + self.add_buddy(get_owner_instance()) + + if home_activity is None: + return + + activity_id = home_activity.get_activity_id() + if activity_id is None: + return + + self._set_current_activity(activity_id) + + def _set_current_activity(self, activity_id): + logging.debug('FriendsTray._set_current_activity') + neighborhood_model = neighborhood.get_model() + self._shared_activity = neighborhood_model.get_activity(activity_id) + if self._shared_activity is None: + return + + for buddy in self._shared_activity.get_buddies(): + self.add_buddy(buddy) + + self._shared_activity.connect('buddy-added', self.__buddy_added_cb) + self._shared_activity.connect('buddy-removed', self.__buddy_removed_cb) + + def __buddy_added_cb(self, activity, buddy): + logging.debug('FriendsTray.__buddy_added_cb') + self.add_buddy(buddy) + + def __buddy_removed_cb(self, activity, buddy): + logging.debug('FriendsTray.__buddy_removed_cb') + self.remove_buddy(buddy) diff --git a/src/jarabe/frame/notification.py b/src/jarabe/frame/notification.py new file mode 100644 index 0000000..3471e2c --- /dev/null +++ b/src/jarabe/frame/notification.py @@ -0,0 +1,102 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk + +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + +from jarabe.view.pulsingicon import PulsingIcon + + +class NotificationIcon(gtk.EventBox): + __gtype_name__ = 'SugarNotificationIcon' + + __gproperties__ = { + 'xo-color': (object, None, None, gobject.PARAM_READWRITE), + 'icon-name': (str, None, None, None, gobject.PARAM_READWRITE), + 'icon-filename': (str, None, None, None, gobject.PARAM_READWRITE), + } + + _PULSE_TIMEOUT = 3 + + def __init__(self, **kwargs): + self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE) + gobject.GObject.__init__(self, **kwargs) + self.props.visible_window = False + + self._icon.props.pulse_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self._icon.props.pulsing = True + self.add(self._icon) + self._icon.show() + + gobject.timeout_add_seconds(self._PULSE_TIMEOUT, + self.__stop_pulsing_cb) + + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + + def __stop_pulsing_cb(self): + self._icon.props.pulsing = False + return False + + def do_set_property(self, pspec, value): + if pspec.name == 'xo-color': + if self._icon.props.base_color != value: + self._icon.props.base_color = value + elif pspec.name == 'icon-name': + if self._icon.props.icon_name != value: + self._icon.props.icon_name = value + elif pspec.name == 'icon-filename': + if self._icon.props.file != value: + self._icon.props.file = value + + def do_get_property(self, pspec): + if pspec.name == 'xo-color': + return self._icon.props.base_color + elif pspec.name == 'icon-name': + return self._icon.props.icon_name + elif pspec.name == 'icon-filename': + return self._icon.props.file + + def _set_palette(self, palette): + self._icon.palette = palette + + def _get_palette(self): + return self._icon.palette + + palette = property(_get_palette, _set_palette) + + +class NotificationWindow(gtk.Window): + __gtype_name__ = 'SugarNotificationWindow' + + def __init__(self, **kwargs): + + gtk.Window.__init__(self, **kwargs) + + self.set_decorated(False) + self.set_resizable(False) + self.connect('realize', self._realize_cb) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + + color = gtk.gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html()) + self.modify_bg(gtk.STATE_NORMAL, color) diff --git a/src/jarabe/frame/zoomtoolbar.py b/src/jarabe/frame/zoomtoolbar.py new file mode 100644 index 0000000..c28fe1c --- /dev/null +++ b/src/jarabe/frame/zoomtoolbar.py @@ -0,0 +1,94 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2009 Simon Schampijer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging + +import glib +import gtk + +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.graphics.radiotoolbutton import RadioToolButton + +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.model import shell + + +class ZoomToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + + # we shouldn't be mirrored in RTL locales + self.set_direction(gtk.TEXT_DIR_LTR) + + # ask not to be collapsed if possible + self.set_size_request(4 * style.GRID_CELL_SIZE, -1) + + self._mesh_button = self._add_button('zoom-neighborhood', + _('Neighborhood'), _('F1'), shell.ShellModel.ZOOM_MESH) + self._groups_button = self._add_button('zoom-groups', + _('Group'), _('F2'), shell.ShellModel.ZOOM_GROUP) + self._home_button = self._add_button('zoom-home', + _('Home'), _('F3'), shell.ShellModel.ZOOM_HOME) + self._activity_button = self._add_button('zoom-activity', + _('Activity'), _('F4'), shell.ShellModel.ZOOM_ACTIVITY) + + shell_model = shell.get_model() + self._set_zoom_level(shell_model.zoom_level) + shell_model.zoom_level_changed.connect(self.__zoom_level_changed_cb) + + def _add_button(self, icon_name, label, accelerator, zoom_level): + if self.get_children(): + group = self.get_children()[0] + else: + group = None + + button = RadioToolButton(named_icon=icon_name, group=group, + accelerator=accelerator) + button.connect('clicked', self.__level_clicked_cb, zoom_level) + self.add(button) + button.show() + + palette = Palette(glib.markup_escape_text(label)) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + return button + + def __level_clicked_cb(self, button, level): + if not button.get_active(): + return + + shell.get_model().set_zoom_level(level) + + def __zoom_level_changed_cb(self, **kwargs): + self._set_zoom_level(kwargs['new_level']) + + def _set_zoom_level(self, new_level): + logging.debug('new zoom level: %r', new_level) + if new_level == shell.ShellModel.ZOOM_MESH: + self._mesh_button.props.active = True + elif new_level == shell.ShellModel.ZOOM_GROUP: + self._groups_button.props.active = True + elif new_level == shell.ShellModel.ZOOM_HOME: + self._home_button.props.active = True + elif new_level == shell.ShellModel.ZOOM_ACTIVITY: + self._activity_button.props.active = True + else: + raise ValueError('Invalid zoom level: %r' % (new_level)) diff --git a/src/jarabe/intro/Makefile.am b/src/jarabe/intro/Makefile.am new file mode 100644 index 0000000..2ea7cea --- /dev/null +++ b/src/jarabe/intro/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pythondir)/jarabe/intro +sugar_PYTHON = \ + __init__.py \ + colorpicker.py \ + window.py diff --git a/src/jarabe/intro/Makefile.in b/src/jarabe/intro/Makefile.in new file mode 100644 index 0000000..ef8a4c6 --- /dev/null +++ b/src/jarabe/intro/Makefile.in @@ -0,0 +1,442 @@ +# 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/intro +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/intro +sugar_PYTHON = \ + __init__.py \ + colorpicker.py \ + window.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/intro/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/intro/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/intro/__init__.py b/src/jarabe/intro/__init__.py new file mode 100644 index 0000000..d2932f1 --- /dev/null +++ b/src/jarabe/intro/__init__.py @@ -0,0 +1,26 @@ +import os + +import gtk + +from sugar import env +from sugar.profile import get_profile + +from jarabe.intro.window import IntroWindow +from jarabe.intro.window import create_profile + + +def check_profile(): + profile = get_profile() + + path = os.path.join(os.path.expanduser('~/.sugar'), 'debug') + if not os.path.exists(path): + profile.create_debug_file() + + path = os.path.join(env.get_profile_path(), 'config') + if os.path.exists(path): + profile.convert_profile() + + if not profile.is_valid(): + win = IntroWindow() + win.show_all() + gtk.main() diff --git a/src/jarabe/intro/colorpicker.py b/src/jarabe/intro/colorpicker.py new file mode 100644 index 0000000..75c15c1 --- /dev/null +++ b/src/jarabe/intro/colorpicker.py @@ -0,0 +1,44 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk + +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + + +class ColorPicker(gtk.EventBox): + def __init__(self): + gtk.EventBox.__init__(self) + self._xo_color = None + + self._xo = Icon(pixel_size=style.XLARGE_ICON_SIZE, + icon_name='computer-xo') + self._set_random_colors() + self.connect('button-press-event', self._button_press_cb) + self.add(self._xo) + + def _button_press_cb(self, widget, event): + if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS: + self._set_random_colors() + + def get_color(self): + return self._xo_color + + def _set_random_colors(self): + self._xo_color = XoColor() + self._xo.props.xo_color = self._xo_color diff --git a/src/jarabe/intro/window.py b/src/jarabe/intro/window.py new file mode 100644 index 0000000..a6a2a29 --- /dev/null +++ b/src/jarabe/intro/window.py @@ -0,0 +1,299 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import os.path +import logging +from gettext import gettext as _ +import gconf +import pwd + +import gtk +import gobject + +from sugar import env +from sugar import profile +from sugar.graphics import style +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor + +from jarabe.intro import colorpicker + + +_BACKGROUND_COLOR = style.COLOR_WHITE + + +def create_profile(name, color=None): + if not color: + color = XoColor() + + client = gconf.client_get_default() + client.set_string('/desktop/sugar/user/nick', name) + client.set_string('/desktop/sugar/user/color', color.to_string()) + client.suggest_sync() + + if profile.get_pubkey() and profile.get_profile().privkey_hash: + logging.info('Valid key pair found, skipping generation.') + return + + # Generate keypair + import commands + keypath = os.path.join(env.get_profile_path(), 'owner.key') + if os.path.exists(keypath): + os.rename(keypath, keypath + '.broken') + logging.warning('Existing private key %s moved to %s.broken', + keypath, keypath) + + if os.path.exists(keypath + '.pub'): + os.rename(keypath + '.pub', keypath + '.pub.broken') + logging.warning('Existing public key %s.pub moved to %s.pub.broken', + keypath, keypath) + + cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % (keypath, ) + (s, o) = commands.getstatusoutput(cmd) + if s != 0: + logging.error('Could not generate key pair: %d %s', s, o) + + +class _Page(gtk.VBox): + __gproperties__ = { + 'valid': (bool, None, None, False, gobject.PARAM_READABLE), + } + + def __init__(self): + gtk.VBox.__init__(self) + self.valid = False + + def set_valid(self, valid): + self.valid = valid + self.notify('valid') + + def do_get_property(self, pspec): + if pspec.name == 'valid': + return self.valid + + def activate(self): + pass + + +class _NamePage(_Page): + def __init__(self, intro): + _Page.__init__(self) + self._intro = intro + + alignment = gtk.Alignment(0.5, 0.5, 0, 0) + self.pack_start(alignment, expand=True, fill=True) + + hbox = gtk.HBox(spacing=style.DEFAULT_SPACING) + alignment.add(hbox) + + label = gtk.Label(_('Name:')) + hbox.pack_start(label, expand=False) + + self._entry = gtk.Entry() + self._entry.connect('notify::text', self._text_changed_cb) + self._entry.set_size_request(style.zoom(300), -1) + self._entry.set_max_length(45) + hbox.pack_start(self._entry, expand=False) + + def _text_changed_cb(self, entry, pspec): + valid = len(entry.props.text.strip()) > 0 + self.set_valid(valid) + + def get_name(self): + return self._entry.props.text + + def set_name(self, new_name): + self._entry.props.text = new_name + + def activate(self): + self._entry.grab_focus() + + +class _ColorPage(_Page): + def __init__(self): + _Page.__init__(self) + + vbox = gtk.VBox(spacing=style.DEFAULT_SPACING) + self.pack_start(vbox, expand=True, fill=False) + + self._label = gtk.Label(_('Click to change color:')) + vbox.pack_start(self._label) + + self._cp = colorpicker.ColorPicker() + vbox.pack_start(self._cp) + + self._color = self._cp.get_color() + self.set_valid(True) + + def get_color(self): + return self._cp.get_color() + + +class _IntroBox(gtk.VBox): + __gsignals__ = { + 'done': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + } + + PAGE_NAME = 0 + PAGE_COLOR = 1 + + PAGE_FIRST = PAGE_NAME + PAGE_LAST = PAGE_COLOR + + def __init__(self): + gtk.VBox.__init__(self) + self.set_border_width(style.zoom(30)) + + self._page = self.PAGE_NAME + self._name_page = _NamePage(self) + self._color_page = _ColorPage() + self._current_page = None + self._next_button = None + + client = gconf.client_get_default() + default_nick = client.get_string('/desktop/sugar/user/default_nick') + if default_nick != 'disabled': + self._page = self.PAGE_COLOR + if default_nick == 'system': + pwd_entry = pwd.getpwuid(os.getuid()) + default_nick = (pwd_entry.pw_gecos.split(',')[0] or + pwd_entry.pw_name) + self._name_page.set_name(default_nick) + + self._setup_page() + + def _setup_page(self): + for child in self.get_children(): + self.remove(child) + + if self._page == self.PAGE_NAME: + self._current_page = self._name_page + elif self._page == self.PAGE_COLOR: + self._current_page = self._color_page + + self.pack_start(self._current_page, expand=True) + + button_box = gtk.HButtonBox() + + if self._page == self.PAGE_FIRST: + button_box.set_layout(gtk.BUTTONBOX_END) + else: + button_box.set_layout(gtk.BUTTONBOX_EDGE) + back_button = gtk.Button(_('Back')) + image = Icon(icon_name='go-left') + back_button.set_image(image) + back_button.connect('clicked', self._back_activated_cb) + button_box.pack_start(back_button) + + self._next_button = gtk.Button() + image = Icon(icon_name='go-right') + self._next_button.set_image(image) + + if self._page == self.PAGE_LAST: + self._next_button.set_label(_('Done')) + self._next_button.connect('clicked', self._done_activated_cb) + else: + self._next_button.set_label(_('Next')) + self._next_button.connect('clicked', self._next_activated_cb) + + self._current_page.activate() + + self._update_next_button() + button_box.pack_start(self._next_button) + + self._current_page.connect('notify::valid', + self._page_valid_changed_cb) + + self.pack_start(button_box, expand=False) + self.show_all() + + def _update_next_button(self): + self._next_button.set_sensitive(self._current_page.props.valid) + + def _page_valid_changed_cb(self, page, pspec): + self._update_next_button() + + def _back_activated_cb(self, widget): + self.back() + + def back(self): + if self._page != self.PAGE_FIRST: + self._page -= 1 + self._setup_page() + + def _next_activated_cb(self, widget): + self.next() + + def next(self): + if self._page == self.PAGE_LAST: + self.done() + if self._current_page.props.valid: + self._page += 1 + self._setup_page() + + def _done_activated_cb(self, widget): + self.done() + + def done(self): + name = self._name_page.get_name() + color = self._color_page.get_color() + + self.emit('done', name, color) + + +class IntroWindow(gtk.Window): + __gtype_name__ = 'SugarIntroWindow' + + def __init__(self): + gtk.Window.__init__(self) + + self.props.decorated = False + self.maximize() + + self._intro_box = _IntroBox() + self._intro_box.connect('done', self._done_cb) + + self.add(self._intro_box) + self._intro_box.show() + self.connect('key-press-event', self.__key_press_cb) + + def _done_cb(self, box, name, color): + self.hide() + gobject.idle_add(self._create_profile_cb, name, color) + + def _create_profile_cb(self, name, color): + create_profile(name, color) + gtk.main_quit() + + return False + + def __key_press_cb(self, widget, event): + if gtk.gdk.keyval_name(event.keyval) == 'Return': + self._intro_box.next() + return True + elif gtk.gdk.keyval_name(event.keyval) == 'Escape': + self._intro_box.back() + return True + return False + + +if __name__ == '__main__': + w = IntroWindow() + w.show() + w.connect('destroy', gtk.main_quit) + gtk.main() diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am new file mode 100644 index 0000000..ba29062 --- /dev/null +++ b/src/jarabe/journal/Makefile.am @@ -0,0 +1,18 @@ +sugardir = $(pythondir)/jarabe/journal +sugar_PYTHON = \ + __init__.py \ + detailview.py \ + expandedentry.py \ + journalactivity.py \ + journalentrybundle.py \ + journaltoolbox.py \ + journalwindow.py \ + keepicon.py \ + listmodel.py \ + listview.py \ + misc.py \ + modalalert.py \ + model.py \ + objectchooser.py \ + palettes.py \ + volumestoolbar.py diff --git a/src/jarabe/journal/Makefile.in b/src/jarabe/journal/Makefile.in new file mode 100644 index 0000000..8d51c6b --- /dev/null +++ b/src/jarabe/journal/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/journal +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/journal +sugar_PYTHON = \ + __init__.py \ + detailview.py \ + expandedentry.py \ + journalactivity.py \ + journalentrybundle.py \ + journaltoolbox.py \ + journalwindow.py \ + keepicon.py \ + listmodel.py \ + listview.py \ + misc.py \ + modalalert.py \ + model.py \ + objectchooser.py \ + palettes.py \ + volumestoolbar.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/journal/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/journal/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/journal/__init__.py b/src/jarabe/journal/__init__.py new file mode 100644 index 0000000..6373228 --- /dev/null +++ b/src/jarabe/journal/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/jarabe/journal/detailview.py b/src/jarabe/journal/detailview.py new file mode 100644 index 0000000..aa8c039 --- /dev/null +++ b/src/jarabe/journal/detailview.py @@ -0,0 +1,119 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ + +import gobject +import gtk +import hippo + +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon + +from jarabe.journal.expandedentry import ExpandedEntry +from jarabe.journal import model + + +class DetailView(gtk.VBox): + __gtype_name__ = 'DetailView' + + __gsignals__ = { + 'go-back-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, **kwargs): + self._metadata = None + self._expanded_entry = None + + canvas = hippo.Canvas() + + self._root = hippo.CanvasBox() + self._root.props.background_color = style.COLOR_PANEL_GREY.get_int() + canvas.set_root(self._root) + + back_bar = BackBar() + back_bar.connect('button-release-event', + self.__back_bar_release_event_cb) + self._root.append(back_bar) + + gobject.GObject.__init__(self, **kwargs) + + self.pack_start(canvas) + canvas.show() + + def _fav_icon_activated_cb(self, fav_icon): + keep = not self._expanded_entry.get_keep() + self._expanded_entry.set_keep(keep) + fav_icon.props.keep = keep + + def __back_bar_release_event_cb(self, back_bar, event): + self.emit('go-back-clicked') + return False + + def _update_view(self): + if self._expanded_entry is None: + self._expanded_entry = ExpandedEntry() + self._root.append(self._expanded_entry, hippo.PACK_EXPAND) + self._expanded_entry.set_metadata(self._metadata) + + def refresh(self): + logging.debug('DetailView.refresh') + self._metadata = model.get(self._metadata['uid']) + self._update_view() + + def get_metadata(self): + return self._metadata + + def set_metadata(self, metadata): + self._metadata = metadata + self._update_view() + + metadata = gobject.property( + type=object, getter=get_metadata, setter=set_metadata) + + +class BackBar(hippo.CanvasBox): + def __init__(self): + hippo.CanvasBox.__init__(self, + orientation=hippo.ORIENTATION_HORIZONTAL, + border=style.LINE_WIDTH, + background_color=style.COLOR_PANEL_GREY.get_int(), + border_color=style.COLOR_SELECTION_GREY.get_int(), + padding=style.DEFAULT_PADDING, + padding_left=style.DEFAULT_SPACING, + spacing=style.DEFAULT_SPACING) + + icon = CanvasIcon(icon_name='go-previous', + size=style.SMALL_ICON_SIZE, + fill_color=style.COLOR_TOOLBAR_GREY.get_svg()) + self.append(icon) + + label = hippo.CanvasText(text=_('Back'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + self.append(label) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + self.reverse() + + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + def __motion_notify_event_cb(self, box, event): + if event.detail == hippo.MOTION_DETAIL_ENTER: + box.props.background_color = style.COLOR_SELECTION_GREY.get_int() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + box.props.background_color = style.COLOR_PANEL_GREY.get_int() + return False diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py new file mode 100644 index 0000000..03f8cd1 --- /dev/null +++ b/src/jarabe/journal/expandedentry.py @@ -0,0 +1,440 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import StringIO +import time +import os + +import hippo +import cairo +import gobject +import glib +import gtk +import simplejson + +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.xocolor import XoColor +from sugar.graphics.canvastextview import CanvasTextView +from sugar.util import format_size + +from jarabe.journal.keepicon import KeepIcon +from jarabe.journal.palettes import ObjectPalette, BuddyPalette +from jarabe.journal import misc +from jarabe.journal import model + + +class Separator(hippo.CanvasBox, hippo.CanvasItem): + def __init__(self, orientation): + hippo.CanvasBox.__init__(self, + background_color=style.COLOR_PANEL_GREY.get_int()) + + if orientation == hippo.ORIENTATION_VERTICAL: + self.props.box_width = style.LINE_WIDTH + else: + self.props.box_height = style.LINE_WIDTH + + +class BuddyList(hippo.CanvasBox): + def __init__(self, buddies): + hippo.CanvasBox.__init__(self, xalign=hippo.ALIGNMENT_START, + orientation=hippo.ORIENTATION_HORIZONTAL) + + for buddy in buddies: + nick_, color = buddy + hbox = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL) + icon = CanvasIcon(icon_name='computer-xo', + xo_color=XoColor(color), + size=style.STANDARD_ICON_SIZE) + icon.set_palette(BuddyPalette(buddy)) + hbox.append(icon) + self.append(hbox) + + +class ExpandedEntry(hippo.CanvasBox): + def __init__(self): + hippo.CanvasBox.__init__(self) + self.props.orientation = hippo.ORIENTATION_VERTICAL + self.props.background_color = style.COLOR_WHITE.get_int() + self.props.padding_top = style.DEFAULT_SPACING * 3 + + self._metadata = None + self._update_title_sid = None + + # Create header + header = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + padding=style.DEFAULT_PADDING, + padding_right=style.GRID_CELL_SIZE, + spacing=style.DEFAULT_SPACING) + self.append(header) + + # Create two column body + + body = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + spacing=style.DEFAULT_SPACING * 3, + padding_left=style.GRID_CELL_SIZE, + padding_right=style.GRID_CELL_SIZE, + padding_top=style.DEFAULT_SPACING * 3) + + self.append(body, hippo.PACK_EXPAND) + + first_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + spacing=style.DEFAULT_SPACING) + body.append(first_column) + + second_column = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + spacing=style.DEFAULT_SPACING) + body.append(second_column, hippo.PACK_EXPAND) + + # Header + + self._keep_icon = self._create_keep_icon() + header.append(self._keep_icon) + + self._icon = None + self._icon_box = hippo.CanvasBox() + header.append(self._icon_box) + + self._title = self._create_title() + header.append(self._title, hippo.PACK_EXPAND) + + # TODO: create a version list popup instead of a date label + self._date = self._create_date() + header.append(self._date) + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + header.reverse() + + # First column + + self._preview_box = hippo.CanvasBox() + first_column.append(self._preview_box) + + self._technical_box = hippo.CanvasBox() + first_column.append(self._technical_box) + + # Second column + + description_box, self._description = self._create_description() + second_column.append(description_box) + + tags_box, self._tags = self._create_tags() + second_column.append(tags_box) + + self._buddy_list = hippo.CanvasBox() + second_column.append(self._buddy_list) + + def set_metadata(self, metadata): + if self._metadata == metadata: + return + self._metadata = metadata + + self._keep_icon.keep = (str(metadata.get('keep', 0)) == '1') + + self._icon = self._create_icon() + self._icon_box.clear() + self._icon_box.append(self._icon) + + self._date.props.text = misc.get_date(metadata) + + title = self._title.props.widget + title.props.text = metadata.get('title', _('Untitled')) + title.props.editable = model.is_editable(metadata) + + self._preview_box.clear() + self._preview_box.append(self._create_preview()) + + self._technical_box.clear() + self._technical_box.append(self._create_technical()) + + self._buddy_list.clear() + self._buddy_list.append(self._create_buddy_list()) + + description = self._description.text_view_widget + description.props.buffer.props.text = metadata.get('description', '') + description.props.editable = model.is_editable(metadata) + + tags = self._tags.text_view_widget + tags.props.buffer.props.text = metadata.get('tags', '') + tags.props.editable = model.is_editable(metadata) + + def _create_keep_icon(self): + keep_icon = KeepIcon(False) + keep_icon.connect('activated', self._keep_icon_activated_cb) + return keep_icon + + def _create_icon(self): + icon = CanvasIcon(file_name=misc.get_icon_name(self._metadata)) + icon.connect_after('button-release-event', + self._icon_button_release_event_cb) + + if misc.is_activity_bundle(self._metadata): + xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + else: + xo_color = misc.get_icon_color(self._metadata) + icon.props.xo_color = xo_color + + icon.set_palette(ObjectPalette(self._metadata)) + + return icon + + def _create_title(self): + entry = gtk.Entry() + entry.connect('focus-out-event', self._title_focus_out_event_cb) + + bg_color = style.COLOR_WHITE.get_gdk_color() + entry.modify_bg(gtk.STATE_INSENSITIVE, bg_color) + entry.modify_base(gtk.STATE_INSENSITIVE, bg_color) + + return hippo.CanvasWidget(widget=entry) + + def _create_date(self): + date = hippo.CanvasText(xalign=hippo.ALIGNMENT_START, + font_desc=style.FONT_NORMAL.get_pango_desc()) + return date + + def _create_preview(self): + width = style.zoom(320) + height = style.zoom(240) + box = hippo.CanvasBox() + + if len(self._metadata.get('preview', '')) > 4: + if self._metadata['preview'][1:4] == 'PNG': + preview_data = self._metadata['preview'] + else: + # TODO: We are close to be able to drop this. + import base64 + preview_data = base64.b64decode( + self._metadata['preview']) + + png_file = StringIO.StringIO(preview_data) + try: + surface = cairo.ImageSurface.create_from_png(png_file) + has_preview = True + except Exception: + logging.exception('Error while loading the preview') + has_preview = False + else: + has_preview = False + + if has_preview: + preview_box = hippo.CanvasImage(image=surface, + border=style.LINE_WIDTH, + border_color=style.COLOR_BUTTON_GREY.get_int(), + xalign=hippo.ALIGNMENT_CENTER, + yalign=hippo.ALIGNMENT_CENTER, + scale_width=width, + scale_height=height) + else: + preview_box = hippo.CanvasText(text=_('No preview'), + font_desc=style.FONT_NORMAL.get_pango_desc(), + xalign=hippo.ALIGNMENT_CENTER, + yalign=hippo.ALIGNMENT_CENTER, + border=style.LINE_WIDTH, + border_color=style.COLOR_BUTTON_GREY.get_int(), + color=style.COLOR_BUTTON_GREY.get_int(), + box_width=width, + box_height=height) + preview_box.connect_after('button-release-event', + self._preview_box_button_release_event_cb) + box.append(preview_box) + return box + + def _create_technical(self): + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + lines = [ + _('Kind: %s') % (self._metadata.get('mime_type') or _('Unknown'),), + _('Date: %s') % (self._format_date(),), + _('Size: %s') % (format_size(int(self._metadata.get('filesize', + model.get_file_size(self._metadata['uid']))))), + ] + + for line in lines: + text = hippo.CanvasText(text=line, + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + return vbox + + def _format_date(self): + if 'timestamp' in self._metadata: + try: + timestamp = float(self._metadata['timestamp']) + except (ValueError, TypeError): + logging.warning('Invalid timestamp for %r: %r', + self._metadata['uid'], + self._metadata['timestamp']) + else: + return time.strftime('%x', time.localtime(timestamp)) + return _('No date') + + def _create_buddy_list(self): + + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=_('Participants:'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + if self._metadata.get('buddies'): + buddies = simplejson.loads(self._metadata['buddies']).values() + vbox.append(BuddyList(buddies)) + return vbox + else: + return vbox + + def _create_description(self): + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=_('Description:'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + text_view = CanvasTextView('', + box_height=style.GRID_CELL_SIZE * 2) + vbox.append(text_view, hippo.PACK_EXPAND) + + text_view.text_view_widget.props.accepts_tab = False + text_view.text_view_widget.connect('focus-out-event', + self._description_focus_out_event_cb) + + return vbox, text_view + + def _create_tags(self): + vbox = hippo.CanvasBox() + vbox.props.spacing = style.DEFAULT_SPACING + + text = hippo.CanvasText(text=_('Tags:'), + font_desc=style.FONT_NORMAL.get_pango_desc()) + text.props.color = style.COLOR_BUTTON_GREY.get_int() + + if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: + text.props.xalign = hippo.ALIGNMENT_END + else: + text.props.xalign = hippo.ALIGNMENT_START + + vbox.append(text) + + text_view = CanvasTextView('', + box_height=style.GRID_CELL_SIZE * 2) + vbox.append(text_view, hippo.PACK_EXPAND) + + text_view.text_view_widget.props.accepts_tab = False + text_view.text_view_widget.connect('focus-out-event', + self._tags_focus_out_event_cb) + + return vbox, text_view + + def _title_notify_text_cb(self, entry, pspec): + if not self._update_title_sid: + self._update_title_sid = gobject.timeout_add_seconds(1, + self._update_title_cb) + + def _title_focus_out_event_cb(self, entry, event): + self._update_entry() + + def _description_focus_out_event_cb(self, text_view, event): + self._update_entry() + + def _tags_focus_out_event_cb(self, text_view, event): + self._update_entry() + + def _update_entry(self, needs_update=False): + if not model.is_editable(self._metadata): + return + + old_title = self._metadata.get('title', None) + new_title = self._title.props.widget.props.text + if old_title != new_title: + label = glib.markup_escape_text(new_title) + self._icon.palette.props.primary_text = label + self._metadata['title'] = new_title + self._metadata['title_set_by_user'] = '1' + needs_update = True + + old_tags = self._metadata.get('tags', None) + new_tags = self._tags.text_view_widget.props.buffer.props.text + if old_tags != new_tags: + self._metadata['tags'] = new_tags + needs_update = True + + old_description = self._metadata.get('description', None) + new_description = \ + self._description.text_view_widget.props.buffer.props.text + if old_description != new_description: + self._metadata['description'] = new_description + needs_update = True + + if needs_update: + if self._metadata.get('mountpoint', '/') == '/': + model.write(self._metadata, update_mtime=False) + else: + old_file_path = os.path.join(self._metadata['mountpoint'], + model.get_file_name(old_title, + self._metadata['mime_type'])) + model.write(self._metadata, file_path=old_file_path, + update_mtime=False) + + self._update_title_sid = None + + def get_keep(self): + return (str(self._metadata.get('keep', 0)) == '1') + + def _keep_icon_activated_cb(self, keep_icon): + if self.get_keep(): + self._metadata['keep'] = 0 + else: + self._metadata['keep'] = 1 + self._update_entry(needs_update=True) + keep_icon.props.keep = self.get_keep() + + def _icon_button_release_event_cb(self, button, event): + logging.debug('_icon_button_release_event_cb') + misc.resume(self._metadata) + return True + + def _preview_box_button_release_event_cb(self, button, event): + logging.debug('_preview_box_button_release_event_cb') + misc.resume(self._metadata) + return True diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py new file mode 100644 index 0000000..bb1c7f6 --- /dev/null +++ b/src/jarabe/journal/journalactivity.py @@ -0,0 +1,375 @@ +# Copyright (C) 2006, Red Hat, Inc. +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import uuid + +import gtk +import dbus +import statvfs +import os + +from sugar.graphics.window import Window +from sugar.graphics.alert import ErrorAlert + +from sugar.bundle.bundle import ZipExtractException, RegistrationException +from sugar import env +from sugar.activity import activityfactory +from sugar import wm + +from jarabe.model import bundleregistry +from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox +from jarabe.journal.listview import ListView +from jarabe.journal.detailview import DetailView +from jarabe.journal.volumestoolbar import VolumesToolbar +from jarabe.journal import misc +from jarabe.journal.journalentrybundle import JournalEntryBundle +from jarabe.journal.objectchooser import ObjectChooser +from jarabe.journal.modalalert import ModalAlert +from jarabe.journal import model +from jarabe.journal.journalwindow import JournalWindow + + +J_DBUS_SERVICE = 'org.laptop.Journal' +J_DBUS_INTERFACE = 'org.laptop.Journal' +J_DBUS_PATH = '/org/laptop/Journal' + +_SPACE_TRESHOLD = 52428800 +_BUNDLE_ID = 'org.laptop.JournalActivity' + +_journal = None + + +class JournalActivityDBusService(dbus.service.Object): + def __init__(self, parent): + self._parent = parent + session_bus = dbus.SessionBus() + bus_name = dbus.service.BusName(J_DBUS_SERVICE, + bus=session_bus, replace_existing=False, allow_replacement=False) + logging.debug('bus_name: %r', bus_name) + dbus.service.Object.__init__(self, bus_name, J_DBUS_PATH) + + @dbus.service.method(J_DBUS_INTERFACE, + in_signature='s', out_signature='') + def ShowObject(self, object_id): + """Pop-up journal and show object with object_id""" + + logging.debug('Trying to show object %s', object_id) + + if self._parent.show_object(object_id): + self._parent.reveal() + + def _chooser_response_cb(self, chooser, response_id, chooser_id): + logging.debug('JournalActivityDBusService._chooser_response_cb') + if response_id == gtk.RESPONSE_ACCEPT: + object_id = chooser.get_selected_object_id() + self.ObjectChooserResponse(chooser_id, object_id) + else: + self.ObjectChooserCancelled(chooser_id) + chooser.destroy() + del chooser + + @dbus.service.method(J_DBUS_INTERFACE, in_signature='is', + out_signature='s') + def ChooseObject(self, parent_xid, what_filter=''): + chooser_id = uuid.uuid4().hex + if parent_xid > 0: + parent = gtk.gdk.window_foreign_new(parent_xid) + else: + parent = None + chooser = ObjectChooser(parent, what_filter) + chooser.connect('response', self._chooser_response_cb, chooser_id) + chooser.show() + + return chooser_id + + @dbus.service.signal(J_DBUS_INTERFACE, signature='ss') + def ObjectChooserResponse(self, chooser_id, object_id): + pass + + @dbus.service.signal(J_DBUS_INTERFACE, signature='s') + def ObjectChooserCancelled(self, chooser_id): + pass + + +class JournalActivity(JournalWindow): + def __init__(self): + logging.debug('STARTUP: Loading the journal') + JournalWindow.__init__(self) + + self.set_title(_('Journal')) + + self._main_view = None + self._secondary_view = None + self._list_view = None + self._detail_view = None + self._main_toolbox = None + self._detail_toolbox = None + self._volumes_toolbar = None + + self._setup_main_view() + self._setup_secondary_view() + + self.add_events(gtk.gdk.ALL_EVENTS_MASK | + gtk.gdk.VISIBILITY_NOTIFY_MASK) + self._realized_sid = self.connect('realize', self.__realize_cb) + self.connect('visibility-notify-event', + self.__visibility_notify_event_cb) + self.connect('window-state-event', self.__window_state_event_cb) + self.connect('key-press-event', self._key_press_event_cb) + self.connect('focus-in-event', self._focus_in_event_cb) + + model.created.connect(self.__model_created_cb) + model.updated.connect(self.__model_updated_cb) + model.deleted.connect(self.__model_deleted_cb) + + self._dbus_service = JournalActivityDBusService(self) + + self.iconify() + + self._critical_space_alert = None + self._check_available_space() + + def __volume_error_cb(self, gobject, message, severity): + alert = ErrorAlert(title=severity, msg=message) + alert.connect('response', self.__alert_response_cb) + self.add_alert(alert) + alert.show() + + def __alert_response_cb(self, alert, response_id): + self.remove_alert(alert) + + def __realize_cb(self, window): + wm.set_bundle_id(window.window, _BUNDLE_ID) + activity_id = activityfactory.create_activity_id() + wm.set_activity_id(window.window, str(activity_id)) + self.disconnect(self._realized_sid) + self._realized_sid = None + + def can_close(self): + return False + + def _setup_main_view(self): + self._main_toolbox = MainToolbox() + self._main_view = gtk.VBox() + + self._list_view = ListView() + self._list_view.connect('detail-clicked', self.__detail_clicked_cb) + self._list_view.connect('clear-clicked', self.__clear_clicked_cb) + self._list_view.connect('volume-error', self.__volume_error_cb) + self._main_view.pack_start(self._list_view) + self._list_view.show() + + self._volumes_toolbar = VolumesToolbar() + self._volumes_toolbar.connect('volume-changed', + self.__volume_changed_cb) + self._volumes_toolbar.connect('volume-error', self.__volume_error_cb) + self._main_view.pack_start(self._volumes_toolbar, expand=False) + + search_toolbar = self._main_toolbox.search_toolbar + search_toolbar.connect('query-changed', self._query_changed_cb) + search_toolbar.set_mount_point('/') + + def _setup_secondary_view(self): + self._secondary_view = gtk.VBox() + + self._detail_toolbox = DetailToolbox() + self._detail_toolbox.entry_toolbar.connect('volume-error', + self.__volume_error_cb) + + self._detail_view = DetailView() + self._detail_view.connect('go-back-clicked', self.__go_back_clicked_cb) + self._secondary_view.pack_end(self._detail_view) + self._detail_view.show() + + def _key_press_event_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == 'Escape': + self.show_main_view() + + def __detail_clicked_cb(self, list_view, object_id): + self._show_secondary_view(object_id) + + def __clear_clicked_cb(self, list_view): + self._main_toolbox.search_toolbar.clear_query() + + def __go_back_clicked_cb(self, detail_view): + self.show_main_view() + + def _query_changed_cb(self, toolbar, query): + self._list_view.update_with_query(query) + self.show_main_view() + + def show_main_view(self): + if self.toolbar_box != self._main_toolbox: + self.set_toolbar_box(self._main_toolbox) + self._main_toolbox.show() + + if self.canvas != self._main_view: + self.set_canvas(self._main_view) + self._main_view.show() + + def _show_secondary_view(self, object_id): + metadata = model.get(object_id) + try: + self._detail_toolbox.entry_toolbar.set_metadata(metadata) + except Exception: + logging.exception('Exception while displaying entry:') + + self.set_toolbar_box(self._detail_toolbox) + self._detail_toolbox.show() + + try: + self._detail_view.props.metadata = metadata + except Exception: + logging.exception('Exception while displaying entry:') + + self.set_canvas(self._secondary_view) + self._secondary_view.show() + + def show_object(self, object_id): + metadata = model.get(object_id) + if metadata is None: + return False + else: + self._show_secondary_view(object_id) + return True + + def __volume_changed_cb(self, volume_toolbar, mount_point): + logging.debug('Selected volume: %r.', mount_point) + self._main_toolbox.search_toolbar.set_mount_point(mount_point) + self._main_toolbox.set_current_toolbar(0) + + def __model_created_cb(self, sender, **kwargs): + self._check_for_bundle(kwargs['object_id']) + self._main_toolbox.search_toolbar.refresh_filters() + self._check_available_space() + + def __model_updated_cb(self, sender, **kwargs): + self._check_for_bundle(kwargs['object_id']) + + if self.canvas == self._secondary_view and \ + kwargs['object_id'] == self._detail_view.props.metadata['uid']: + self._detail_view.refresh() + + self._check_available_space() + + def __model_deleted_cb(self, sender, **kwargs): + if self.canvas == self._secondary_view and \ + kwargs['object_id'] == self._detail_view.props.metadata['uid']: + self.show_main_view() + + def _focus_in_event_cb(self, window, event): + self.search_grab_focus() + self._list_view.update_dates() + + def _check_for_bundle(self, object_id): + registry = bundleregistry.get_registry() + + metadata = model.get(object_id) + if metadata.get('progress', '').isdigit(): + if int(metadata['progress']) < 100: + return + + bundle = misc.get_bundle(metadata) + if bundle is None: + return + + if registry.is_installed(bundle): + logging.debug('_check_for_bundle bundle already installed') + return + + if metadata['mime_type'] == JournalEntryBundle.MIME_TYPE: + # JournalEntryBundle code takes over the datastore entry and + # transforms it into the journal entry from the bundle -- we have + # nothing more to do. + try: + registry.install(bundle, metadata['uid']) + except (ZipExtractException, RegistrationException): + logging.exception('Could not install bundle %s', + bundle.get_path()) + return + + try: + registry.install(bundle) + except (ZipExtractException, RegistrationException): + logging.exception('Could not install bundle %s', bundle.get_path()) + return + + metadata['bundle_id'] = bundle.get_bundle_id() + model.write(metadata) + + def search_grab_focus(self): + search_toolbar = self._main_toolbox.search_toolbar + search_toolbar.give_entry_focus() + + def __window_state_event_cb(self, window, event): + logging.debug('window_state_event_cb %r', self) + if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED: + state = event.new_window_state + visible = not state & gtk.gdk.WINDOW_STATE_ICONIFIED + self._list_view.set_is_visible(visible) + + def __visibility_notify_event_cb(self, window, event): + logging.debug('visibility_notify_event_cb %r', self) + visible = event.state != gtk.gdk.VISIBILITY_FULLY_OBSCURED + self._list_view.set_is_visible(visible) + + def _check_available_space(self): + """Check available space on device + + If the available space is below 50MB an alert will be + shown which encourages to delete old journal entries. + """ + + if self._critical_space_alert: + return + stat = os.statvfs(env.get_profile_path()) + free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL] + if free_space < _SPACE_TRESHOLD: + self._critical_space_alert = ModalAlert() + self._critical_space_alert.connect('destroy', + self.__alert_closed_cb) + self._critical_space_alert.show() + + def __alert_closed_cb(self, data): + self.show_main_view() + self.reveal() + self._critical_space_alert = None + + def set_active_volume(self, mount): + self._volumes_toolbar.set_active_volume(mount) + + def focus_search(self): + """Become visible and give focus to the search entry + """ + self.reveal() + self.show_main_view() + self.search_grab_focus() + + +def get_journal(): + global _journal + if _journal is None: + _journal = JournalActivity() + _journal.show() + return _journal + + +def start(): + get_journal() diff --git a/src/jarabe/journal/journalentrybundle.py b/src/jarabe/journal/journalentrybundle.py new file mode 100644 index 0000000..c220c09 --- /dev/null +++ b/src/jarabe/journal/journalentrybundle.py @@ -0,0 +1,94 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import tempfile +import shutil + +import simplejson +import dbus + +from sugar.bundle.bundle import Bundle, MalformedBundleException + +from jarabe.journal import model + + +class JournalEntryBundle(Bundle): + """A Journal entry bundle + + See http://wiki.laptop.org/go/Journal_entry_bundles for details + """ + + MIME_TYPE = 'application/vnd.olpc-journal-entry' + + _zipped_extension = '.xoj' + _unzipped_extension = None + _infodir = None + + def __init__(self, path): + Bundle.__init__(self, path) + + def install(self, uid=''): + if 'SUGAR_ACTIVITY_ROOT' in os.environ: + install_dir = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'], + 'data') + else: + install_dir = tempfile.gettempdir() + bundle_dir = os.path.join(install_dir, self._zip_root_dir) + temp_uid = self._zip_root_dir + self._unzip(install_dir) + try: + metadata = self._read_metadata(bundle_dir) + metadata['uid'] = uid + + preview = self._read_preview(temp_uid, bundle_dir) + if preview is not None: + metadata['preview'] = dbus.ByteArray(preview) + + file_path = os.path.join(bundle_dir, temp_uid) + model.write(metadata, file_path) + finally: + shutil.rmtree(bundle_dir, ignore_errors=True) + + def get_bundle_id(self): + return None + + def _read_metadata(self, bundle_dir): + metadata_path = os.path.join(bundle_dir, '_metadata.json') + if not os.path.exists(metadata_path): + raise MalformedBundleException( + 'Bundle must contain the file "_metadata.json"') + f = open(metadata_path, 'r') + try: + json_data = f.read() + finally: + f.close() + return simplejson.loads(json_data) + + def _read_preview(self, uid, bundle_dir): + preview_path = os.path.join(bundle_dir, 'preview', uid) + if not os.path.exists(preview_path): + return '' + f = open(preview_path, 'r') + try: + preview_data = f.read() + finally: + f.close() + return preview_data + + def is_installed(self): + # These bundles can be reinstalled as many times as desired. + return False diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py new file mode 100644 index 0000000..2aa4153 --- /dev/null +++ b/src/jarabe/journal/journaltoolbox.py @@ -0,0 +1,572 @@ +# Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2009, Walter Bender +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging +from datetime import datetime, timedelta +import os +import gconf +import time + +import gobject +import gio +import glib +import gtk + +from sugar.graphics.palette import Palette +from sugar.graphics.toolbox import Toolbox +from sugar.graphics.toolcombobox import ToolComboBox +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toggletoolbutton import ToggleToolButton +from sugar.graphics.combobox import ComboBox +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.graphics import iconentry +from sugar.graphics import style +from sugar import mime + +from jarabe.model import bundleregistry +from jarabe.journal import misc +from jarabe.journal import model +from jarabe.journal.palettes import ClipboardMenu +from jarabe.journal.palettes import VolumeMenu + + +_AUTOSEARCH_TIMEOUT = 1000 + +_ACTION_ANYTIME = 0 +_ACTION_TODAY = 1 +_ACTION_SINCE_YESTERDAY = 2 +_ACTION_PAST_WEEK = 3 +_ACTION_PAST_MONTH = 4 +_ACTION_PAST_YEAR = 5 + +_ACTION_ANYTHING = 0 + +_ACTION_EVERYBODY = 0 +_ACTION_MY_FRIENDS = 1 +_ACTION_MY_CLASS = 2 + + +class MainToolbox(Toolbox): + def __init__(self): + Toolbox.__init__(self) + + self.search_toolbar = SearchToolbar() + self.search_toolbar.set_size_request(-1, style.GRID_CELL_SIZE) + self.add_toolbar(_('Search'), self.search_toolbar) + self.search_toolbar.show() + + +class SearchToolbar(gtk.Toolbar): + __gtype_name__ = 'SearchToolbar' + + __gsignals__ = { + 'query-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._mount_point = None + + self._search_entry = iconentry.IconEntry() + self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, + 'system-search') + self._search_entry.connect('activate', self._search_entry_activated_cb) + self._search_entry.connect('changed', self._search_entry_changed_cb) + self._search_entry.add_clear_button() + self._autosearch_timer = None + self._add_widget(self._search_entry, expand=True) + + self._favorite_button = ToggleToolButton('emblem-favorite') + self._favorite_button.connect('toggled', + self.__favorite_button_toggled_cb) + self.insert(self._favorite_button, -1) + self._favorite_button.show() + + self._what_search_combo = ComboBox() + self._what_combo_changed_sid = self._what_search_combo.connect( + 'changed', self._combo_changed_cb) + tool_item = ToolComboBox(self._what_search_combo) + self.insert(tool_item, -1) + tool_item.show() + + self._when_search_combo = self._get_when_search_combo() + tool_item = ToolComboBox(self._when_search_combo) + self.insert(tool_item, -1) + tool_item.show() + + self._sorting_button = SortingButton() + self._sorting_button.connect('clicked', + self.__sorting_button_clicked_cb) + self.insert(self._sorting_button, -1) + self._sorting_button.connect('sort-property-changed', + self.__sort_changed_cb) + self._sorting_button.show() + + # TODO: enable it when the DS supports saving the buddies. + #self._with_search_combo = self._get_with_search_combo() + #tool_item = ToolComboBox(self._with_search_combo) + #self.insert(tool_item, -1) + #tool_item.show() + + self._query = self._build_query() + + self.refresh_filters() + + def give_entry_focus(self): + self._search_entry.grab_focus() + + def _get_when_search_combo(self): + when_search = ComboBox() + when_search.append_item(_ACTION_ANYTIME, _('Anytime')) + when_search.append_separator() + when_search.append_item(_ACTION_TODAY, _('Today')) + when_search.append_item(_ACTION_SINCE_YESTERDAY, + _('Since yesterday')) + # TRANS: Filter entries modified during the last 7 days. + when_search.append_item(_ACTION_PAST_WEEK, _('Past week')) + # TRANS: Filter entries modified during the last 30 days. + when_search.append_item(_ACTION_PAST_MONTH, _('Past month')) + # TRANS: Filter entries modified during the last 356 days. + when_search.append_item(_ACTION_PAST_YEAR, _('Past year')) + when_search.set_active(0) + when_search.connect('changed', self._combo_changed_cb) + return when_search + + def _get_with_search_combo(self): + with_search = ComboBox() + with_search.append_item(_ACTION_EVERYBODY, _('Anyone')) + with_search.append_separator() + with_search.append_item(_ACTION_MY_FRIENDS, _('My friends')) + with_search.append_item(_ACTION_MY_CLASS, _('My class')) + with_search.append_separator() + + # TODO: Ask the model for buddies. + with_search.append_item(3, 'Dan', 'theme:xo') + + with_search.set_active(0) + with_search.connect('changed', self._combo_changed_cb) + return with_search + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + def _build_query(self): + query = {} + + if self._mount_point: + query['mountpoints'] = [self._mount_point] + + if self._favorite_button.props.active: + query['keep'] = 1 + + if self._what_search_combo.props.value: + value = self._what_search_combo.props.value + generic_type = mime.get_generic_type(value) + if generic_type: + mime_types = generic_type.mime_types + query['mime_type'] = mime_types + else: + query['activity'] = self._what_search_combo.props.value + + if self._when_search_combo.props.value: + date_from, date_to = self._get_date_range() + query['timestamp'] = {'start': date_from, 'end': date_to} + + if self._search_entry.props.text: + text = self._search_entry.props.text.strip() + if text: + query['query'] = text + + property_, order = self._sorting_button.get_current_sort() + + if order == gtk.SORT_ASCENDING: + sign = '+' + else: + sign = '-' + query['order_by'] = [sign + property_] + + return query + + def _get_date_range(self): + today_start = datetime.today().replace(hour=0, minute=0, second=0) + right_now = datetime.today() + if self._when_search_combo.props.value == _ACTION_TODAY: + date_range = (today_start, right_now) + elif self._when_search_combo.props.value == _ACTION_SINCE_YESTERDAY: + date_range = (today_start - timedelta(1), right_now) + elif self._when_search_combo.props.value == _ACTION_PAST_WEEK: + date_range = (today_start - timedelta(7), right_now) + elif self._when_search_combo.props.value == _ACTION_PAST_MONTH: + date_range = (today_start - timedelta(30), right_now) + elif self._when_search_combo.props.value == _ACTION_PAST_YEAR: + date_range = (today_start - timedelta(356), right_now) + + return (time.mktime(date_range[0].timetuple()), + time.mktime(date_range[1].timetuple())) + + def _combo_changed_cb(self, combo): + self._update_if_needed() + + def __sort_changed_cb(self, button): + self._update_if_needed() + + def __sorting_button_clicked_cb(self, button): + self._sorting_button.palette.popup(immediate=True, state=1) + + def _update_if_needed(self): + new_query = self._build_query() + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def _search_entry_activated_cb(self, search_entry): + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + new_query = self._build_query() + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def _search_entry_changed_cb(self, search_entry): + if not search_entry.props.text: + search_entry.activate() + return + + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT, + self._autosearch_timer_cb) + + def _autosearch_timer_cb(self): + logging.debug('_autosearch_timer_cb') + self._autosearch_timer = None + self._search_entry.activate() + return False + + def set_mount_point(self, mount_point): + self._mount_point = mount_point + new_query = self._build_query() + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def set_what_filter(self, what_filter): + combo_model = self._what_search_combo.get_model() + what_filter_index = -1 + for i in range(0, len(combo_model) - 1): + if combo_model[i][0] == what_filter: + what_filter_index = i + break + + if what_filter_index == -1: + logging.warning('what_filter %r not known', what_filter) + else: + self._what_search_combo.set_active(what_filter_index) + + def refresh_filters(self): + current_value = self._what_search_combo.props.value + current_value_index = 0 + + self._what_search_combo.handler_block(self._what_combo_changed_sid) + try: + self._what_search_combo.remove_all() + # TRANS: Item in a combo box that filters by entry type. + self._what_search_combo.append_item(_ACTION_ANYTHING, + _('Anything')) + + registry = bundleregistry.get_registry() + appended_separator = False + + types = mime.get_all_generic_types() + for generic_type in types: + if not appended_separator: + self._what_search_combo.append_separator() + appended_separator = True + self._what_search_combo.append_item( + generic_type.type_id, generic_type.name, generic_type.icon) + if generic_type.type_id == current_value: + current_value_index = \ + len(self._what_search_combo.get_model()) - 1 + + self._what_search_combo.set_active(current_value_index) + + self._what_search_combo.append_separator() + + for service_name in model.get_unique_values('activity'): + activity_info = registry.get_bundle(service_name) + if activity_info is None: + continue + + if service_name == current_value: + combo_model = self._what_search_combo.get_model() + current_value_index = len(combo_model) + + # try activity-provided icon + if os.path.exists(activity_info.get_icon()): + try: + self._what_search_combo.append_item(service_name, + activity_info.get_name(), + file_name=activity_info.get_icon()) + except glib.GError, exception: + logging.warning('Falling back to default icon for' + ' "what" filter because %r (%r) has an' + ' invalid icon: %s', + activity_info.get_name(), + str(service_name), exception) + else: + continue + + # fall back to generic icon + self._what_search_combo.append_item(service_name, + activity_info.get_name(), + icon_name='application-octet-stream') + + finally: + self._what_search_combo.handler_unblock( + self._what_combo_changed_sid) + + def __favorite_button_toggled_cb(self, favorite_button): + self._update_if_needed() + + def clear_query(self): + self._search_entry.props.text = '' + self._what_search_combo.set_active(0) + self._when_search_combo.set_active(0) + self._favorite_button.props.active = False + + +class DetailToolbox(Toolbox): + def __init__(self): + Toolbox.__init__(self) + + self.entry_toolbar = EntryToolbar() + self.add_toolbar('', self.entry_toolbar) + self.entry_toolbar.show() + + +class EntryToolbar(gtk.Toolbar): + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._metadata = None + self._temp_file_path = None + + self._resume = ToolButton('activity-start') + self._resume.connect('clicked', self._resume_clicked_cb) + self.add(self._resume) + self._resume.show() + + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + self._copy = ToolButton() + icon = Icon(icon_name='edit-copy', xo_color=color) + self._copy.set_icon_widget(icon) + icon.show() + self._copy.set_tooltip(_('Copy to')) + self._copy.connect('clicked', self._copy_clicked_cb) + self.add(self._copy) + self._copy.show() + + self._duplicate = ToolButton() + icon = Icon(icon_name='edit-duplicate', xo_color=color) + self._duplicate.set_icon_widget(icon) + self._duplicate.set_tooltip(_('Duplicate')) + self._duplicate.connect('clicked', self._duplicate_clicked_cb) + self.add(self._duplicate) + + separator = gtk.SeparatorToolItem() + self.add(separator) + separator.show() + + erase_button = ToolButton('list-remove') + erase_button.set_tooltip(_('Erase')) + erase_button.connect('clicked', self._erase_button_clicked_cb) + self.add(erase_button) + erase_button.show() + + def set_metadata(self, metadata): + self._metadata = metadata + self._refresh_copy_palette() + self._refresh_duplicate_palette() + self._refresh_resume_palette() + + def _resume_clicked_cb(self, button): + misc.resume(self._metadata) + + def _copy_clicked_cb(self, button): + button.palette.popup(immediate=True, state=Palette.SECONDARY) + + def _duplicate_clicked_cb(self, button): + file_path = model.get_file(self._metadata['uid']) + try: + model.copy(self._metadata, '/') + except IOError, e: + logging.exception('Error while copying the entry.') + self.emit('volume-error', + _('Error while copying the entry. %s') % (e.strerror, ), + _('Error')) + + def _erase_button_clicked_cb(self, button): + registry = bundleregistry.get_registry() + + bundle = misc.get_bundle(self._metadata) + if bundle is not None and registry.is_installed(bundle): + registry.uninstall(bundle) + model.delete(self._metadata['uid']) + + def _resume_menu_item_activate_cb(self, menu_item, service_name): + misc.resume(self._metadata, service_name) + + def _refresh_copy_palette(self): + palette = self._copy.get_palette() + + for menu_item in palette.menu.get_children(): + palette.menu.remove(menu_item) + menu_item.destroy() + + clipboard_menu = ClipboardMenu(self._metadata) + clipboard_menu.set_image(Icon(icon_name='toolbar-edit', + icon_size=gtk.ICON_SIZE_MENU)) + clipboard_menu.connect('volume-error', self.__volume_error_cb) + palette.menu.append(clipboard_menu) + clipboard_menu.show() + + if self._metadata['mountpoint'] != '/': + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + journal_menu = VolumeMenu(self._metadata, _('Journal'), '/') + journal_menu.set_image(Icon(icon_name='activity-journal', + xo_color=color, + icon_size=gtk.ICON_SIZE_MENU)) + journal_menu.connect('volume-error', self.__volume_error_cb) + palette.menu.append(journal_menu) + journal_menu.show() + + volume_monitor = gio.volume_monitor_get() + icon_theme = gtk.icon_theme_get_default() + for mount in volume_monitor.get_mounts(): + if self._metadata['mountpoint'] == mount.get_root().get_path(): + continue + volume_menu = VolumeMenu(self._metadata, mount.get_name(), + mount.get_root().get_path()) + for name in mount.get_icon().props.names: + if icon_theme.has_icon(name): + volume_menu.set_image(Icon(icon_name=name, + icon_size=gtk.ICON_SIZE_MENU)) + break + volume_menu.connect('volume-error', self.__volume_error_cb) + palette.menu.append(volume_menu) + volume_menu.show() + + def _refresh_duplicate_palette(self): + color = misc.get_icon_color(self._metadata) + self._copy.get_icon_widget().props.xo_color = color + if self._metadata['mountpoint'] == '/': + self._duplicate.show() + icon = self._duplicate.get_icon_widget() + icon.props.xo_color = color + icon.show() + else: + self._duplicate.hide() + + def __volume_error_cb(self, menu_item, message, severity): + self.emit('volume-error', message, severity) + + def _refresh_resume_palette(self): + if self._metadata.get('activity_id', ''): + # TRANS: Action label for resuming an activity. + self._resume.set_tooltip(_('Resume')) + else: + # TRANS: Action label for starting an entry. + self._resume.set_tooltip(_('Start')) + + palette = self._resume.get_palette() + + for menu_item in palette.menu.get_children(): + palette.menu.remove(menu_item) + menu_item.destroy() + + for activity_info in misc.get_activities(self._metadata): + menu_item = MenuItem(activity_info.get_name()) + menu_item.set_image(Icon(file=activity_info.get_icon(), + icon_size=gtk.ICON_SIZE_MENU)) + menu_item.connect('activate', self._resume_menu_item_activate_cb, + activity_info.get_bundle_id()) + palette.menu.append(menu_item) + menu_item.show() + + +class SortingButton(ToolButton): + __gtype_name__ = 'JournalSortingButton' + + __gsignals__ = { + 'sort-property-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + + _SORT_OPTIONS = [ + ('timestamp', 'view-lastedit', _('Sort by date modified')), + ('creation_time', 'view-created', _('Sort by date created')), + ('filesize', 'view-size', _('Sort by size')), + ] + + def __init__(self): + ToolButton.__init__(self) + + self._property = 'timestamp' + self._order = gtk.SORT_ASCENDING + + self.props.tooltip = _('Sort view') + self.props.icon_name = 'view-lastedit' + + for property_, icon, label in self._SORT_OPTIONS: + button = MenuItem(icon_name=icon, text_label=label) + button.connect('activate', + self.__sort_type_changed_cb, + property_, + icon) + button.show() + self.props.palette.menu.insert(button, -1) + + def __sort_type_changed_cb(self, widget, property_, icon_name): + self._property = property_ + #FIXME: Implement sorting order + self._order = gtk.SORT_ASCENDING + self.emit('sort-property-changed') + + self.props.icon_name = icon_name + + def get_current_sort(self): + return (self._property, self._order) diff --git a/src/jarabe/journal/journalwindow.py b/src/jarabe/journal/journalwindow.py new file mode 100644 index 0000000..31bc790 --- /dev/null +++ b/src/jarabe/journal/journalwindow.py @@ -0,0 +1,33 @@ +#Copyright (C) 2010 Software for Education, Entertainment and Training +#Activities +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.graphics.window import Window + +_journal_window = None + + +class JournalWindow(Window): + + def __init__(self): + + global _journal_window + Window.__init__(self) + _journal_window = self + + +def get_journal_window(): + return _journal_window diff --git a/src/jarabe/journal/keepicon.py b/src/jarabe/journal/keepicon.py new file mode 100644 index 0000000..5bc299b --- /dev/null +++ b/src/jarabe/journal/keepicon.py @@ -0,0 +1,64 @@ +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import hippo +import gconf + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + + +class KeepIcon(CanvasIcon): + def __init__(self, keep): + CanvasIcon.__init__(self, icon_name='emblem-favorite', + box_width=style.GRID_CELL_SIZE * 3 / 5, + size=style.SMALL_ICON_SIZE) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + self._keep = None + self.set_keep(keep) + + def set_keep(self, keep): + if keep == self._keep: + return + + self._keep = keep + if keep: + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + self.props.xo_color = color + else: + self.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.fill_color = style.COLOR_TRANSPARENT.get_svg() + + def get_keep(self): + return self._keep + + keep = gobject.property(type=int, default=0, getter=get_keep, + setter=set_keep) + + def __motion_notify_event_cb(self, icon, event): + if not self._keep: + if event.detail == hippo.MOTION_DETAIL_ENTER: + client = gconf.client_get_default() + prelit_color = XoColor(client.get_string('/desktop/sugar/user/color')) + icon.props.stroke_color = prelit_color.get_stroke_color() + icon.props.fill_color = prelit_color.get_fill_color() + elif event.detail == hippo.MOTION_DETAIL_LEAVE: + icon.props.stroke_color = style.COLOR_BUTTON_GREY.get_svg() + icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg() diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py new file mode 100644 index 0000000..417ff61 --- /dev/null +++ b/src/jarabe/journal/listmodel.py @@ -0,0 +1,243 @@ +# Copyright (C) 2009, Tomeu Vizoso +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import simplejson +import gobject +import gtk +from gettext import gettext as _ + +from sugar.graphics.xocolor import XoColor +from sugar.graphics import style +from sugar import util + +from jarabe.journal import model +from jarabe.journal import misc + + +DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' +DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' +DS_DBUS_PATH = '/org/laptop/sugar/DataStore' + + +class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): + __gtype_name__ = 'JournalListModel' + + __gsignals__ = { + 'ready': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + COLUMN_UID = 0 + COLUMN_FAVORITE = 1 + COLUMN_ICON = 2 + COLUMN_ICON_COLOR = 3 + COLUMN_TITLE = 4 + COLUMN_TIMESTAMP = 5 + COLUMN_CREATION_TIME = 6 + COLUMN_FILESIZE = 7 + COLUMN_PROGRESS = 8 + COLUMN_BUDDY_1 = 9 + COLUMN_BUDDY_2 = 10 + COLUMN_BUDDY_3 = 11 + + _COLUMN_TYPES = { + COLUMN_UID: str, + COLUMN_FAVORITE: bool, + COLUMN_ICON: str, + COLUMN_ICON_COLOR: object, + COLUMN_TITLE: str, + COLUMN_TIMESTAMP: str, + COLUMN_CREATION_TIME: str, + COLUMN_FILESIZE: str, + COLUMN_PROGRESS: int, + COLUMN_BUDDY_1: object, + COLUMN_BUDDY_3: object, + COLUMN_BUDDY_2: object, + } + + _PAGE_SIZE = 10 + + def __init__(self, query): + gobject.GObject.__init__(self) + + self._last_requested_index = None + self._cached_row = None + self._result_set = model.find(query, ListModel._PAGE_SIZE) + self._temp_drag_file_path = None + + # HACK: The view will tell us that it is resizing so the model can + # avoid hitting D-Bus and disk. + self.view_is_resizing = False + + self._result_set.ready.connect(self.__result_set_ready_cb) + self._result_set.progress.connect(self.__result_set_progress_cb) + + def __result_set_ready_cb(self, **kwargs): + self.emit('ready') + + def __result_set_progress_cb(self, **kwargs): + self.emit('progress') + + def setup(self): + self._result_set.setup() + + def stop(self): + self._result_set.stop() + + def get_metadata(self, path): + return model.get(self[path][ListModel.COLUMN_UID]) + + def on_get_n_columns(self): + return len(ListModel._COLUMN_TYPES) + + def on_get_column_type(self, index): + return ListModel._COLUMN_TYPES[index] + + def on_iter_n_children(self, iterator): + if iterator == None: + return self._result_set.length + else: + return 0 + + def on_get_value(self, index, column): + if self.view_is_resizing: + return None + + if index == self._last_requested_index: + return self._cached_row[column] + + if index >= self._result_set.length: + return None + + self._result_set.seek(index) + metadata = self._result_set.read() + + self._last_requested_index = index + self._cached_row = [] + self._cached_row.append(metadata['uid']) + self._cached_row.append(metadata.get('keep', '0') == '1') + self._cached_row.append(misc.get_icon_name(metadata)) + + if misc.is_activity_bundle(metadata): + xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + else: + xo_color = misc.get_icon_color(metadata) + self._cached_row.append(xo_color) + + title = gobject.markup_escape_text(metadata.get('title', + _('Untitled'))) + self._cached_row.append('%s' % (title, )) + + try: + timestamp = float(metadata.get('timestamp', 0)) + except (TypeError, ValueError): + timestamp_content = _('Unknown') + else: + timestamp_content = util.timestamp_to_elapsed_string(timestamp) + self._cached_row.append(timestamp_content) + + try: + creation_time = float(metadata.get('creation_time')) + except (TypeError, ValueError): + self._cached_row.append(_('Unknown')) + else: + self._cached_row.append( + util.timestamp_to_elapsed_string(float(creation_time))) + + try: + size = int(metadata.get('filesize')) + except (TypeError, ValueError): + size = None + self._cached_row.append(util.format_size(size)) + + try: + progress = int(float(metadata.get('progress', 100))) + except (TypeError, ValueError): + progress = 100 + self._cached_row.append(progress) + + buddies = [] + if metadata.get('buddies'): + try: + buddies = simplejson.loads(metadata['buddies']).values() + except simplejson.decoder.JSONDecodeError, exception: + logging.warning('Cannot decode buddies for %r: %s', + metadata['uid'], exception) + + if not isinstance(buddies, list): + logging.warning('Content of buddies for %r is not a list: %r', + metadata['uid'], buddies) + buddies = [] + + for n_ in xrange(0, 3): + if buddies: + try: + nick, color = buddies.pop(0) + except (AttributeError, ValueError), exception: + logging.warning('Malformed buddies for %r: %s', + metadata['uid'], exception) + else: + self._cached_row.append((nick, XoColor(color))) + continue + + self._cached_row.append(None) + + return self._cached_row[column] + + def on_iter_nth_child(self, iterator, n): + return n + + def on_get_path(self, iterator): + return (iterator) + + def on_get_iter(self, path): + return path[0] + + def on_iter_next(self, iterator): + if iterator != None: + if iterator >= self._result_set.length - 1: + return None + return iterator + 1 + return None + + def on_get_flags(self): + return gtk.TREE_MODEL_ITERS_PERSIST | gtk.TREE_MODEL_LIST_ONLY + + def on_iter_children(self, iterator): + return None + + def on_iter_has_child(self, iterator): + return False + + def on_iter_parent(self, iterator): + return None + + def do_drag_data_get(self, path, selection): + uid = self[path][ListModel.COLUMN_UID] + if selection.target == 'text/uri-list': + # Get hold of a reference so the temp file doesn't get deleted + self._temp_drag_file_path = model.get_file(uid) + logging.debug('putting %r in selection', self._temp_drag_file_path) + selection.set(selection.target, 8, self._temp_drag_file_path) + return True + elif selection.target == 'journal-object-id': + selection.set(selection.target, 8, uid) + return True + + return False diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py new file mode 100644 index 0000000..57836f2 --- /dev/null +++ b/src/jarabe/journal/listview.py @@ -0,0 +1,670 @@ +# Copyright (C) 2009, Tomeu Vizoso +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import time + +import gobject +import gtk +import hippo +import gconf +import pango + +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon, Icon, CellRendererIcon +from sugar.graphics.xocolor import XoColor +from sugar import util + +from jarabe.journal.listmodel import ListModel +from jarabe.journal.palettes import ObjectPalette, BuddyPalette +from jarabe.journal import model +from jarabe.journal import misc + + +UPDATE_INTERVAL = 300 + + +class TreeView(gtk.TreeView): + __gtype_name__ = 'JournalTreeView' + + def __init__(self): + gtk.TreeView.__init__(self) + self.set_headers_visible(False) + self.set_enable_search(False) + + def do_size_request(self, requisition): + # HACK: We tell the model that the view is just resizing so it can + # avoid hitting both D-Bus and disk. + tree_model = self.get_model() + if tree_model is not None: + tree_model.view_is_resizing = True + try: + gtk.TreeView.do_size_request(self, requisition) + finally: + if tree_model is not None: + tree_model.view_is_resizing = False + + +class BaseListView(gtk.Bin): + __gtype_name__ = 'JournalBaseListView' + + __gsignals__ = { + 'clear-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self): + self._query = {} + self._model = None + self._progress_bar = None + self._last_progress_bar_pulse = None + self._scroll_position = 0. + + gobject.GObject.__init__(self) + + self.connect('map', self.__map_cb) + self.connect('unrealize', self.__unrealize_cb) + self.connect('destroy', self.__destroy_cb) + + self._scrolled_window = gtk.ScrolledWindow() + self._scrolled_window.set_policy(gtk.POLICY_NEVER, + gtk.POLICY_AUTOMATIC) + self.add(self._scrolled_window) + self._scrolled_window.show() + + self.tree_view = TreeView() + selection = self.tree_view.get_selection() + selection.set_mode(gtk.SELECTION_NONE) + self.tree_view.props.fixed_height_mode = True + self.tree_view.modify_base(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + self._scrolled_window.add(self.tree_view) + self.tree_view.show() + + self.cell_title = None + self.cell_icon = None + self._title_column = None + self.sort_column = None + self._add_columns() + + self.tree_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, + [('text/uri-list', 0, 0), + ('journal-object-id', 0, 0)], + gtk.gdk.ACTION_COPY) + + # Auto-update stuff + self._fully_obscured = True + self._dirty = False + self._refresh_idle_handler = None + self._update_dates_timer = None + + model.created.connect(self.__model_created_cb) + model.updated.connect(self.__model_updated_cb) + model.deleted.connect(self.__model_deleted_cb) + + def __model_created_cb(self, sender, signal, object_id): + if self._is_new_item_visible(object_id): + self._set_dirty() + + def __model_updated_cb(self, sender, signal, object_id): + if self._is_new_item_visible(object_id): + self._set_dirty() + + def __model_deleted_cb(self, sender, signal, object_id): + if self._is_new_item_visible(object_id): + self._set_dirty() + + def _is_new_item_visible(self, object_id): + """Check if the created item is part of the currently selected view""" + if self._query['mountpoints'] == ['/']: + return not object_id.startswith('/') + else: + return object_id.startswith(self._query['mountpoints'][0]) + + def _add_columns(self): + cell_favorite = CellRendererFavorite(self.tree_view) + cell_favorite.connect('clicked', self.__favorite_clicked_cb) + + column = gtk.TreeViewColumn() + column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + column.props.fixed_width = cell_favorite.props.width + column.pack_start(cell_favorite) + column.set_cell_data_func(cell_favorite, self.__favorite_set_data_cb) + self.tree_view.append_column(column) + + self.cell_icon = CellRendererActivityIcon(self.tree_view) + + column = gtk.TreeViewColumn() + column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + column.props.fixed_width = self.cell_icon.props.width + column.pack_start(self.cell_icon) + column.add_attribute(self.cell_icon, 'file-name', + ListModel.COLUMN_ICON) + column.add_attribute(self.cell_icon, 'xo-color', + ListModel.COLUMN_ICON_COLOR) + self.tree_view.append_column(column) + + self.cell_title = gtk.CellRendererText() + self.cell_title.props.ellipsize = pango.ELLIPSIZE_MIDDLE + self.cell_title.props.ellipsize_set = True + + self._title_column = gtk.TreeViewColumn() + self._title_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + self._title_column.props.expand = True + self._title_column.props.clickable = True + self._title_column.pack_start(self.cell_title) + self._title_column.add_attribute(self.cell_title, 'markup', + ListModel.COLUMN_TITLE) + self.tree_view.append_column(self._title_column) + + buddies_column = gtk.TreeViewColumn() + buddies_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + self.tree_view.append_column(buddies_column) + + for column_index in [ListModel.COLUMN_BUDDY_1, + ListModel.COLUMN_BUDDY_2, + ListModel.COLUMN_BUDDY_3]: + cell_icon = CellRendererBuddy(self.tree_view, + column_index=column_index) + buddies_column.pack_start(cell_icon) + buddies_column.props.fixed_width += cell_icon.props.width + buddies_column.add_attribute(cell_icon, 'buddy', column_index) + buddies_column.set_cell_data_func(cell_icon, + self.__buddies_set_data_cb) + + cell_progress = gtk.CellRendererProgress() + cell_progress.props.ypad = style.GRID_CELL_SIZE / 4 + buddies_column.pack_start(cell_progress) + buddies_column.add_attribute(cell_progress, 'value', + ListModel.COLUMN_PROGRESS) + buddies_column.set_cell_data_func(cell_progress, + self.__progress_data_cb) + + cell_text = gtk.CellRendererText() + cell_text.props.xalign = 1 + + # Measure the required width for a date in the form of "10 hours, 10 + # minutes ago" + timestamp = time.time() - 10 * 60 - 10 * 60 * 60 + date = util.timestamp_to_elapsed_string(timestamp) + date_width = self._get_width_for_string(date) + + self.sort_column = gtk.TreeViewColumn() + self.sort_column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + self.sort_column.props.fixed_width = date_width + self.sort_column.set_alignment(1) + self.sort_column.props.resizable = True + self.sort_column.props.clickable = True + self.sort_column.pack_start(cell_text) + self.sort_column.add_attribute(cell_text, 'text', + ListModel.COLUMN_TIMESTAMP) + self.tree_view.append_column(self.sort_column) + + def _get_width_for_string(self, text): + # Add some extra margin + text = text + 'aaaaa' + + widget = gtk.Label('') + context = widget.get_pango_context() + layout = pango.Layout(context) + layout.set_text(text) + width, height_ = layout.get_size() + return pango.PIXELS(width) + + def do_size_allocate(self, allocation): + self.allocation = allocation + self.child.size_allocate(allocation) + + def do_size_request(self, requisition): + requisition.width, requisition.height = self.child.size_request() + + def __destroy_cb(self, widget): + if self._model is not None: + self._model.stop() + + def __buddies_set_data_cb(self, column, cell, tree_model, tree_iter): + progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS] + cell.props.visible = progress >= 100 + + def __progress_data_cb(self, column, cell, tree_model, tree_iter): + progress = tree_model[tree_iter][ListModel.COLUMN_PROGRESS] + cell.props.visible = progress < 100 + + def __favorite_set_data_cb(self, column, cell, tree_model, tree_iter): + favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE] + if favorite: + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + cell.props.xo_color = color + else: + cell.props.xo_color = None + + def __favorite_clicked_cb(self, cell, path): + row = self._model[path] + metadata = model.get(row[ListModel.COLUMN_UID]) + if not model.is_editable(metadata): + return + if metadata.get('keep', 0) == '1': + metadata['keep'] = '0' + else: + metadata['keep'] = '1' + model.write(metadata, update_mtime=False) + + def update_with_query(self, query_dict): + logging.debug('ListView.update_with_query') + if 'order_by' not in query_dict: + query_dict['order_by'] = ['+timestamp'] + if query_dict['order_by'] != self._query.get('order_by'): + property_ = query_dict['order_by'][0][1:] + cell_text = self.sort_column.get_cell_renderers()[0] + self.sort_column.set_attributes(cell_text, + text=getattr(ListModel, 'COLUMN_' + property_.upper(), + ListModel.COLUMN_TIMESTAMP)) + self._query = query_dict + + self.refresh() + + def refresh(self): + logging.debug('ListView.refresh query %r', self._query) + self._stop_progress_bar() + + if self._model is not None: + self._model.stop() + self._dirty = False + + self._model = ListModel(self._query) + self._model.connect('ready', self.__model_ready_cb) + self._model.connect('progress', self.__model_progress_cb) + self._model.setup() + + def __model_ready_cb(self, tree_model): + self._stop_progress_bar() + + self._scroll_position = self.tree_view.props.vadjustment.props.value + logging.debug('ListView.__model_ready_cb %r', self._scroll_position) + + if self.tree_view.window is not None: + # prevent glitches while later vadjustment setting, see #1235 + self.tree_view.get_bin_window().hide() + + # Cannot set it up earlier because will try to access the model + # and it needs to be ready. + self.tree_view.set_model(self._model) + + self.tree_view.props.vadjustment.props.value = self._scroll_position + self.tree_view.props.vadjustment.value_changed() + + if self.tree_view.window is not None: + # prevent glitches while later vadjustment setting, see #1235 + self.tree_view.get_bin_window().show() + + if len(tree_model) == 0: + if self._is_query_empty(): + if self._query['mountpoints'] == ['/']: + self._show_message(_('Your Journal is empty')) + elif self._query['mountpoints'] == \ + [model.get_documents_path()]: + self._show_message(_('Your documents folder is empty')) + else: + self._show_message(_('The device is empty')) + else: + self._show_message(_('No matching entries'), + show_clear_query=True) + else: + self._clear_message() + + def __map_cb(self, widget): + logging.debug('ListView.__map_cb %r', self._scroll_position) + self.tree_view.props.vadjustment.props.value = self._scroll_position + self.tree_view.props.vadjustment.value_changed() + + def __unrealize_cb(self, widget): + self._scroll_position = self.tree_view.props.vadjustment.props.value + logging.debug('ListView.__map_cb %r', self._scroll_position) + + def _is_query_empty(self): + # FIXME: This is a hack, we shouldn't have to update this every time + # a new search term is added. + return not (self._query.get('query') or self._query.get('mime_type') or + self._query.get('keep') or self._query.get('mtime') or + self._query.get('activity')) + + def __model_progress_cb(self, tree_model): + if self._progress_bar is None: + self._start_progress_bar() + + if time.time() - self._last_progress_bar_pulse > 0.05: + self._progress_bar.pulse() + self._last_progress_bar_pulse = time.time() + + def _start_progress_bar(self): + alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5) + self.remove(self.child) + self.add(alignment) + alignment.show() + + self._progress_bar = gtk.ProgressBar() + self._progress_bar.props.pulse_step = 0.01 + self._last_progress_bar_pulse = time.time() + alignment.add(self._progress_bar) + self._progress_bar.show() + + def _stop_progress_bar(self): + if self._progress_bar is None: + return + self.remove(self.child) + self.add(self._scrolled_window) + self._progress_bar = None + + def _show_message(self, message, show_clear_query=False): + canvas = hippo.Canvas() + self.remove(self.child) + self.add(canvas) + canvas.show() + + box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, + background_color=style.COLOR_WHITE.get_int(), + yalign=hippo.ALIGNMENT_CENTER, + spacing=style.DEFAULT_SPACING, + padding_bottom=style.GRID_CELL_SIZE) + canvas.set_root(box) + + icon = CanvasIcon(size=style.LARGE_ICON_SIZE, + icon_name='activity-journal', + stroke_color=style.COLOR_BUTTON_GREY.get_svg(), + fill_color=style.COLOR_TRANSPARENT.get_svg()) + box.append(icon) + + text = hippo.CanvasText(text=message, + xalign=hippo.ALIGNMENT_CENTER, + font_desc=style.FONT_BOLD.get_pango_desc(), + color=style.COLOR_BUTTON_GREY.get_int()) + box.append(text) + + if show_clear_query: + button = gtk.Button(label=_('Clear search')) + button.connect('clicked', self.__clear_button_clicked_cb) + button.props.image = Icon(icon_name='dialog-cancel', + icon_size=gtk.ICON_SIZE_BUTTON) + canvas_button = hippo.CanvasWidget(widget=button, + xalign=hippo.ALIGNMENT_CENTER) + box.append(canvas_button) + + def __clear_button_clicked_cb(self, button): + self.emit('clear-clicked') + + def _clear_message(self): + if self.child == self._scrolled_window: + return + self.remove(self.child) + self.add(self._scrolled_window) + self._scrolled_window.show() + + def update_dates(self): + if not self.tree_view.flags() & gtk.REALIZED: + return + visible_range = self.tree_view.get_visible_range() + if visible_range is None: + return + + logging.debug('ListView.update_dates') + + path, end_path = visible_range + tree_model = self.tree_view.get_model() + + while True: + x, y, width, height = self.tree_view.get_cell_area(path, + self.sort_column) + x, y = self.tree_view.convert_tree_to_widget_coords(x, y) + self.tree_view.queue_draw_area(x, y, width, height) + if path == end_path: + break + else: + next_iter = tree_model.iter_next(tree_model.get_iter(path)) + path = tree_model.get_path(next_iter) + + def _set_dirty(self): + if self._fully_obscured: + self._dirty = True + else: + self.refresh() + + def set_is_visible(self, visible): + if visible != self._fully_obscured: + return + + logging.debug('canvas_visibility_notify_event_cb %r', visible) + if visible: + self._fully_obscured = False + if self._dirty: + self.refresh() + if self._update_dates_timer is None: + logging.debug('Adding date updating timer') + self._update_dates_timer = \ + gobject.timeout_add_seconds(UPDATE_INTERVAL, + self.__update_dates_timer_cb) + else: + self._fully_obscured = True + if self._update_dates_timer is not None: + logging.debug('Remove date updating timer') + gobject.source_remove(self._update_dates_timer) + self._update_dates_timer = None + + def __update_dates_timer_cb(self): + self.update_dates() + return True + + +class ListView(BaseListView): + __gtype_name__ = 'JournalListView' + + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self): + BaseListView.__init__(self) + self._is_dragging = False + + self.tree_view.connect('drag-begin', self.__drag_begin_cb) + self.tree_view.connect('button-release-event', + self.__button_release_event_cb) + + self.cell_title.connect('edited', self.__cell_title_edited_cb) + self.cell_title.connect('editing-canceled', self.__editing_canceled_cb) + + self.cell_icon.connect('clicked', self.__icon_clicked_cb) + self.cell_icon.connect('detail-clicked', self.__detail_clicked_cb) + self.cell_icon.connect('volume-error', self.__volume_error_cb) + + cell_detail = CellRendererDetail(self.tree_view) + cell_detail.connect('clicked', self.__detail_cell_clicked_cb) + + column = gtk.TreeViewColumn() + column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED + column.props.fixed_width = cell_detail.props.width + column.pack_start(cell_detail) + self.tree_view.append_column(column) + + def __drag_begin_cb(self, widget, drag_context): + self._is_dragging = True + + def __button_release_event_cb(self, tree_view, event): + try: + if self._is_dragging: + return + finally: + self._is_dragging = False + + pos = tree_view.get_path_at_pos(int(event.x), int(event.y)) + if pos is None: + return + + path, column, x_, y_ = pos + if column != self._title_column: + return + + row = self.tree_view.get_model()[path] + metadata = model.get(row[ListModel.COLUMN_UID]) + self.cell_title.props.editable = model.is_editable(metadata) + + tree_view.set_cursor_on_cell(path, column, start_editing=True) + + def __detail_cell_clicked_cb(self, cell, path): + row = self.tree_view.get_model()[path] + self.emit('detail-clicked', row[ListModel.COLUMN_UID]) + + def __detail_clicked_cb(self, cell, uid): + self.emit('detail-clicked', uid) + + def __volume_error_cb(self, cell, message, severity): + self.emit('volume-error', message, severity) + + def __icon_clicked_cb(self, cell, path): + row = self.tree_view.get_model()[path] + metadata = model.get(row[ListModel.COLUMN_UID]) + misc.resume(metadata) + + def __cell_title_edited_cb(self, cell, path, new_text): + row = self._model[path] + metadata = model.get(row[ListModel.COLUMN_UID]) + metadata['title'] = new_text + model.write(metadata, update_mtime=False) + self.cell_title.props.editable = False + + def __editing_canceled_cb(self, cell): + self.cell_title.props.editable = False + + +class CellRendererFavorite(CellRendererIcon): + __gtype_name__ = 'JournalCellRendererFavorite' + + def __init__(self, tree_view): + CellRendererIcon.__init__(self, tree_view) + + self.props.width = style.GRID_CELL_SIZE + self.props.height = style.GRID_CELL_SIZE + self.props.size = style.SMALL_ICON_SIZE + self.props.icon_name = 'emblem-favorite' + self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE + 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 CellRendererDetail(CellRendererIcon): + __gtype_name__ = 'JournalCellRendererDetail' + + def __init__(self, tree_view): + CellRendererIcon.__init__(self, tree_view) + + self.props.width = style.GRID_CELL_SIZE + self.props.height = style.GRID_CELL_SIZE + self.props.size = style.SMALL_ICON_SIZE + self.props.icon_name = 'go-right' + self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE + self.props.stroke_color = style.COLOR_TRANSPARENT.get_svg() + self.props.fill_color = style.COLOR_BUTTON_GREY.get_svg() + self.props.prelit_stroke_color = style.COLOR_TRANSPARENT.get_svg() + self.props.prelit_fill_color = style.COLOR_BLACK.get_svg() + + +class CellRendererActivityIcon(CellRendererIcon): + __gtype_name__ = 'JournalCellRendererActivityIcon' + + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self, tree_view): + self._show_palette = True + + CellRendererIcon.__init__(self, tree_view) + + self.props.width = style.GRID_CELL_SIZE + self.props.height = style.GRID_CELL_SIZE + self.props.size = style.STANDARD_ICON_SIZE + self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE + + self.tree_view = tree_view + + def create_palette(self): + if not self._show_palette: + return None + + tree_model = self.tree_view.get_model() + metadata = tree_model.get_metadata(self.props.palette_invoker.path) + + palette = ObjectPalette(metadata, detail=True) + palette.connect('detail-clicked', + self.__detail_clicked_cb) + palette.connect('volume-error', + self.__volume_error_cb) + return palette + + def __detail_clicked_cb(self, palette, uid): + self.emit('detail-clicked', uid) + + def __volume_error_cb(self, palette, message, severity): + self.emit('volume-error', message, severity) + + def set_show_palette(self, show_palette): + self._show_palette = show_palette + + show_palette = gobject.property(type=bool, default=True, + setter=set_show_palette) + + +class CellRendererBuddy(CellRendererIcon): + __gtype_name__ = 'JournalCellRendererBuddy' + + def __init__(self, tree_view, column_index): + CellRendererIcon.__init__(self, tree_view) + + self.props.width = style.STANDARD_ICON_SIZE + self.props.height = style.STANDARD_ICON_SIZE + self.props.size = style.STANDARD_ICON_SIZE + self.props.mode = gtk.CELL_RENDERER_MODE_ACTIVATABLE + + self.tree_view = tree_view + self._model_column_index = column_index + + def create_palette(self): + tree_model = self.tree_view.get_model() + row = tree_model[self.props.palette_invoker.path] + + if row[self._model_column_index] is not None: + nick, xo_color = row[self._model_column_index] + return BuddyPalette((nick, xo_color.to_string())) + else: + return None + + def set_buddy(self, buddy): + if buddy is None: + self.props.icon_name = None + else: + nick_, xo_color = buddy + self.props.icon_name = 'computer-xo' + self.props.xo_color = xo_color + + buddy = gobject.property(type=object, setter=set_buddy) diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py new file mode 100644 index 0000000..1431d5f --- /dev/null +++ b/src/jarabe/journal/misc.py @@ -0,0 +1,315 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import time +import os +from gettext import gettext as _ + +import gio +import gconf +import gtk + +from sugar.activity import activityfactory +from sugar.activity.activityhandle import ActivityHandle +from sugar.graphics.icon import get_icon_file_name +from sugar.graphics.xocolor import XoColor +from sugar.graphics.alert import ConfirmationAlert +from sugar import mime +from sugar.bundle.activitybundle import ActivityBundle +from sugar.bundle.bundle import AlreadyInstalledException +from sugar.bundle.contentbundle import ContentBundle +from sugar import util + +from jarabe.view import launcher +from jarabe.model import bundleregistry, shell +from jarabe.journal.journalentrybundle import JournalEntryBundle +from jarabe.journal import model +from jarabe.journal import journalwindow + + +def _get_icon_for_mime(mime_type): + generic_types = mime.get_all_generic_types() + for generic_type in generic_types: + if mime_type in generic_type.mime_types: + file_name = get_icon_file_name(generic_type.icon) + if file_name is not None: + return file_name + + icons = gio.content_type_get_icon(mime_type) + logging.debug('icons for this file: %r', icons.props.names) + for icon_name in icons.props.names: + file_name = get_icon_file_name(icon_name) + if file_name is not None: + return file_name + + +def get_icon_name(metadata): + file_name = None + + bundle_id = metadata.get('activity', '') + if not bundle_id: + bundle_id = metadata.get('bundle_id', '') + + if bundle_id: + activity_info = bundleregistry.get_registry().get_bundle(bundle_id) + if activity_info: + file_name = activity_info.get_icon() + + if file_name is None and is_activity_bundle(metadata): + file_path = model.get_file(metadata['uid']) + if file_path is not None and os.path.exists(file_path): + try: + bundle = ActivityBundle(file_path) + file_name = bundle.get_icon() + except Exception: + logging.exception('Could not read bundle') + + if file_name is None: + file_name = _get_icon_for_mime(metadata.get('mime_type', '')) + + if file_name is None: + file_name = get_icon_file_name('application-octet-stream') + + return file_name + + +def get_date(metadata): + """ Convert from a string in iso format to a more human-like format. """ + if 'timestamp' in metadata: + try: + timestamp = float(metadata['timestamp']) + except (TypeError, ValueError): + logging.warning('Invalid timestamp: %r', metadata['timestamp']) + else: + return util.timestamp_to_elapsed_string(timestamp) + + if 'mtime' in metadata: + try: + ti = time.strptime(metadata['mtime'], '%Y-%m-%dT%H:%M:%S') + except (TypeError, ValueError): + logging.warning('Invalid mtime: %r', metadata['mtime']) + else: + return util.timestamp_to_elapsed_string(time.mktime(ti)) + + return _('No date') + + +def get_bundle(metadata): + try: + if is_activity_bundle(metadata): + file_path = model.get_file(metadata['uid']) + if not os.path.exists(file_path): + logging.warning('Invalid path: %r', file_path) + return None + return ActivityBundle(file_path) + + elif is_content_bundle(metadata): + file_path = model.get_file(metadata['uid']) + if not os.path.exists(file_path): + logging.warning('Invalid path: %r', file_path) + return None + return ContentBundle(file_path) + + elif is_journal_bundle(metadata): + file_path = model.get_file(metadata['uid']) + if not os.path.exists(file_path): + logging.warning('Invalid path: %r', file_path) + return None + return JournalEntryBundle(file_path) + else: + return None + except Exception: + logging.exception('Incorrect bundle') + return None + + +def _get_activities_for_mime(mime_type): + registry = bundleregistry.get_registry() + result = registry.get_activities_for_type(mime_type) + if not result: + for parent_mime in mime.get_mime_parents(mime_type): + for activity in registry.get_activities_for_type(parent_mime): + if activity not in result: + result.append(activity) + return result + + +def get_activities(metadata): + activities = [] + + bundle_id = metadata.get('activity', '') + if bundle_id: + activity_info = bundleregistry.get_registry().get_bundle(bundle_id) + if activity_info: + activities.append(activity_info) + + mime_type = metadata.get('mime_type', '') + if mime_type: + activities_info = _get_activities_for_mime(mime_type) + for activity_info in activities_info: + if activity_info not in activities: + activities.append(activity_info) + + return activities + + +def resume(metadata, bundle_id=None): + registry = bundleregistry.get_registry() + + if is_activity_bundle(metadata) and bundle_id is None: + + logging.debug('Creating activity bundle') + + file_path = model.get_file(metadata['uid']) + bundle = ActivityBundle(file_path) + if not registry.is_installed(bundle): + logging.debug('Installing activity bundle') + try: + registry.install(bundle) + except AlreadyInstalledException: + _downgrade_option_alert(bundle) + return + else: + logging.debug('Upgrading activity bundle') + registry.upgrade(bundle) + + _launch_bundle(bundle) + + elif is_content_bundle(metadata) and bundle_id is None: + + logging.debug('Creating content bundle') + + file_path = model.get_file(metadata['uid']) + bundle = ContentBundle(file_path) + if not bundle.is_installed(): + logging.debug('Installing content bundle') + bundle.install() + + activities = _get_activities_for_mime('text/html') + if len(activities) == 0: + logging.warning('No activity can open HTML content bundles') + return + + uri = bundle.get_start_uri() + logging.debug('activityfactory.creating with uri %s', uri) + + activity_bundle = registry.get_bundle(activities[0].get_bundle_id()) + launch(activity_bundle, uri=uri) + else: + activity_id = metadata.get('activity_id', '') + + if bundle_id is None: + activities = get_activities(metadata) + if not activities: + logging.warning('No activity can open this object, %s.', + metadata.get('mime_type', None)) + return + bundle_id = activities[0].get_bundle_id() + + bundle = registry.get_bundle(bundle_id) + + if metadata.get('mountpoint', '/') == '/': + object_id = metadata['uid'] + else: + object_id = model.copy(metadata, '/') + + launch(bundle, activity_id=activity_id, object_id=object_id, + color=get_icon_color(metadata)) + + +def _launch_bundle(bundle): + registry = bundleregistry.get_registry() + logging.debug('activityfactory.creating bundle with id %r', + bundle.get_bundle_id()) + installed_bundle = registry.get_bundle(bundle.get_bundle_id()) + if installed_bundle: + launch(installed_bundle) + else: + logging.error('Bundle %r is not installed.', + bundle.get_bundle_id()) + + +def launch(bundle, activity_id=None, object_id=None, uri=None, color=None, + invited=False): + if activity_id is None or not activity_id: + activity_id = activityfactory.create_activity_id() + + logging.debug('launch bundle_id=%s activity_id=%s object_id=%s uri=%s', + bundle.get_bundle_id(), activity_id, object_id, uri) + + shell_model = shell.get_model() + activity = shell_model.get_activity_by_id(activity_id) + if activity is not None: + logging.debug('re-launch %r', activity.get_window()) + activity.get_window().activate(gtk.get_current_event_time()) + return + + if color is None: + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + + launcher.add_launcher(activity_id, bundle.get_icon(), color) + activity_handle = ActivityHandle(activity_id=activity_id, + object_id=object_id, uri=uri, invited=invited) + activityfactory.create(bundle, activity_handle) + + +def _downgrade_option_alert(bundle): + alert = ConfirmationAlert() + alert.props.title = _('Older Version Of %s Activity') % (bundle.get_name()) + alert.props.msg = _('Do you want to downgrade to version %s') % \ + bundle.get_activity_version() + alert.connect('response', _downgrade_alert_response_cb, bundle) + journalwindow.get_journal_window().add_alert(alert) + alert.show() + + +def _downgrade_alert_response_cb(alert, response_id, bundle): + if response_id is gtk.RESPONSE_OK: + journalwindow.get_journal_window().remove_alert(alert) + registry = bundleregistry.get_registry() + registry.install(bundle, force_downgrade=True) + _launch_bundle(bundle) + elif response_id is gtk.RESPONSE_CANCEL: + journalwindow.get_journal_window().remove_alert(alert) + + +def is_activity_bundle(metadata): + mime_type = metadata.get('mime_type', '') + return mime_type == ActivityBundle.MIME_TYPE or \ + mime_type == ActivityBundle.DEPRECATED_MIME_TYPE + + +def is_content_bundle(metadata): + return metadata.get('mime_type', '') == ContentBundle.MIME_TYPE + + +def is_journal_bundle(metadata): + return metadata.get('mime_type', '') == JournalEntryBundle.MIME_TYPE + + +def is_bundle(metadata): + return is_activity_bundle(metadata) or is_content_bundle(metadata) or \ + is_journal_bundle(metadata) + + +def get_icon_color(metadata): + if metadata is None or not 'icon-color' in metadata: + client = gconf.client_get_default() + return XoColor(client.get_string('/desktop/sugar/user/color')) + else: + return XoColor(metadata['icon-color']) diff --git a/src/jarabe/journal/modalalert.py b/src/jarabe/journal/modalalert.py new file mode 100644 index 0000000..6880941 --- /dev/null +++ b/src/jarabe/journal/modalalert.py @@ -0,0 +1,96 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +from gettext import gettext as _ +import gconf + +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + + +class ModalAlert(gtk.Window): + + __gtype_name__ = 'SugarModalAlert' + + def __init__(self): + gtk.Window.__init__(self) + + self.set_border_width(style.LINE_WIDTH) + offset = style.GRID_CELL_SIZE + width = gtk.gdk.screen_width() - offset * 2 + height = gtk.gdk.screen_height() - offset * 2 + self.set_size_request(width, height) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_decorated(False) + self.set_resizable(False) + self.set_modal(True) + + self._main_view = gtk.EventBox() + self._vbox = gtk.VBox() + self._vbox.set_spacing(style.DEFAULT_SPACING) + self._vbox.set_border_width(style.GRID_CELL_SIZE * 2) + self._main_view.modify_bg(gtk.STATE_NORMAL, + style.COLOR_BLACK.get_gdk_color()) + self._main_view.add(self._vbox) + self._vbox.show() + + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + + icon = Icon(icon_name='activity-journal', + pixel_size=style.XLARGE_ICON_SIZE, + xo_color=color) + self._vbox.pack_start(icon, False) + icon.show() + + self._title = gtk.Label() + self._title.modify_fg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + self._title.set_markup('%s' % _('Your Journal is full')) + self._vbox.pack_start(self._title, False) + self._title.show() + + self._message = gtk.Label(_('Please delete some old Journal' + ' entries to make space for new ones.')) + self._message.modify_fg(gtk.STATE_NORMAL, + style.COLOR_WHITE.get_gdk_color()) + self._vbox.pack_start(self._message, False) + self._message.show() + + alignment = gtk.Alignment(xalign=0.5, yalign=0.5) + self._vbox.pack_start(alignment, expand=False) + alignment.show() + + self._show_journal = gtk.Button() + self._show_journal.set_label(_('Show Journal')) + alignment.add(self._show_journal) + self._show_journal.show() + self._show_journal.connect('clicked', self.__show_journal_cb) + + self.add(self._main_view) + self._main_view.show() + + self.connect('realize', self.__realize_cb) + + def __realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + + def __show_journal_cb(self, button): + """The opener will listen on the destroy signal""" + self.destroy() diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py new file mode 100644 index 0000000..5285a7c --- /dev/null +++ b/src/jarabe/journal/model.py @@ -0,0 +1,818 @@ +# Copyright (C) 2007-2011, One Laptop per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import os +import errno +import subprocess +from datetime import datetime +import time +import shutil +import tempfile +from stat import S_IFLNK, S_IFMT, S_IFDIR, S_IFREG +import re +from operator import itemgetter +import simplejson +from gettext import gettext as _ + +import gobject +import dbus +import gio +import gconf + +from sugar import dispatch +from sugar import mime +from sugar import util + + +DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' +DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' +DS_DBUS_PATH = '/org/laptop/sugar/DataStore' + +# Properties the journal cares about. +PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id', + 'creation_time', 'filesize', 'icon-color', 'keep', 'mime_type', + 'mountpoint', 'mtime', 'progress', 'timestamp', 'title', 'uid'] + +MIN_PAGES_TO_CACHE = 3 +MAX_PAGES_TO_CACHE = 5 + +JOURNAL_METADATA_DIR = '.Sugar-Metadata' + +_datastore = None +created = dispatch.Signal() +updated = dispatch.Signal() +deleted = dispatch.Signal() + + +class _Cache(object): + + __gtype_name__ = 'model_Cache' + + def __init__(self, entries=None): + self._array = [] + if entries is not None: + self.append_all(entries) + + def prepend_all(self, entries): + self._array[0:0] = entries + + def append_all(self, entries): + self._array += entries + + def __len__(self): + return len(self._array) + + def __getitem__(self, key): + return self._array[key] + + def __delitem__(self, key): + del self._array[key] + + +class BaseResultSet(object): + """Encapsulates the result of a query + """ + + def __init__(self, query, page_size): + self._total_count = -1 + self._position = -1 + self._query = query + self._page_size = page_size + + self._offset = 0 + self._cache = _Cache() + + self.ready = dispatch.Signal() + self.progress = dispatch.Signal() + + def setup(self): + self.ready.send(self) + + def stop(self): + pass + + def get_length(self): + if self._total_count == -1: + query = self._query.copy() + query['limit'] = self._page_size * MIN_PAGES_TO_CACHE + entries, self._total_count = self.find(query) + self._cache.append_all(entries) + self._offset = 0 + return self._total_count + + length = property(get_length) + + def find(self, query): + raise NotImplementedError() + + def seek(self, position): + self._position = position + + def read(self): + if self._position == -1: + self.seek(0) + + if self._position < self._offset: + remaining_forward_entries = 0 + else: + remaining_forward_entries = self._offset + len(self._cache) - \ + self._position + + if self._position > self._offset + len(self._cache): + remaining_backwards_entries = 0 + else: + remaining_backwards_entries = self._position - self._offset + + last_cached_entry = self._offset + len(self._cache) + + if remaining_forward_entries <= 0 and remaining_backwards_entries <= 0: + + # Total cache miss: remake it + limit = self._page_size * MIN_PAGES_TO_CACHE + offset = max(0, self._position - limit / 2) + logging.debug('remaking cache, offset: %r limit: %r', offset, + limit) + query = self._query.copy() + query['limit'] = limit + query['offset'] = offset + entries, self._total_count = self.find(query) + + del self._cache[:] + self._cache.append_all(entries) + self._offset = offset + + elif (remaining_forward_entries <= 0 and + remaining_backwards_entries > 0): + + # Add one page to the end of cache + logging.debug('appending one more page, offset: %r', + last_cached_entry) + query = self._query.copy() + query['limit'] = self._page_size + query['offset'] = last_cached_entry + entries, self._total_count = self.find(query) + + # update cache + self._cache.append_all(entries) + + # apply the cache limit + cache_limit = self._page_size * MAX_PAGES_TO_CACHE + objects_excess = len(self._cache) - cache_limit + if objects_excess > 0: + self._offset += objects_excess + del self._cache[:objects_excess] + + elif remaining_forward_entries > 0 and \ + remaining_backwards_entries <= 0 and self._offset > 0: + + # Add one page to the beginning of cache + limit = min(self._offset, self._page_size) + self._offset = max(0, self._offset - limit) + + logging.debug('prepending one more page, offset: %r limit: %r', + self._offset, limit) + query = self._query.copy() + query['limit'] = limit + query['offset'] = self._offset + entries, self._total_count = self.find(query) + + # update cache + self._cache.prepend_all(entries) + + # apply the cache limit + cache_limit = self._page_size * MAX_PAGES_TO_CACHE + objects_excess = len(self._cache) - cache_limit + if objects_excess > 0: + del self._cache[-objects_excess:] + + return self._cache[self._position - self._offset] + + +class DatastoreResultSet(BaseResultSet): + """Encapsulates the result of a query on the datastore + """ + def __init__(self, query, page_size): + + if query.get('query', '') and not query['query'].startswith('"'): + query_text = '' + words = query['query'].split(' ') + for word in words: + if word: + if query_text: + query_text += ' ' + query_text += word + '*' + + query['query'] = query_text + + BaseResultSet.__init__(self, query, page_size) + + def find(self, query): + entries, total_count = _get_datastore().find(query, PROPERTIES, + byte_arrays=True) + + for entry in entries: + entry['mountpoint'] = '/' + + return entries, total_count + + +class InplaceResultSet(BaseResultSet): + """Encapsulates the result of a query on a mount point + """ + def __init__(self, query, page_size, mount_point): + BaseResultSet.__init__(self, query, page_size) + self._mount_point = mount_point + self._file_list = None + self._pending_directories = [] + self._visited_directories = [] + self._pending_files = [] + self._stopped = False + + query_text = query.get('query', '') + if query_text.startswith('"') and query_text.endswith('"'): + self._regex = re.compile('*%s*' % query_text.strip(['"'])) + elif query_text: + expression = '' + for word in query_text.split(' '): + expression += '(?=.*%s.*)' % word + self._regex = re.compile(expression, re.IGNORECASE) + else: + self._regex = None + + if query.get('timestamp', ''): + self._date_start = int(query['timestamp']['start']) + self._date_end = int(query['timestamp']['end']) + else: + self._date_start = None + self._date_end = None + + self._mime_types = query.get('mime_type', []) + + self._sort = query.get('order_by', ['+timestamp'])[0] + + def setup(self): + self._file_list = [] + self._pending_directories = [self._mount_point] + self._visited_directories = [] + self._pending_files = [] + gobject.idle_add(self._scan) + + def stop(self): + self._stopped = True + + def setup_ready(self): + if self._sort[1:] == 'filesize': + keygetter = itemgetter(3) + else: + # timestamp + keygetter = itemgetter(2) + self._file_list.sort(lambda a, b: cmp(b, a), + key=keygetter, + reverse=(self._sort[0] == '-')) + self.ready.send(self) + + def find(self, query): + if self._file_list is None: + raise ValueError('Need to call setup() first') + + if self._stopped: + raise ValueError('InplaceResultSet already stopped') + + t = time.time() + + offset = int(query.get('offset', 0)) + limit = int(query.get('limit', len(self._file_list))) + total_count = len(self._file_list) + + files = self._file_list[offset:offset + limit] + + entries = [] + for file_path, stat, mtime_, size_, metadata in files: + if metadata is None: + metadata = _get_file_metadata(file_path, stat) + metadata['mountpoint'] = self._mount_point + entries.append(metadata) + + logging.debug('InplaceResultSet.find took %f s.', time.time() - t) + + return entries, total_count + + def _scan(self): + if self._stopped: + return False + + self.progress.send(self) + + if self._pending_files: + self._scan_a_file() + return True + + if self._pending_directories: + self._scan_a_directory() + return True + + self.setup_ready() + self._visited_directories = [] + return False + + def _scan_a_file(self): + full_path = self._pending_files.pop(0) + metadata = None + + try: + stat = os.lstat(full_path) + except OSError, e: + if e.errno != errno.ENOENT: + logging.exception( + 'Error reading metadata of file %r', full_path) + return + + if S_IFMT(stat.st_mode) == S_IFLNK: + try: + link = os.readlink(full_path) + except OSError, e: + logging.exception( + 'Error reading target of link %r', full_path) + return + + if not os.path.abspath(link).startswith(self._mount_point): + return + + try: + stat = os.stat(full_path) + + except OSError, e: + if e.errno != errno.ENOENT: + logging.exception( + 'Error reading metadata of linked file %r', full_path) + return + + if S_IFMT(stat.st_mode) == S_IFDIR: + id_tuple = stat.st_ino, stat.st_dev + if not id_tuple in self._visited_directories: + self._visited_directories.append(id_tuple) + self._pending_directories.append(full_path) + return + + if S_IFMT(stat.st_mode) != S_IFREG: + return + + if self._regex is not None and \ + not self._regex.match(full_path): + metadata = _get_file_metadata(full_path, stat, + fetch_preview=False) + if not metadata: + return + add_to_list = False + for f in ['fulltext', 'title', + 'description', 'tags']: + if f in metadata and \ + self._regex.match(metadata[f]): + add_to_list = True + break + if not add_to_list: + return + + if self._date_start is not None and stat.st_mtime < self._date_start: + return + + if self._date_end is not None and stat.st_mtime > self._date_end: + return + + if self._mime_types: + mime_type = gio.content_type_guess(filename=full_path) + if mime_type not in self._mime_types: + return + + file_info = (full_path, stat, int(stat.st_mtime), stat.st_size, + metadata) + self._file_list.append(file_info) + + return + + def _scan_a_directory(self): + dir_path = self._pending_directories.pop(0) + + try: + entries = os.listdir(dir_path) + except OSError, e: + if e.errno != errno.EACCES: + logging.exception('Error reading directory %r', dir_path) + return + + for entry in entries: + if entry.startswith('.'): + continue + self._pending_files.append(dir_path + '/' + entry) + return + + +def _get_file_metadata(path, stat, fetch_preview=True): + """Return the metadata from the corresponding file. + + Reads the metadata stored in the json file or create the + metadata based on the file properties. + + """ + filename = os.path.basename(path) + dir_path = os.path.dirname(path) + metadata = _get_file_metadata_from_json(dir_path, filename, fetch_preview) + if metadata: + if 'filesize' not in metadata: + metadata['filesize'] = stat.st_size + return metadata + + return {'uid': path, + 'title': os.path.basename(path), + 'timestamp': stat.st_mtime, + 'filesize': stat.st_size, + 'mime_type': gio.content_type_guess(filename=path), + 'activity': '', + 'activity_id': '', + 'icon-color': '#000000,#ffffff', + 'description': path} + + +def _get_file_metadata_from_json(dir_path, filename, fetch_preview): + """Read the metadata from the json file and the preview + stored on the external device. + + If the metadata is corrupted we do remove it and the preview as well. + + """ + metadata = None + metadata_path = os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.metadata') + preview_path = os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.preview') + + if not os.path.exists(metadata_path): + return None + + try: + metadata = simplejson.load(open(metadata_path)) + except (ValueError, EnvironmentError): + os.unlink(metadata_path) + if os.path.exists(preview_path): + os.unlink(preview_path) + logging.error('Could not read metadata for file %r on ' + 'external device.', filename) + return None + else: + metadata['uid'] = os.path.join(dir_path, filename) + + if not fetch_preview: + if 'preview' in metadata: + del(metadata['preview']) + else: + if os.path.exists(preview_path): + try: + metadata['preview'] = dbus.ByteArray(open(preview_path).read()) + except EnvironmentError: + logging.debug('Could not read preview for file %r on ' + 'external device.', filename) + + return metadata + + +def _get_datastore(): + global _datastore + if _datastore is None: + bus = dbus.SessionBus() + remote_object = bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH) + _datastore = dbus.Interface(remote_object, DS_DBUS_INTERFACE) + + _datastore.connect_to_signal('Created', _datastore_created_cb) + _datastore.connect_to_signal('Updated', _datastore_updated_cb) + _datastore.connect_to_signal('Deleted', _datastore_deleted_cb) + + return _datastore + + +def _datastore_created_cb(object_id): + created.send(None, object_id=object_id) + + +def _datastore_updated_cb(object_id): + updated.send(None, object_id=object_id) + + +def _datastore_deleted_cb(object_id): + deleted.send(None, object_id=object_id) + + +def find(query_, page_size): + """Returns a ResultSet + """ + query = query_.copy() + + mount_points = query.pop('mountpoints', ['/']) + if mount_points is None or len(mount_points) != 1: + raise ValueError('Exactly one mount point must be specified') + + if mount_points[0] == '/': + return DatastoreResultSet(query, page_size) + else: + return InplaceResultSet(query, page_size, mount_points[0]) + + +def _get_mount_point(path): + dir_path = os.path.dirname(path) + while dir_path: + if os.path.ismount(dir_path): + return dir_path + else: + dir_path = dir_path.rsplit(os.sep, 1)[0] + return None + + +def get(object_id): + """Returns the metadata for an object + """ + if os.path.exists(object_id): + stat = os.stat(object_id) + metadata = _get_file_metadata(object_id, stat) + metadata['mountpoint'] = _get_mount_point(object_id) + else: + metadata = _get_datastore().get_properties(object_id, byte_arrays=True) + metadata['mountpoint'] = '/' + return metadata + + +def get_file(object_id): + """Returns the file for an object + """ + if os.path.exists(object_id): + logging.debug('get_file asked for file with path %r', object_id) + return object_id + else: + logging.debug('get_file asked for entry with id %r', object_id) + file_path = _get_datastore().get_filename(object_id) + if file_path: + return util.TempFilePath(file_path) + else: + return None + + +def get_file_size(object_id): + """Return the file size for an object + """ + logging.debug('get_file_size %r', object_id) + if os.path.exists(object_id): + return os.stat(object_id).st_size + + file_path = _get_datastore().get_filename(object_id) + if file_path: + size = os.stat(file_path).st_size + os.remove(file_path) + return size + + return 0 + + +def get_unique_values(key): + """Returns a list with the different values a property has taken + """ + empty_dict = dbus.Dictionary({}, signature='ss') + return _get_datastore().get_uniquevaluesfor(key, empty_dict) + + +def delete(object_id): + """Removes an object from persistent storage + """ + if not os.path.exists(object_id): + _get_datastore().delete(object_id) + else: + os.unlink(object_id) + dir_path = os.path.dirname(object_id) + filename = os.path.basename(object_id) + old_files = [os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.metadata'), + os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.preview')] + for old_file in old_files: + if os.path.exists(old_file): + try: + os.unlink(old_file) + except EnvironmentError: + logging.error('Could not remove metadata=%s ' + 'for file=%s', old_file, filename) + deleted.send(None, object_id=object_id) + + +def copy(metadata, mount_point): + """Copies an object to another mount point + """ + metadata = get(metadata['uid']) + if mount_point == '/' and metadata['icon-color'] == '#000000,#ffffff': + client = gconf.client_get_default() + metadata['icon-color'] = client.get_string('/desktop/sugar/user/color') + file_path = get_file(metadata['uid']) + if file_path is None: + file_path = '' + + metadata['mountpoint'] = mount_point + del metadata['uid'] + + return write(metadata, file_path, transfer_ownership=False) + + +def write(metadata, file_path='', update_mtime=True, transfer_ownership=True): + """Creates or updates an entry for that id + """ + logging.debug('model.write %r %r %r', metadata.get('uid', ''), file_path, + update_mtime) + if update_mtime: + metadata['mtime'] = datetime.now().isoformat() + metadata['timestamp'] = int(time.time()) + + if metadata.get('mountpoint', '/') == '/': + if metadata.get('uid', ''): + object_id = _get_datastore().update(metadata['uid'], + dbus.Dictionary(metadata), + file_path, + transfer_ownership) + else: + object_id = _get_datastore().create(dbus.Dictionary(metadata), + file_path, + transfer_ownership) + else: + object_id = _write_entry_on_external_device(metadata, file_path) + + return object_id + + +def _rename_entry_on_external_device(file_path, destination_path, + metadata_dir_path): + """Rename an entry with the associated metadata on an external device.""" + old_file_path = file_path + if old_file_path != destination_path: + os.rename(file_path, destination_path) + old_fname = os.path.basename(file_path) + old_files = [os.path.join(metadata_dir_path, + old_fname + '.metadata'), + os.path.join(metadata_dir_path, + old_fname + '.preview')] + for ofile in old_files: + if os.path.exists(ofile): + try: + os.unlink(ofile) + except EnvironmentError: + logging.error('Could not remove metadata=%s ' + 'for file=%s', ofile, old_fname) + + +def _write_entry_on_external_device(metadata, file_path): + """Create and update an entry copied from the + DS to an external storage device. + + Besides copying the associated file a file for the preview + and one for the metadata are stored in the hidden directory + .Sugar-Metadata. + + This function handles renames of an entry on the + external device and avoids name collisions. Renames are + handled failsafe. + + """ + if 'uid' in metadata and os.path.exists(metadata['uid']): + file_path = metadata['uid'] + + if not file_path or not os.path.exists(file_path): + raise ValueError('Entries without a file cannot be copied to ' + 'removable devices') + + if not metadata.get('title'): + metadata['title'] = _('Untitled') + file_name = get_file_name(metadata['title'], metadata['mime_type']) + + destination_path = os.path.join(metadata['mountpoint'], file_name) + if destination_path != file_path: + file_name = get_unique_file_name(metadata['mountpoint'], file_name) + destination_path = os.path.join(metadata['mountpoint'], file_name) + clean_name, extension_ = os.path.splitext(file_name) + metadata['title'] = clean_name + + metadata_copy = metadata.copy() + metadata_copy.pop('mountpoint', None) + metadata_copy.pop('uid', None) + metadata_copy.pop('filesize', None) + + metadata_dir_path = os.path.join(metadata['mountpoint'], + JOURNAL_METADATA_DIR) + if not os.path.exists(metadata_dir_path): + os.mkdir(metadata_dir_path) + + preview = None + if 'preview' in metadata_copy: + preview = metadata_copy['preview'] + preview_fname = file_name + '.preview' + metadata_copy.pop('preview', None) + + try: + metadata_json = simplejson.dumps(metadata_copy) + except (UnicodeDecodeError, EnvironmentError): + logging.error('Could not convert metadata to json.') + else: + (fh, fn) = tempfile.mkstemp(dir=metadata['mountpoint']) + os.write(fh, metadata_json) + os.close(fh) + os.rename(fn, os.path.join(metadata_dir_path, file_name + '.metadata')) + + if preview: + (fh, fn) = tempfile.mkstemp(dir=metadata['mountpoint']) + os.write(fh, preview) + os.close(fh) + os.rename(fn, os.path.join(metadata_dir_path, preview_fname)) + + if not os.path.dirname(destination_path) == os.path.dirname(file_path): + shutil.copy(file_path, destination_path) + else: + _rename_entry_on_external_device(file_path, destination_path, + metadata_dir_path) + + object_id = destination_path + created.send(None, object_id=object_id) + + return object_id + + +def get_file_name(title, mime_type): + file_name = title + + extension = mime.get_primary_extension(mime_type) + if extension is not None and extension: + extension = '.' + extension + if not file_name.endswith(extension): + file_name += extension + + # Invalid characters in VFAT filenames. From + # http://en.wikipedia.org/wiki/File_Allocation_Table + invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\x7F'] + invalid_chars.extend([chr(x) for x in range(0, 32)]) + for char in invalid_chars: + file_name = file_name.replace(char, '_') + + # FAT limit is 255, leave some space for uniqueness + max_len = 250 + if len(file_name) > max_len: + name, extension = os.path.splitext(file_name) + file_name = name[0:max_len - len(extension)] + extension + + return file_name + + +def get_unique_file_name(mount_point, file_name): + if os.path.exists(os.path.join(mount_point, file_name)): + i = 1 + name, extension = os.path.splitext(file_name) + while len(file_name) <= 255: + file_name = name + '_' + str(i) + extension + if not os.path.exists(os.path.join(mount_point, file_name)): + break + i += 1 + + return file_name + + +def is_editable(metadata): + if metadata.get('mountpoint', '/') == '/': + return True + else: + return os.access(metadata['mountpoint'], os.W_OK) + + +def get_documents_path(): + """Gets the path of the DOCUMENTS folder + + If xdg-user-dir can not find the DOCUMENTS folder it returns + $HOME, which we omit. xdg-user-dir handles localization + (i.e. translation) of the filenames. + + Returns: Path to $HOME/DOCUMENTS or None if an error occurs + """ + try: + pipe = subprocess.Popen(['xdg-user-dir', 'DOCUMENTS'], + stdout=subprocess.PIPE) + documents_path = os.path.normpath(pipe.communicate()[0].strip()) + if os.path.exists(documents_path) and \ + os.environ.get('HOME') != documents_path: + return documents_path + except OSError, exception: + if exception.errno != errno.ENOENT: + logging.exception('Could not run xdg-user-dir') + return None diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py new file mode 100644 index 0000000..ecb8ecf --- /dev/null +++ b/src/jarabe/journal/objectchooser.py @@ -0,0 +1,199 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging + +import gobject +import gtk +import wnck + +from sugar.graphics import style +from sugar.graphics.toolbutton import ToolButton + +from jarabe.journal.listview import BaseListView +from jarabe.journal.listmodel import ListModel +from jarabe.journal.journaltoolbox import SearchToolbar +from jarabe.journal.volumestoolbar import VolumesToolbar + + +class ObjectChooser(gtk.Window): + + __gtype_name__ = 'ObjectChooser' + + __gsignals__ = { + 'response': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([int])), + } + + def __init__(self, parent=None, what_filter=''): + gtk.Window.__init__(self) + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.set_decorated(False) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_border_width(style.LINE_WIDTH) + + self._selected_object_id = None + + self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) + self.connect('visibility-notify-event', + self.__visibility_notify_event_cb) + self.connect('delete-event', self.__delete_event_cb) + self.connect('key-press-event', self.__key_press_event_cb) + + if parent is None: + logging.warning('ObjectChooser: No parent window specified') + else: + self.connect('realize', self.__realize_cb, parent) + + screen = wnck.screen_get_default() + screen.connect('window-closed', self.__window_closed_cb, parent) + + vbox = gtk.VBox() + self.add(vbox) + vbox.show() + + title_box = TitleBox() + title_box.connect('volume-changed', self.__volume_changed_cb) + title_box.close_button.connect('clicked', + self.__close_button_clicked_cb) + title_box.set_size_request(-1, style.GRID_CELL_SIZE) + vbox.pack_start(title_box, expand=False) + title_box.show() + + separator = gtk.HSeparator() + vbox.pack_start(separator, expand=False) + separator.show() + + self._toolbar = SearchToolbar() + self._toolbar.connect('query-changed', self.__query_changed_cb) + self._toolbar.set_size_request(-1, style.GRID_CELL_SIZE) + vbox.pack_start(self._toolbar, expand=False) + self._toolbar.show() + + self._list_view = ChooserListView() + self._list_view.connect('entry-activated', self.__entry_activated_cb) + vbox.pack_start(self._list_view) + self._list_view.show() + + self._toolbar.set_mount_point('/') + + width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2 + height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2 + self.set_size_request(width, height) + + if what_filter: + self._toolbar.set_what_filter(what_filter) + + def __realize_cb(self, chooser, parent): + self.window.set_transient_for(parent) + # TODO: Should we disconnect the signal here? + + def __window_closed_cb(self, screen, window, parent): + if window.get_xid() == parent.xid: + self.destroy() + + def __entry_activated_cb(self, list_view, uid): + self._selected_object_id = uid + self.emit('response', gtk.RESPONSE_ACCEPT) + + def __delete_event_cb(self, chooser, event): + self.emit('response', gtk.RESPONSE_DELETE_EVENT) + + def __key_press_event_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == 'Escape': + self.emit('response', gtk.RESPONSE_DELETE_EVENT) + + def __close_button_clicked_cb(self, button): + self.emit('response', gtk.RESPONSE_DELETE_EVENT) + + def get_selected_object_id(self): + return self._selected_object_id + + def __query_changed_cb(self, toolbar, query): + self._list_view.update_with_query(query) + + def __volume_changed_cb(self, volume_toolbar, mount_point): + logging.debug('Selected volume: %r.', mount_point) + self._toolbar.set_mount_point(mount_point) + + def __visibility_notify_event_cb(self, window, event): + logging.debug('visibility_notify_event_cb %r', self) + visible = event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED + self._list_view.set_is_visible(visible) + + +class TitleBox(VolumesToolbar): + __gtype_name__ = 'TitleBox' + + def __init__(self): + VolumesToolbar.__init__(self) + + label = gtk.Label() + label.set_markup('%s' % _('Choose an object')) + label.set_alignment(0, 0.5) + self._add_widget(label, expand=True) + + self.close_button = ToolButton(icon_name='dialog-cancel') + self.close_button.set_tooltip(_('Close')) + self.insert(self.close_button, -1) + self.close_button.show() + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + +class ChooserListView(BaseListView): + __gtype_name__ = 'ChooserListView' + + __gsignals__ = { + 'entry-activated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self): + BaseListView.__init__(self) + + self.cell_icon.props.show_palette = False + self.tree_view.props.hover_selection = True + + self.tree_view.connect('button-release-event', + self.__button_release_event_cb) + + def __entry_activated_cb(self, entry): + self.emit('entry-activated', entry) + + def __button_release_event_cb(self, tree_view, event): + if event.window != tree_view.get_bin_window(): + return False + + pos = tree_view.get_path_at_pos(int(event.x), int(event.y)) + if pos is None: + return False + + path, column_, x_, y_ = pos + uid = tree_view.get_model()[path][ListModel.COLUMN_UID] + self.emit('entry-activated', uid) + + return False diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py new file mode 100644 index 0000000..8fc1e5d --- /dev/null +++ b/src/jarabe/journal/palettes.py @@ -0,0 +1,383 @@ +# 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 +import gconf +import gio +import glib + +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar import mime + +from jarabe.model import friends +from jarabe.model import filetransfer +from jarabe.model import mimeregistry +from jarabe.journal import misc +from jarabe.journal import model + + +class ObjectPalette(Palette): + + __gtype_name__ = 'ObjectPalette' + + __gsignals__ = { + 'detail-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self, metadata, detail=False): + + self._metadata = metadata + + activity_icon = Icon(icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + activity_icon.props.file = misc.get_icon_name(metadata) + color = misc.get_icon_color(metadata) + activity_icon.props.xo_color = color + + if 'title' in metadata: + title = gobject.markup_escape_text(metadata['title']) + else: + title = glib.markup_escape_text(_('Untitled')) + + Palette.__init__(self, primary_text=title, + icon=activity_icon) + + if misc.get_activities(metadata) or misc.is_bundle(metadata): + if metadata.get('activity_id', ''): + resume_label = _('Resume') + resume_with_label = _('Resume with') + else: + resume_label = _('Start') + resume_with_label = _('Start with') + menu_item = MenuItem(resume_label, 'activity-start') + menu_item.connect('activate', self.__start_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(resume_with_label, 'activity-start') + self.menu.append(menu_item) + menu_item.show() + start_with_menu = StartWithMenu(self._metadata) + menu_item.set_submenu(start_with_menu) + + else: + menu_item = MenuItem(_('No activity to start entry')) + menu_item.set_sensitive(False) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Copy to')) + icon = Icon(icon_name='edit-copy', xo_color=color, + icon_size=gtk.ICON_SIZE_MENU) + menu_item.set_image(icon) + self.menu.append(menu_item) + menu_item.show() + copy_menu = CopyMenu(metadata) + copy_menu.connect('volume-error', self.__volume_error_cb) + menu_item.set_submenu(copy_menu) + + if self._metadata['mountpoint'] == '/': + menu_item = MenuItem(_('Duplicate')) + icon = Icon(icon_name='edit-duplicate', xo_color=color, + icon_size=gtk.ICON_SIZE_MENU) + menu_item.set_image(icon) + menu_item.connect('activate', self.__duplicate_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Send to'), 'document-send') + self.menu.append(menu_item) + menu_item.show() + + friends_menu = FriendsMenu() + friends_menu.connect('friend-selected', self.__friend_selected_cb) + menu_item.set_submenu(friends_menu) + + if detail == True: + menu_item = MenuItem(_('View Details'), 'go-right') + menu_item.connect('activate', self.__detail_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Erase'), 'list-remove') + menu_item.connect('activate', self.__erase_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __start_activate_cb(self, menu_item): + misc.resume(self._metadata) + + def __duplicate_activate_cb(self, menu_item): + file_path = model.get_file(self._metadata['uid']) + try: + model.copy(self._metadata, '/') + except IOError, e: + logging.exception('Error while copying the entry. %s', e.strerror) + self.emit('volume-error', + _('Error while copying the entry. %s') % e.strerror, + _('Error')) + + def __erase_activate_cb(self, menu_item): + model.delete(self._metadata['uid']) + + def __detail_activate_cb(self, menu_item): + self.emit('detail-clicked', self._metadata['uid']) + + def __volume_error_cb(self, menu_item, message, severity): + self.emit('volume-error', message, severity) + + def __friend_selected_cb(self, menu_item, buddy): + logging.debug('__friend_selected_cb') + file_name = model.get_file(self._metadata['uid']) + + if not file_name or not os.path.exists(file_name): + logging.warn('Entries without a file cannot be sent.') + self.emit('volume-error', + _('Entries without a file cannot be sent.'), + _('Warning')) + return + + title = str(self._metadata['title']) + description = str(self._metadata.get('description', '')) + mime_type = str(self._metadata['mime_type']) + + if not mime_type: + mime_type = mime.get_for_file(file_name) + + filetransfer.start_transfer(buddy, file_name, title, description, + mime_type) + + +class CopyMenu(gtk.Menu): + __gtype_name__ = 'JournalCopyMenu' + + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self, metadata): + gobject.GObject.__init__(self) + + self._metadata = metadata + + clipboard_menu = ClipboardMenu(self._metadata) + clipboard_menu.set_image(Icon(icon_name='toolbar-edit', + icon_size=gtk.ICON_SIZE_MENU)) + clipboard_menu.connect('volume-error', self.__volume_error_cb) + self.append(clipboard_menu) + clipboard_menu.show() + + if self._metadata['mountpoint'] != '/': + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + journal_menu = VolumeMenu(self._metadata, _('Journal'), '/') + journal_menu.set_image(Icon(icon_name='activity-journal', + xo_color=color, + icon_size=gtk.ICON_SIZE_MENU)) + journal_menu.connect('volume-error', self.__volume_error_cb) + self.append(journal_menu) + journal_menu.show() + + volume_monitor = gio.volume_monitor_get() + icon_theme = gtk.icon_theme_get_default() + for mount in volume_monitor.get_mounts(): + if self._metadata['mountpoint'] == mount.get_root().get_path(): + continue + volume_menu = VolumeMenu(self._metadata, mount.get_name(), + mount.get_root().get_path()) + for name in mount.get_icon().props.names: + if icon_theme.has_icon(name): + volume_menu.set_image(Icon(icon_name=name, + icon_size=gtk.ICON_SIZE_MENU)) + break + volume_menu.connect('volume-error', self.__volume_error_cb) + self.append(volume_menu) + volume_menu.show() + + def __volume_error_cb(self, menu_item, message, severity): + self.emit('volume-error', message, severity) + + +class VolumeMenu(MenuItem): + __gtype_name__ = 'JournalVolumeMenu' + + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self, metadata, label, mount_point): + MenuItem.__init__(self, label) + self._metadata = metadata + self.connect('activate', self.__copy_to_volume_cb, mount_point) + + def __copy_to_volume_cb(self, menu_item, mount_point): + file_path = model.get_file(self._metadata['uid']) + + if not file_path or not os.path.exists(file_path): + logging.warn('Entries without a file cannot be copied.') + self.emit('volume-error', + _('Entries without a file cannot be copied.'), + _('Warning')) + return + + try: + model.copy(self._metadata, mount_point) + except IOError, e: + logging.exception('Error while copying the entry. %s', e.strerror) + self.emit('volume-error', + _('Error while copying the entry. %s') % e.strerror, + _('Error')) + + +class ClipboardMenu(MenuItem): + __gtype_name__ = 'JournalClipboardMenu' + + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self, metadata): + MenuItem.__init__(self, _('Clipboard')) + + self._temp_file_path = None + self._metadata = metadata + self.connect('activate', self.__copy_to_clipboard_cb) + + def __copy_to_clipboard_cb(self, menu_item): + file_path = model.get_file(self._metadata['uid']) + if not file_path or not os.path.exists(file_path): + logging.warn('Entries without a file cannot be copied.') + self.emit('volume-error', + _('Entries without a file cannot be copied.'), + _('Warning')) + return + + clipboard = gtk.Clipboard() + clipboard.set_with_data([('text/uri-list', 0, 0)], + self.__clipboard_get_func_cb, + self.__clipboard_clear_func_cb) + + def __clipboard_get_func_cb(self, clipboard, selection_data, info, data): + # Get hold of a reference so the temp file doesn't get deleted + self._temp_file_path = model.get_file(self._metadata['uid']) + logging.debug('__clipboard_get_func_cb %r', self._temp_file_path) + selection_data.set_uris(['file://' + self._temp_file_path]) + + def __clipboard_clear_func_cb(self, clipboard, data): + # Release and delete the temp file + self._temp_file_path = None + + +class FriendsMenu(gtk.Menu): + __gtype_name__ = 'JournalFriendsMenu' + + __gsignals__ = { + 'friend-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + if filetransfer.file_transfer_available(): + friends_model = friends.get_model() + for friend in friends_model: + if friend.is_present(): + menu_item = MenuItem(text_label=friend.get_nick(), + icon_name='computer-xo', + xo_color=friend.get_color()) + menu_item.connect('activate', self.__item_activate_cb, + friend) + self.append(menu_item) + menu_item.show() + + if not self.get_children(): + menu_item = MenuItem(_('No friends present')) + menu_item.set_sensitive(False) + self.append(menu_item) + menu_item.show() + else: + menu_item = MenuItem(_('No valid connection found')) + menu_item.set_sensitive(False) + self.append(menu_item) + menu_item.show() + + def __item_activate_cb(self, menu_item, friend): + self.emit('friend-selected', friend) + + +class StartWithMenu(gtk.Menu): + __gtype_name__ = 'JournalStartWithMenu' + + def __init__(self, metadata): + gobject.GObject.__init__(self) + + self._metadata = metadata + + for activity_info in misc.get_activities(metadata): + menu_item = MenuItem(activity_info.get_name()) + menu_item.set_image(Icon(file=activity_info.get_icon(), + icon_size=gtk.ICON_SIZE_MENU)) + menu_item.connect('activate', self.__item_activate_cb, + activity_info.get_bundle_id()) + self.append(menu_item) + menu_item.show() + + if not self.get_children(): + if metadata.get('activity_id', ''): + resume_label = _('No activity to resume entry') + else: + resume_label = _('No activity to start entry') + menu_item = MenuItem(resume_label) + menu_item.set_sensitive(False) + self.append(menu_item) + menu_item.show() + + def __item_activate_cb(self, menu_item, service_name): + mime_type = self._metadata.get('mime_type', '') + if mime_type: + mime_registry = mimeregistry.get_registry() + mime_registry.set_default_activity(mime_type, service_name) + misc.resume(self._metadata, service_name) + + +class BuddyPalette(Palette): + def __init__(self, buddy): + self._buddy = buddy + + nick, colors = buddy + buddy_icon = Icon(icon_name='computer-xo', + icon_size=style.STANDARD_ICON_SIZE, + xo_color=XoColor(colors)) + + Palette.__init__(self, primary_text=glib.markup_escape_text(nick), + icon=buddy_icon) + + # TODO: Support actions on buddies, like make friend, invite, etc. diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py new file mode 100644 index 0000000..71b6ea8 --- /dev/null +++ b/src/jarabe/journal/volumestoolbar.py @@ -0,0 +1,404 @@ +# Copyright (C) 2007, 2011, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import os +import statvfs +from gettext import gettext as _ + +import gobject +import gio +import glib +import gtk +import gconf +import cPickle +import xapian +import simplejson +import tempfile +import shutil + +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.palette import Palette +from sugar.graphics.xocolor import XoColor +from sugar import env + +from jarabe.journal import model +from jarabe.view.palettes import VolumePalette + + +_JOURNAL_0_METADATA_DIR = '.olpc.store' + + +def _get_id(document): + """Get the ID for the document in the xapian database.""" + tl = document.termlist() + try: + term = tl.skip_to('Q').term + if len(term) == 0 or term[0] != 'Q': + return None + return term[1:] + except StopIteration: + return None + + +def _convert_entries(root): + """Convert entries written by the datastore version 0. + + The metadata and the preview will be written using the new + scheme for writing Journal entries to removable storage + devices. + + - entries that do not have an associated file are not + converted. + - if an entry has no title we set it to Untitled and rename + the file accordingly, taking care of creating a unique + filename + + """ + try: + database = xapian.Database(os.path.join(root, _JOURNAL_0_METADATA_DIR, + 'index')) + except xapian.DatabaseError: + logging.exception('Convert DS-0 Journal entries: error reading db: %s', + os.path.join(root, _JOURNAL_0_METADATA_DIR, 'index')) + return + + metadata_dir_path = os.path.join(root, model.JOURNAL_METADATA_DIR) + if not os.path.exists(metadata_dir_path): + try: + os.mkdir(metadata_dir_path) + except EnvironmentError: + logging.error('Convert DS-0 Journal entries: ' + 'error creating the Journal metadata directory.') + return + + for posting_item in database.postlist(''): + try: + document = database.get_document(posting_item.docid) + except xapian.DocNotFoundError, e: + logging.debug('Convert DS-0 Journal entries: error getting ' + 'document %s: %s', posting_item.docid, e) + continue + _convert_entry(root, document) + + +def _convert_entry(root, document): + try: + metadata_loaded = cPickle.loads(document.get_data()) + except cPickle.PickleError, e: + logging.debug('Convert DS-0 Journal entries: ' + 'error converting metadata: %s', e) + return + + if not ('activity_id' in metadata_loaded and + 'mime_type' in metadata_loaded and + 'title' in metadata_loaded): + return + + metadata = {} + + uid = _get_id(document) + if uid is None: + return + + for key, value in metadata_loaded.items(): + metadata[str(key)] = str(value[0]) + + if 'uid' not in metadata: + metadata['uid'] = uid + + filename = metadata.pop('filename', None) + if not filename: + return + if not os.path.exists(os.path.join(root, filename)): + return + + if not metadata.get('title'): + metadata['title'] = _('Untitled') + fn = model.get_file_name(metadata['title'], + metadata['mime_type']) + new_filename = model.get_unique_file_name(root, fn) + os.rename(os.path.join(root, filename), + os.path.join(root, new_filename)) + filename = new_filename + + preview_path = os.path.join(root, _JOURNAL_0_METADATA_DIR, + 'preview', uid) + if os.path.exists(preview_path): + preview_fname = filename + '.preview' + new_preview_path = os.path.join(root, + model.JOURNAL_METADATA_DIR, + preview_fname) + if not os.path.exists(new_preview_path): + shutil.copy(preview_path, new_preview_path) + + metadata_fname = filename + '.metadata' + metadata_path = os.path.join(root, model.JOURNAL_METADATA_DIR, + metadata_fname) + if not os.path.exists(metadata_path): + (fh, fn) = tempfile.mkstemp(dir=root) + os.write(fh, simplejson.dumps(metadata)) + os.close(fh) + os.rename(fn, metadata_path) + + logging.debug('Convert DS-0 Journal entries: entry converted: ' + 'file=%s metadata=%s', + os.path.join(root, filename), metadata) + + +class VolumesToolbar(gtk.Toolbar): + __gtype_name__ = 'VolumesToolbar' + + __gsignals__ = { + 'volume-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self): + gtk.Toolbar.__init__(self) + self._mount_added_hid = None + self._mount_removed_hid = None + + button = JournalButton() + button.connect('toggled', self._button_toggled_cb) + self.insert(button, 0) + button.show() + self._volume_buttons = [button] + + self.connect('destroy', self.__destroy_cb) + + gobject.idle_add(self._set_up_volumes) + + def __destroy_cb(self, widget): + volume_monitor = gio.volume_monitor_get() + volume_monitor.disconnect(self._mount_added_hid) + volume_monitor.disconnect(self._mount_removed_hid) + + def _set_up_volumes(self): + self._set_up_documents_button() + + volume_monitor = gio.volume_monitor_get() + self._mount_added_hid = volume_monitor.connect('mount-added', + self.__mount_added_cb) + self._mount_removed_hid = volume_monitor.connect('mount-removed', + self.__mount_removed_cb) + + for mount in volume_monitor.get_mounts(): + self._add_button(mount) + + def _set_up_documents_button(self): + documents_path = model.get_documents_path() + if documents_path is not None: + button = DocumentsButton(documents_path) + button.props.group = self._volume_buttons[0] + label = glib.markup_escape_text(_('Documents')) + button.set_palette(Palette(label)) + button.connect('toggled', self._button_toggled_cb) + button.show() + + position = self.get_item_index(self._volume_buttons[-1]) + 1 + self.insert(button, position) + self._volume_buttons.append(button) + self.show() + + def __mount_added_cb(self, volume_monitor, mount): + self._add_button(mount) + + def __mount_removed_cb(self, volume_monitor, mount): + self._remove_button(mount) + + def _add_button(self, mount): + logging.debug('VolumeToolbar._add_button: %r', mount.get_name()) + + if os.path.exists(os.path.join(mount.get_root().get_path(), + _JOURNAL_0_METADATA_DIR)): + logging.debug('Convert DS-0 Journal entries: starting conversion') + gobject.idle_add(_convert_entries, mount.get_root().get_path()) + + button = VolumeButton(mount) + button.props.group = self._volume_buttons[0] + button.connect('toggled', self._button_toggled_cb) + button.connect('volume-error', self.__volume_error_cb) + position = self.get_item_index(self._volume_buttons[-1]) + 1 + self.insert(button, position) + button.show() + + self._volume_buttons.append(button) + + if len(self.get_children()) > 1: + self.show() + + def __volume_error_cb(self, button, strerror, severity): + self.emit('volume-error', strerror, severity) + + def _button_toggled_cb(self, button): + if button.props.active: + self.emit('volume-changed', button.mount_point) + + def _unmount_activated_cb(self, menu_item, mount): + logging.debug('VolumesToolbar._unmount_activated_cb: %r', mount) + mount.unmount(self.__unmount_cb) + + def __unmount_cb(self, source, result): + logging.debug('__unmount_cb %r %r', source, result) + + def _get_button_for_mount(self, mount): + mount_point = mount.get_root().get_path() + for button in self.get_children(): + if button.mount_point == mount_point: + return button + logging.error('Couldnt find button with mount_point %r', mount_point) + return None + + def _remove_button(self, mount): + button = self._get_button_for_mount(mount) + self._volume_buttons.remove(button) + self.remove(button) + self.get_children()[0].props.active = True + + if len(self.get_children()) < 2: + self.hide() + + def set_active_volume(self, mount): + button = self._get_button_for_mount(mount) + button.props.active = True + + +class BaseButton(RadioToolButton): + __gsignals__ = { + 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str, str])), + } + + def __init__(self, mount_point): + RadioToolButton.__init__(self) + + self.mount_point = mount_point + + self.drag_dest_set(gtk.DEST_DEFAULT_ALL, + [('journal-object-id', 0, 0)], + gtk.gdk.ACTION_COPY) + self.connect('drag-data-received', self._drag_data_received_cb) + + def _drag_data_received_cb(self, widget, drag_context, x, y, + selection_data, info, timestamp): + object_id = selection_data.data + metadata = model.get(object_id) + file_path = model.get_file(metadata['uid']) + if not file_path or not os.path.exists(file_path): + logging.warn('Entries without a file cannot be copied.') + self.emit('volume-error', + _('Entries without a file cannot be copied.'), + _('Warning')) + return + + try: + model.copy(metadata, self.mount_point) + except IOError, e: + logging.exception('Error while copying the entry. %s', e.strerror) + self.emit('volume-error', + _('Error while copying the entry. %s') % e.strerror, + _('Error')) + + +class VolumeButton(BaseButton): + def __init__(self, mount): + self._mount = mount + mount_point = mount.get_root().get_path() + BaseButton.__init__(self, mount_point) + + icon_name = None + icon_theme = gtk.icon_theme_get_default() + for icon_name in mount.get_icon().props.names: + icon_info = icon_theme.lookup_icon(icon_name, + gtk.ICON_SIZE_LARGE_TOOLBAR, 0) + if icon_info is not None: + break + + if icon_name is None: + icon_name = 'drive' + + self.props.named_icon = icon_name + + # TODO: retrieve the colors from the owner of the device + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + self.props.xo_color = color + + def create_palette(self): + palette = VolumePalette(self._mount) + #palette.props.invoker = FrameWidgetInvoker(self) + #palette.set_group_id('frame') + return palette + + +class JournalButton(BaseButton): + def __init__(self): + BaseButton.__init__(self, mount_point='/') + + self.props.named_icon = 'activity-journal' + + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + self.props.xo_color = color + + def create_palette(self): + palette = JournalButtonPalette(self) + return palette + + +class JournalButtonPalette(Palette): + + def __init__(self, mount): + Palette.__init__(self, glib.markup_escape_text(_('Journal'))) + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self._progress_bar = gtk.ProgressBar() + vbox.add(self._progress_bar) + self._progress_bar.show() + + self._free_space_label = gtk.Label() + self._free_space_label.set_alignment(0.5, 0.5) + vbox.add(self._free_space_label) + self._free_space_label.show() + + self.connect('popup', self.__popup_cb) + + def __popup_cb(self, palette): + stat = os.statvfs(env.get_profile_path()) + free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL] + total_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BLOCKS] + + fraction = (total_space - free_space) / float(total_space) + self._progress_bar.props.fraction = fraction + self._free_space_label.props.label = _('%(free_space)d MB Free') % \ + {'free_space': free_space / (1024 * 1024)} + + +class DocumentsButton(BaseButton): + + def __init__(self, documents_path): + BaseButton.__init__(self, mount_point=documents_path) + + self.props.named_icon = 'user-documents' + + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + self.props.xo_color = color diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am new file mode 100644 index 0000000..2fc6b1c --- /dev/null +++ b/src/jarabe/model/Makefile.am @@ -0,0 +1,20 @@ +sugardir = $(pythondir)/jarabe/model +sugar_PYTHON = \ + adhoc.py \ + __init__.py \ + buddy.py \ + bundleregistry.py \ + filetransfer.py \ + friends.py \ + invites.py \ + olpcmesh.py \ + mimeregistry.py \ + neighborhood.py \ + network.py \ + notifications.py \ + shell.py \ + screen.py \ + session.py \ + sound.py \ + speech.py \ + telepathyclient.py diff --git a/src/jarabe/model/Makefile.in b/src/jarabe/model/Makefile.in new file mode 100644 index 0000000..f76fc87 --- /dev/null +++ b/src/jarabe/model/Makefile.in @@ -0,0 +1,457 @@ +# 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/model +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/model +sugar_PYTHON = \ + adhoc.py \ + __init__.py \ + buddy.py \ + bundleregistry.py \ + filetransfer.py \ + friends.py \ + invites.py \ + olpcmesh.py \ + mimeregistry.py \ + neighborhood.py \ + network.py \ + notifications.py \ + shell.py \ + screen.py \ + session.py \ + sound.py \ + speech.py \ + telepathyclient.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/model/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/model/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/model/__init__.py b/src/jarabe/model/__init__.py new file mode 100644 index 0000000..85f6a24 --- /dev/null +++ b/src/jarabe/model/__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/model/adhoc.py b/src/jarabe/model/adhoc.py new file mode 100644 index 0000000..68a9aa3 --- /dev/null +++ b/src/jarabe/model/adhoc.py @@ -0,0 +1,282 @@ +# Copyright (C) 2010 One Laptop per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +import gobject + +from jarabe.model import network +from jarabe.model.network import Settings +from sugar.util import unique_id +from jarabe.model.network import IP4Config + + +_adhoc_manager_instance = None + + +def get_adhoc_manager_instance(): + global _adhoc_manager_instance + if _adhoc_manager_instance is None: + _adhoc_manager_instance = AdHocManager() + return _adhoc_manager_instance + + +class AdHocManager(gobject.GObject): + """To mimic the mesh behavior on devices where mesh hardware is + not available we support the creation of an Ad-hoc network on + three channels 1, 6, 11. If Sugar sees no "known" network when it + starts, it does autoconnect to an Ad-hoc network. + + """ + + __gsignals__ = { + 'members-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + } + + _AUTOCONNECT_TIMEOUT = 60 + _CHANNEL_1 = 1 + _CHANNEL_6 = 6 + _CHANNEL_11 = 11 + + def __init__(self): + gobject.GObject.__init__(self) + + self._bus = dbus.SystemBus() + self._device = None + self._idle_source = 0 + self._listening_called = 0 + self._device_state = network.NM_DEVICE_STATE_UNKNOWN + + self._current_channel = None + self._networks = {self._CHANNEL_1: None, + self._CHANNEL_6: None, + self._CHANNEL_11: None} + + for channel in (self._CHANNEL_1, self._CHANNEL_6, self._CHANNEL_11): + if not self._find_connection(channel): + self._add_connection(channel) + + def start_listening(self, device): + self._listening_called += 1 + if self._listening_called > 1: + raise RuntimeError('The start listening method can' \ + ' only be called once.') + + self._device = device + props = dbus.Interface(device, dbus.PROPERTIES_IFACE) + self._device_state = props.Get(network.NM_DEVICE_IFACE, 'State') + + 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 stop_listening(self): + self._listening_called = 0 + 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) + + def __device_state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update_state() + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveAccessPoint' in properties and \ + properties['ActiveAccessPoint'] != '/': + active_ap = self._bus.get_object(network.NM_SERVICE, + properties['ActiveAccessPoint']) + props = dbus.Interface(active_ap, dbus.PROPERTIES_IFACE) + props.GetAll(network.NM_ACCESSPOINT_IFACE, byte_arrays=True, + reply_handler=self.__get_all_ap_props_reply_cb, + error_handler=self.__get_all_ap_props_error_cb) + + def __get_all_ap_props_reply_cb(self, properties): + if properties['Mode'] == network.NM_802_11_MODE_ADHOC and \ + 'Frequency' in properties: + frequency = properties['Frequency'] + self._current_channel = network.frequency_to_channel(frequency) + else: + self._current_channel = None + self._update_state() + + def __get_all_ap_props_error_cb(self, err): + logging.error('Error getting the access point properties: %s', err) + + def _update_state(self): + self.emit('state-changed', self._current_channel, self._device_state) + + def autoconnect(self): + """Start a timer which basically looks for 30 seconds of inactivity + on the device, then does autoconnect to an Ad-hoc network. + + This function may be called early on (e.g. when the device is still + in NM_DEVICE_STATE_UNMANAGED). It is assumed that initialisation + will complete quickly, and long before the timeout ticks. + """ + if self._idle_source != 0: + gobject.source_remove(self._idle_source) + self._idle_source = gobject.timeout_add_seconds( + self._AUTOCONNECT_TIMEOUT, self.__idle_check_cb) + + def __idle_check_cb(self): + if self._device_state == network.NM_DEVICE_STATE_DISCONNECTED: + logging.debug('Connect to Ad-hoc network due to inactivity.') + self._autoconnect_adhoc() + else: + logging.debug('autoconnect Sugar Ad-hoc: already connected') + return False + + def _autoconnect_adhoc(self): + """First we try if there is an Ad-hoc network that is used by other + learners in the area, if not we default to channel 1. + + """ + if self._networks[self._CHANNEL_1] is not None: + self.activate_channel(self._CHANNEL_1) + elif self._networks[self._CHANNEL_6] is not None: + self.activate_channel(self._CHANNEL_6) + elif self._networks[self._CHANNEL_11] is not None: + self.activate_channel(self._CHANNEL_11) + else: + self.activate_channel(self._CHANNEL_1) + + def activate_channel(self, channel): + """Activate a sugar Ad-hoc network. + + Keyword arguments: + channel -- Channel to connect to (should be 1, 6, 11) + + """ + connection = self._find_connection(channel) + if connection: + connection.activate(self._device.object_path) + + @staticmethod + def _get_connection_id(channel): + return '%s%d' % (network.ADHOC_CONNECTION_ID_PREFIX, channel) + + def _add_connection(self, channel): + ssid = 'Ad-hoc Network %d' % (channel,) + settings = Settings() + settings.connection.id = self._get_connection_id(channel) + settings.connection.uuid = unique_id() + settings.connection.type = '802-11-wireless' + settings.connection.autoconnect = False + settings.wireless.ssid = dbus.ByteArray(ssid) + settings.wireless.band = 'bg' + settings.wireless.channel = channel + settings.wireless.mode = 'adhoc' + settings.ip4_config = IP4Config() + settings.ip4_config.method = 'link-local' + network.add_connection(settings) + + def _find_connection(self, channel): + connection_id = self._get_connection_id(channel) + return network.find_connection_by_id(connection_id) + + def deactivate_active_channel(self): + """Deactivate the current active channel.""" + obj = self._bus.get_object(network.NM_SERVICE, network.NM_PATH) + netmgr = dbus.Interface(obj, network.NM_IFACE) + + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + netmgr_props.Get(network.NM_IFACE, 'ActiveConnections', \ + reply_handler=self.__get_active_connections_reply_cb, + error_handler=self.__get_active_connections_error_cb) + + def __get_active_connections_reply_cb(self, active_connections_o): + for connection_o in active_connections_o: + obj = self._bus.get_object(network.NM_IFACE, connection_o) + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) + state = props.Get(network.NM_ACTIVE_CONN_IFACE, 'State') + if state == network.NM_ACTIVE_CONNECTION_STATE_ACTIVATED: + access_point_o = props.Get(network.NM_ACTIVE_CONN_IFACE, + 'SpecificObject') + if access_point_o != '/': + obj = self._bus.get_object(network.NM_SERVICE, network.NM_PATH) + netmgr = dbus.Interface(obj, network.NM_IFACE) + netmgr.DeactivateConnection(connection_o) + + def __get_active_connections_error_cb(self, err): + logging.error('Error getting the active connections: %s', err) + + def __activate_reply_cb(self, connection): + logging.debug('Ad-hoc network created: %s', connection) + + def __activate_error_cb(self, err): + logging.error('Failed to create Ad-hoc network: %s', err) + + def add_access_point(self, access_point): + """Add an access point to a network and notify the view to idicate + the member change. + + Keyword arguments: + access_point -- Access Point + + """ + if access_point.ssid.endswith(' 1'): + self._networks[self._CHANNEL_1] = access_point + self.emit('members-changed', self._CHANNEL_1, True) + elif access_point.ssid.endswith(' 6'): + self._networks[self._CHANNEL_6] = access_point + self.emit('members-changed', self._CHANNEL_6, True) + elif access_point.ssid.endswith('11'): + self._networks[self._CHANNEL_11] = access_point + self.emit('members-changed', self._CHANNEL_11, True) + + def is_sugar_adhoc_access_point(self, ap_object_path): + """Checks whether an access point is part of a sugar Ad-hoc network. + + Keyword arguments: + ap_object_path -- Access Point object path + + Return: Boolean + + """ + for access_point in self._networks.values(): + if access_point is not None: + if access_point.model.object_path == ap_object_path: + return True + return False + + def remove_access_point(self, ap_object_path): + """Remove an access point from a sugar Ad-hoc network. + + Keyword arguments: + ap_object_path -- Access Point object path + + """ + for channel in self._networks: + if self._networks[channel] is not None: + if self._networks[channel].model.object_path == ap_object_path: + self.emit('members-changed', channel, False) + self._networks[channel] = None + break diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py new file mode 100644 index 0000000..8f17d7e --- /dev/null +++ b/src/jarabe/model/buddy.py @@ -0,0 +1,213 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gobject +import gconf +import dbus +from telepathy.client import Connection +from telepathy.interfaces import CONNECTION + +from sugar.graphics.xocolor import XoColor +from sugar.profile import get_profile + +from jarabe.util.telepathy import connection_watcher + + +CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' + +_owner_instance = None + + +class BaseBuddyModel(gobject.GObject): + __gtype_name__ = 'SugarBaseBuddyModel' + + def __init__(self, **kwargs): + self._key = None + self._nick = None + self._color = None + self._tags = None + self._current_activity = None + + gobject.GObject.__init__(self, **kwargs) + + def get_nick(self): + return self._nick + + def set_nick(self, nick): + self._nick = nick + + nick = gobject.property(type=object, getter=get_nick, setter=set_nick) + + def get_key(self): + return self._key + + def set_key(self, key): + self._key = key + + key = gobject.property(type=object, getter=get_key, setter=set_key) + + def get_color(self): + return self._color + + def set_color(self, color): + self._color = color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def get_tags(self): + return self._tags + + tags = gobject.property(type=object, getter=get_tags) + + def get_current_activity(self): + return self._current_activity + + def set_current_activity(self, current_activity): + if self._current_activity != current_activity: + self._current_activity = current_activity + self.notify('current-activity') + + current_activity = gobject.property(type=object, + getter=get_current_activity, + setter=set_current_activity) + + def is_owner(self): + raise NotImplementedError + + +class OwnerBuddyModel(BaseBuddyModel): + __gtype_name__ = 'SugarOwnerBuddyModel' + + def __init__(self): + BaseBuddyModel.__init__(self) + + client = gconf.client_get_default() + self.props.nick = client.get_string('/desktop/sugar/user/nick') + color = client.get_string('/desktop/sugar/user/color') + self.props.color = XoColor(color) + + self.props.key = get_profile().pubkey + + self.connect('notify::nick', self.__property_changed_cb) + self.connect('notify::color', self.__property_changed_cb) + + bus = dbus.SessionBus() + bus.add_signal_receiver( + self.__name_owner_changed_cb, + signal_name='NameOwnerChanged', + dbus_interface='org.freedesktop.DBus') + + bus_object = bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH) + for service in bus_object.ListNames( + dbus_interface=dbus.BUS_DAEMON_IFACE): + if service.startswith(CONNECTION + '.'): + path = '/%s' % service.replace('.', '/') + Connection(service, path, bus, + ready_handler=self.__connection_ready_cb) + + def __connection_ready_cb(self, connection): + self._sync_properties_on_connection(connection) + + def __name_owner_changed_cb(self, name, old, new): + if name.startswith(CONNECTION + '.') and not old and new: + path = '/' + name.replace('.', '/') + Connection(name, path, ready_handler=self.__connection_ready_cb) + + def __property_changed_cb(self, buddy, pspec): + self._sync_properties() + + def _sync_properties(self): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): + self._sync_properties_on_connection(connection) + + def _sync_properties_on_connection(self, connection): + if CONNECTION_INTERFACE_BUDDY_INFO in connection: + properties = {} + if self.props.key is not None: + properties['key'] = dbus.ByteArray(self.props.key) + if self.props.color is not None: + properties['color'] = self.props.color.to_string() + + logging.debug('calling SetProperties with %r', properties) + connection[CONNECTION_INTERFACE_BUDDY_INFO].SetProperties( + properties, + reply_handler=self.__set_properties_cb, + error_handler=self.__error_handler_cb) + + def __set_properties_cb(self): + logging.debug('__set_properties_cb') + + def __error_handler_cb(self, error): + raise RuntimeError(error) + + def __connection_added_cb(self, conn_watcher, connection): + self._sync_properties_on_connection(connection) + + def is_owner(self): + return True + + +def get_owner_instance(): + global _owner_instance + if _owner_instance is None: + _owner_instance = OwnerBuddyModel() + return _owner_instance + + +class BuddyModel(BaseBuddyModel): + __gtype_name__ = 'SugarBuddyModel' + + def __init__(self, **kwargs): + + self._account = None + self._contact_id = None + self._handle = None + + BaseBuddyModel.__init__(self, **kwargs) + + def is_owner(self): + return False + + def get_account(self): + return self._account + + def set_account(self, account): + self._account = account + + account = gobject.property(type=object, getter=get_account, + setter=set_account) + + def get_contact_id(self): + return self._contact_id + + def set_contact_id(self, contact_id): + self._contact_id = contact_id + + contact_id = gobject.property(type=object, getter=get_contact_id, + setter=set_contact_id) + + def get_handle(self): + return self._handle + + def set_handle(self, handle): + self._handle = handle + + handle = gobject.property(type=object, getter=get_handle, + setter=set_handle) diff --git a/src/jarabe/model/bundleregistry.py b/src/jarabe/model/bundleregistry.py new file mode 100644 index 0000000..26e719f --- /dev/null +++ b/src/jarabe/model/bundleregistry.py @@ -0,0 +1,450 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2009 Aleksey Lim +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +import gconf +import gobject +import gio +import simplejson + +from sugar.bundle.activitybundle import ActivityBundle +from sugar.bundle.contentbundle import ContentBundle +from sugar.bundle.bundleversion import NormalizedVersion +from jarabe.journal.journalentrybundle import JournalEntryBundle +from sugar.bundle.bundle import MalformedBundleException, \ + AlreadyInstalledException, RegistrationException +from sugar import env + +from jarabe import config +from jarabe.model import mimeregistry + + +_instance = None + + +class BundleRegistry(gobject.GObject): + """Tracks the available activity bundles""" + + __gsignals__ = { + 'bundle-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'bundle-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'bundle-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self): + logging.debug('STARTUP: Loading the bundle registry') + gobject.GObject.__init__(self) + + self._mime_defaults = self._load_mime_defaults() + + self._bundles = [] + # hold a reference to the monitors so they don't get disposed + self._gio_monitors = [] + + user_path = env.get_user_activities_path() + for activity_dir in [user_path, config.activities_path]: + self._scan_directory(activity_dir) + directory = gio.File(activity_dir) + monitor = directory.monitor_directory() + monitor.connect('changed', self.__file_monitor_changed_cb) + self._gio_monitors.append(monitor) + + self._last_defaults_mtime = -1 + self._favorite_bundles = {} + + client = gconf.client_get_default() + self._protected_activities = client.get_list( + '/desktop/sugar/protected_activities', + gconf.VALUE_STRING) + + if self._protected_activities is None: + self._protected_activities = [] + + try: + self._load_favorites() + except Exception: + logging.exception('Error while loading favorite_activities.') + + self._merge_default_favorites() + + def __file_monitor_changed_cb(self, monitor, one_file, other_file, + event_type): + if not one_file.get_path().endswith('.activity'): + return + if event_type == gio.FILE_MONITOR_EVENT_CREATED: + self.add_bundle(one_file.get_path(), install_mime_type=True) + elif event_type == gio.FILE_MONITOR_EVENT_DELETED: + self.remove_bundle(one_file.get_path()) + + def _load_mime_defaults(self): + defaults = {} + + f = open(os.path.join(config.data_path, 'mime.defaults'), 'r') + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + mime = line[:line.find(' ')] + handler = line[line.rfind(' ') + 1:] + defaults[mime] = handler + f.close() + + return defaults + + def _get_favorite_key(self, bundle_id, version): + """We use a string as a composite key for the favorites dictionary + because JSON doesn't support tuples and python won't accept a list + as a dictionary key. + """ + if ' ' in bundle_id: + raise ValueError('bundle_id cannot contain spaces') + return '%s %s' % (bundle_id, version) + + def _load_favorites(self): + favorites_path = env.get_profile_path('favorite_activities') + if os.path.exists(favorites_path): + favorites_data = simplejson.load(open(favorites_path)) + + favorite_bundles = favorites_data['favorites'] + if not isinstance(favorite_bundles, dict): + raise ValueError('Invalid format in %s.' % favorites_path) + if favorite_bundles: + first_key = favorite_bundles.keys()[0] + if not isinstance(first_key, basestring): + raise ValueError('Invalid format in %s.' % favorites_path) + + first_value = favorite_bundles.values()[0] + if first_value is not None and \ + not isinstance(first_value, dict): + raise ValueError('Invalid format in %s.' % favorites_path) + + self._last_defaults_mtime = float(favorites_data['defaults-mtime']) + self._favorite_bundles = favorite_bundles + + def _merge_default_favorites(self): + default_activities = [] + defaults_path = os.path.join(config.data_path, 'activities.defaults') + if os.path.exists(defaults_path): + file_mtime = os.stat(defaults_path).st_mtime + if file_mtime > self._last_defaults_mtime: + f = open(defaults_path, 'r') + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + default_activities.append(line) + f.close() + self._last_defaults_mtime = file_mtime + + if not default_activities: + return + + for bundle_id in default_activities: + max_version = '0' + for bundle in self._bundles: + if bundle.get_bundle_id() == bundle_id and \ + NormalizedVersion(max_version) < \ + NormalizedVersion(bundle.get_activity_version()): + max_version = bundle.get_activity_version() + + key = self._get_favorite_key(bundle_id, max_version) + if NormalizedVersion(max_version) > NormalizedVersion('0') and \ + key not in self._favorite_bundles: + self._favorite_bundles[key] = None + + logging.debug('After merging: %r', self._favorite_bundles) + + self._write_favorites_file() + + def get_bundle(self, bundle_id): + """Returns an bundle given his service name""" + for bundle in self._bundles: + if bundle.get_bundle_id() == bundle_id: + return bundle + return None + + def __iter__(self): + return self._bundles.__iter__() + + def __len__(self): + return len(self._bundles) + + def _scan_directory(self, path): + if not os.path.isdir(path): + return + + # Sort by mtime to ensure a stable activity order + bundles = {} + for f in os.listdir(path): + if not f.endswith('.activity'): + continue + try: + bundle_dir = os.path.join(path, f) + if os.path.isdir(bundle_dir): + bundles[bundle_dir] = os.stat(bundle_dir).st_mtime + except Exception: + logging.exception('Error while processing installed activity' + ' bundle %s:', bundle_dir) + + bundle_dirs = bundles.keys() + bundle_dirs.sort(lambda d1, d2: cmp(bundles[d1], bundles[d2])) + for folder in bundle_dirs: + try: + self._add_bundle(folder) + except: + # pylint: disable=W0702 + logging.exception('Error while processing installed activity' + ' bundle %s:', folder) + + def add_bundle(self, bundle_path, install_mime_type=False): + bundle = self._add_bundle(bundle_path, install_mime_type) + if bundle is not None: + self._set_bundle_favorite(bundle.get_bundle_id(), + bundle.get_activity_version(), + True) + self.emit('bundle-added', bundle) + return True + else: + return False + + def _add_bundle(self, bundle_path, install_mime_type=False): + logging.debug('STARTUP: Adding bundle %r', bundle_path) + try: + bundle = ActivityBundle(bundle_path) + if install_mime_type: + bundle.install_mime_type(bundle_path) + except MalformedBundleException: + logging.exception('Error loading bundle %r', bundle_path) + return None + + bundle_id = bundle.get_bundle_id() + installed = self.get_bundle(bundle_id) + + if installed is not None: + if NormalizedVersion(installed.get_activity_version()) >= \ + NormalizedVersion(bundle.get_activity_version()): + logging.debug('Skip old version for %s', bundle_id) + return None + else: + logging.debug('Upgrade %s', bundle_id) + self.remove_bundle(installed.get_path()) + + self._bundles.append(bundle) + return bundle + + def remove_bundle(self, bundle_path): + for bundle in self._bundles: + if bundle.get_path() == bundle_path: + self._bundles.remove(bundle) + self.emit('bundle-removed', bundle) + return True + return False + + def get_activities_for_type(self, mime_type): + result = [] + + mime = mimeregistry.get_registry() + default_bundle_id = mime.get_default_activity(mime_type) + default_bundle = None + + for bundle in self._bundles: + if mime_type in (bundle.get_mime_types() or []): + if bundle.get_bundle_id() == default_bundle_id: + default_bundle = bundle + elif self.get_default_for_type(mime_type) == \ + bundle.get_bundle_id(): + result.insert(0, bundle) + else: + result.append(bundle) + + if default_bundle is not None: + result.insert(0, default_bundle) + + return result + + def get_default_for_type(self, mime_type): + return self._mime_defaults.get(mime_type) + + def _find_bundle(self, bundle_id, version): + for bundle in self._bundles: + if bundle.get_bundle_id() == bundle_id and \ + bundle.get_activity_version() == version: + return bundle + raise ValueError('No bundle %r with version %r exists.' % \ + (bundle_id, version)) + + def set_bundle_favorite(self, bundle_id, version, favorite): + changed = self._set_bundle_favorite(bundle_id, version, favorite) + if changed: + bundle = self._find_bundle(bundle_id, version) + self.emit('bundle-changed', bundle) + + def _set_bundle_favorite(self, bundle_id, version, favorite): + key = self._get_favorite_key(bundle_id, version) + if favorite and not key in self._favorite_bundles: + self._favorite_bundles[key] = None + elif not favorite and key in self._favorite_bundles: + del self._favorite_bundles[key] + else: + return False + + self._write_favorites_file() + return True + + def is_bundle_favorite(self, bundle_id, version): + key = self._get_favorite_key(bundle_id, version) + return key in self._favorite_bundles + + def is_activity_protected(self, bundle_id): + return bundle_id in self._protected_activities + + def set_bundle_position(self, bundle_id, version, x, y): + key = self._get_favorite_key(bundle_id, version) + if key not in self._favorite_bundles: + raise ValueError('Bundle %s %s not favorite' % + (bundle_id, version)) + + if self._favorite_bundles[key] is None: + self._favorite_bundles[key] = {} + if 'position' not in self._favorite_bundles[key] or \ + [x, y] != self._favorite_bundles[key]['position']: + self._favorite_bundles[key]['position'] = [x, y] + else: + return + + self._write_favorites_file() + bundle = self._find_bundle(bundle_id, version) + self.emit('bundle-changed', bundle) + + def get_bundle_position(self, bundle_id, version): + """Get the coordinates where the user wants the representation of this + bundle to be displayed. Coordinates are relative to a 1000x1000 area. + """ + key = self._get_favorite_key(bundle_id, version) + if key not in self._favorite_bundles or \ + self._favorite_bundles[key] is None or \ + 'position' not in self._favorite_bundles[key]: + return (-1, -1) + else: + return tuple(self._favorite_bundles[key]['position']) + + def _write_favorites_file(self): + path = env.get_profile_path('favorite_activities') + favorites_data = {'defaults-mtime': self._last_defaults_mtime, + 'favorites': self._favorite_bundles} + simplejson.dump(favorites_data, open(path, 'w'), indent=1) + + def is_installed(self, bundle): + # TODO treat ContentBundle in special way + # needs rethinking while fixing ContentBundle support + if isinstance(bundle, ContentBundle) or \ + isinstance(bundle, JournalEntryBundle): + return bundle.is_installed() + + for installed_bundle in self._bundles: + if bundle.get_bundle_id() == installed_bundle.get_bundle_id() and \ + NormalizedVersion(bundle.get_activity_version()) == \ + NormalizedVersion(installed_bundle.get_activity_version()): + return True + return False + + def install(self, bundle, uid=None, force_downgrade=False): + activities_path = env.get_user_activities_path() + + for installed_bundle in self._bundles: + if bundle.get_bundle_id() == installed_bundle.get_bundle_id() and \ + NormalizedVersion(bundle.get_activity_version()) <= \ + NormalizedVersion(installed_bundle.get_activity_version()): + if not force_downgrade: + raise AlreadyInstalledException + else: + self.uninstall(installed_bundle, force=True) + elif bundle.get_bundle_id() == installed_bundle.get_bundle_id(): + self.uninstall(installed_bundle, force=True) + + install_dir = env.get_user_activities_path() + if isinstance(bundle, JournalEntryBundle): + install_path = bundle.install(uid) + elif isinstance(bundle, ContentBundle): + install_path = bundle.install() + else: + install_path = bundle.install(install_dir) + + # TODO treat ContentBundle in special way + # needs rethinking while fixing ContentBundle support + if isinstance(bundle, ContentBundle) or \ + isinstance(bundle, JournalEntryBundle): + pass + elif not self.add_bundle(install_path): + raise RegistrationException + + def uninstall(self, bundle, force=False, delete_profile=False): + # TODO treat ContentBundle in special way + # needs rethinking while fixing ContentBundle support + if isinstance(bundle, ContentBundle) or \ + isinstance(bundle, JournalEntryBundle): + if bundle.is_installed(): + bundle.uninstall() + else: + logging.warning('Not uninstalling, bundle is not installed') + return + + act = self.get_bundle(bundle.get_bundle_id()) + if not force and \ + act.get_activity_version() != bundle.get_activity_version(): + logging.warning('Not uninstalling, different bundle present') + return + + if not act.is_user_activity(): + logging.debug('Do not uninstall system activity') + return + + install_path = act.get_path() + + bundle.uninstall(install_path, force, delete_profile) + + if not self.remove_bundle(install_path): + raise RegistrationException + + def upgrade(self, bundle): + act = self.get_bundle(bundle.get_bundle_id()) + if act is None: + logging.warning('Activity not installed') + elif act.get_activity_version() == bundle.get_activity_version(): + logging.debug('No upgrade needed, same version already installed.') + return + elif act.is_user_activity(): + try: + self.uninstall(bundle, force=True) + except Exception: + logging.exception('Uninstall failed, still trying to install' + ' newer bundle:') + else: + logging.warning('Unable to uninstall system activity, ' + 'installing upgraded version in user activities') + + self.install(bundle) + + +def get_registry(): + global _instance + if not _instance: + _instance = BundleRegistry() + return _instance diff --git a/src/jarabe/model/filetransfer.py b/src/jarabe/model/filetransfer.py new file mode 100644 index 0000000..710c3a4 --- /dev/null +++ b/src/jarabe/model/filetransfer.py @@ -0,0 +1,368 @@ +# Copyright (C) 2008 Tomeu Vizoso +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import socket + +import gobject +import gio +import dbus +from telepathy.interfaces import CONNECTION_INTERFACE_REQUESTS, CHANNEL +from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT, \ + SOCKET_ADDRESS_TYPE_UNIX, \ + SOCKET_ACCESS_CONTROL_LOCALHOST +from telepathy.client import Connection, Channel + +from sugar.presence import presenceservice +from sugar import dispatch + +from jarabe.util.telepathy import connection_watcher +from jarabe.model import neighborhood + + +FT_STATE_NONE = 0 +FT_STATE_PENDING = 1 +FT_STATE_ACCEPTED = 2 +FT_STATE_OPEN = 3 +FT_STATE_COMPLETED = 4 +FT_STATE_CANCELLED = 5 + +FT_REASON_NONE = 0 +FT_REASON_REQUESTED = 1 +FT_REASON_LOCAL_STOPPED = 2 +FT_REASON_REMOTE_STOPPED = 3 +FT_REASON_LOCAL_ERROR = 4 +FT_REASON_LOCAL_ERROR = 5 +FT_REASON_REMOTE_ERROR = 6 + +# FIXME: use constants from tp-python once the spec is undrafted +CHANNEL_TYPE_FILE_TRANSFER = \ + 'org.freedesktop.Telepathy.Channel.Type.FileTransfer' + +new_file_transfer = dispatch.Signal() + + +# TODO Move to use splice_async() in Sugar 0.88 +class StreamSplicer(gobject.GObject): + _CHUNK_SIZE = 10240 # 10K + __gsignals__ = { + 'finished': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + + def __init__(self, input_stream, output_stream): + gobject.GObject.__init__(self) + + self._input_stream = input_stream + self._output_stream = output_stream + self._pending_buffers = [] + + def start(self): + self._input_stream.read_async(self._CHUNK_SIZE, self.__read_async_cb, + gobject.PRIORITY_LOW) + + def __read_async_cb(self, input_stream, result): + data = input_stream.read_finish(result) + + if not data: + logging.debug('closing input stream') + self._input_stream.close() + else: + self._pending_buffers.append(data) + self._input_stream.read_async(self._CHUNK_SIZE, + self.__read_async_cb, + gobject.PRIORITY_LOW) + self._write_next_buffer() + + def __write_async_cb(self, output_stream, result, user_data): + count_ = output_stream.write_finish(result) + + if not self._pending_buffers and \ + not self._output_stream.has_pending() and \ + not self._input_stream.has_pending(): + logging.debug('closing output stream') + output_stream.close() + self.emit('finished') + else: + self._write_next_buffer() + + def _write_next_buffer(self): + if self._pending_buffers and not self._output_stream.has_pending(): + data = self._pending_buffers.pop(0) + # TODO: we pass the buffer as user_data because of + # http://bugzilla.gnome.org/show_bug.cgi?id=564102 + self._output_stream.write_async(data, self.__write_async_cb, + gobject.PRIORITY_LOW, + user_data=data) + + +class BaseFileTransfer(gobject.GObject): + + def __init__(self, connection): + gobject.GObject.__init__(self) + self._connection = connection + self._state = FT_STATE_NONE + self._transferred_bytes = 0 + + self.channel = None + self.buddy = None + self.title = None + self.file_size = None + self.description = None + self.mime_type = None + self.initial_offset = 0 + self.reason_last_change = FT_REASON_NONE + + def set_channel(self, channel): + self.channel = channel + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'FileTransferStateChanged', self.__state_changed_cb) + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'TransferredBytesChanged', self.__transferred_bytes_changed_cb) + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'InitialOffsetDefined', self.__initial_offset_defined_cb) + + channel_properties = self.channel[dbus.PROPERTIES_IFACE] + + props = channel_properties.GetAll(CHANNEL_TYPE_FILE_TRANSFER) + self._state = props['State'] + self.title = props['Filename'] + self.file_size = props['Size'] + self.description = props['Description'] + self.mime_type = props['ContentType'] + + handle = channel_properties.Get(CHANNEL, 'TargetHandle') + self.buddy = neighborhood.get_model().get_buddy_by_handle(handle) + + def __transferred_bytes_changed_cb(self, transferred_bytes): + logging.debug('__transferred_bytes_changed_cb %r', transferred_bytes) + self.props.transferred_bytes = transferred_bytes + + def _set_transferred_bytes(self, transferred_bytes): + self._transferred_bytes = transferred_bytes + + def _get_transferred_bytes(self): + return self._transferred_bytes + + transferred_bytes = gobject.property(type=int, default=0, + getter=_get_transferred_bytes, setter=_set_transferred_bytes) + + def __initial_offset_defined_cb(self, offset): + logging.debug('__initial_offset_defined_cb %r', offset) + self.initial_offset = offset + + def __state_changed_cb(self, state, reason): + logging.debug('__state_changed_cb %r %r', state, reason) + self.reason_last_change = reason + self.props.state = state + + def _set_state(self, state): + self._state = state + + def _get_state(self): + return self._state + + state = gobject.property(type=int, getter=_get_state, setter=_set_state) + + def cancel(self): + self.channel[CHANNEL].Close() + + +class IncomingFileTransfer(BaseFileTransfer): + def __init__(self, connection, object_path, props): + BaseFileTransfer.__init__(self, connection) + + channel = Channel(connection.service_name, object_path) + self.set_channel(channel) + + self.connect('notify::state', self.__notify_state_cb) + + self.destination_path = None + self._socket_address = None + self._socket = None + self._splicer = None + + def accept(self, destination_path): + if os.path.exists(destination_path): + raise ValueError('Destination path already exists: %r' % \ + destination_path) + + self.destination_path = destination_path + + channel_ft = self.channel[CHANNEL_TYPE_FILE_TRANSFER] + self._socket_address = channel_ft.AcceptFile(SOCKET_ADDRESS_TYPE_UNIX, + SOCKET_ACCESS_CONTROL_LOCALHOST, '', 0, byte_arrays=True) + + def __notify_state_cb(self, file_transfer, pspec): + logging.debug('__notify_state_cb %r', self.props.state) + if self.props.state == FT_STATE_OPEN: + # Need to hold a reference to the socket so that python doesn't + # close the fd when it goes out of scope + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(self._socket_address) + input_stream = gio.unix.InputStream(self._socket.fileno(), True) + + destination_file = gio.File(self.destination_path) + if self.initial_offset == 0: + output_stream = destination_file.create() + else: + output_stream = destination_file.append_to() + + # TODO: Use splice_async when it gets implemented + self._splicer = StreamSplicer(input_stream, output_stream) + self._splicer.start() + + +class OutgoingFileTransfer(BaseFileTransfer): + def __init__(self, buddy, file_name, title, description, mime_type): + + presence_service = presenceservice.get_instance() + name, path = presence_service.get_preferred_connection() + connection = Connection(name, path, + ready_handler=self.__connection_ready_cb) + + BaseFileTransfer.__init__(self, connection) + self.connect('notify::state', self.__notify_state_cb) + + self._file_name = file_name + self._socket_address = None + self._socket = None + self._splicer = None + self._output_stream = None + + self.buddy = buddy + self.title = title + self.file_size = os.stat(file_name).st_size + self.description = description + self.mime_type = mime_type + + def __connection_ready_cb(self, connection): + requests = connection[CONNECTION_INTERFACE_REQUESTS] + object_path, properties_ = requests.CreateChannel({ + CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, + CHANNEL + '.TargetHandle': self.buddy.handle, + CHANNEL_TYPE_FILE_TRANSFER + '.ContentType': self.mime_type, + CHANNEL_TYPE_FILE_TRANSFER + '.Filename': self.title, + CHANNEL_TYPE_FILE_TRANSFER + '.Size': self.file_size, + CHANNEL_TYPE_FILE_TRANSFER + '.Description': self.description, + CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset': 0}) + + self.set_channel(Channel(connection.service_name, object_path)) + + channel_file_transfer = self.channel[CHANNEL_TYPE_FILE_TRANSFER] + self._socket_address = channel_file_transfer.ProvideFile( + SOCKET_ADDRESS_TYPE_UNIX, SOCKET_ACCESS_CONTROL_LOCALHOST, '', + byte_arrays=True) + + def __notify_state_cb(self, file_transfer, pspec): + logging.debug('__notify_state_cb %r', self.props.state) + if self.props.state == FT_STATE_OPEN: + # Need to hold a reference to the socket so that python doesn't + # closes the fd when it goes out of scope + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(self._socket_address) + output_stream = gio.unix.OutputStream(self._socket.fileno(), True) + + logging.debug('opening %s for reading', self._file_name) + input_stream = gio.File(self._file_name).read() + if self.initial_offset > 0: + input_stream.skip(self.initial_offset) + + # TODO: Use splice_async when it gets implemented + self._splicer = StreamSplicer(input_stream, output_stream) + self._splicer.start() + + def cancel(self): + self.channel[CHANNEL].Close() + + +def _new_channels_cb(connection, channels): + for object_path, props in channels: + if props[CHANNEL + '.ChannelType'] == CHANNEL_TYPE_FILE_TRANSFER and \ + not props[CHANNEL + '.Requested']: + + logging.debug('__new_channels_cb %r', object_path) + + incoming_file_transfer = IncomingFileTransfer(connection, + object_path, props) + new_file_transfer.send(None, file_transfer=incoming_file_transfer) + + +def _monitor_connection(connection): + logging.debug('connection added %r', connection) + connection[CONNECTION_INTERFACE_REQUESTS].connect_to_signal('NewChannels', + lambda channels: _new_channels_cb(connection, channels)) + + +def _connection_added_cb(conn_watcher, connection): + _monitor_connection(connection) + + +def _connection_removed_cb(conn_watcher, connection): + logging.debug('connection removed %r', connection) + + +def init(): + conn_watcher = connection_watcher.get_instance() + conn_watcher.connect('connection-added', _connection_added_cb) + conn_watcher.connect('connection-removed', _connection_removed_cb) + + for connection in conn_watcher.get_connections(): + _monitor_connection(connection) + + +def start_transfer(buddy, file_name, title, description, mime_type): + outgoing_file_transfer = OutgoingFileTransfer(buddy, file_name, title, + description, mime_type) + new_file_transfer.send(None, file_transfer=outgoing_file_transfer) + + +def file_transfer_available(): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): + + properties_iface = connection[dbus.PROPERTIES_IFACE] + properties = properties_iface.GetAll(CONNECTION_INTERFACE_REQUESTS) + classes = properties['RequestableChannelClasses'] + for prop, allowed_prop in classes: + + channel_type = prop.get(CHANNEL + '.ChannelType', '') + target_handle_type = prop.get(CHANNEL + '.TargetHandleType', '') + + if len(prop) == 2 and \ + channel_type == CHANNEL_TYPE_FILE_TRANSFER and \ + target_handle_type == CONNECTION_HANDLE_TYPE_CONTACT: + return True + + return False + + +if __name__ == '__main__': + import tempfile + + test_file_name = '/home/tomeu/isos/Soas2-200904031934.iso' + test_input_stream = gio.File(test_file_name).read() + test_output_stream = gio.File(tempfile.mkstemp()[1]).append_to() + + # TODO: Use splice_async when it gets implemented + splicer = StreamSplicer(test_input_stream, test_output_stream) + splicer.start() + + loop = gobject.MainLoop() + loop.run() diff --git a/src/jarabe/model/friends.py b/src/jarabe/model/friends.py new file mode 100644 index 0000000..7605af1 --- /dev/null +++ b/src/jarabe/model/friends.py @@ -0,0 +1,174 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +from ConfigParser import ConfigParser + +import gobject +import dbus + +from sugar import env +from sugar.graphics.xocolor import XoColor + +from jarabe.model.buddy import BuddyModel +from jarabe.model import neighborhood + + +_model = None + + +class FriendBuddyModel(BuddyModel): + __gtype_name__ = 'SugarFriendBuddyModel' + + _NOT_PRESENT_COLOR = '#D5D5D5,#FFFFFF' + + def __init__(self, nick, key, account=None, contact_id=None): + self._online_buddy = None + + BuddyModel.__init__(self, nick=nick, key=key, account=account, + contact_id=contact_id) + + neighborhood_model = neighborhood.get_model() + neighborhood_model.connect('buddy-added', self.__buddy_added_cb) + neighborhood_model.connect('buddy-removed', self.__buddy_removed_cb) + + buddy = neighborhood_model.get_buddy_by_key(key) + if buddy is not None: + self._set_online_buddy(buddy) + + def __buddy_added_cb(self, model_, buddy): + if buddy.key != self.key: + return + self._set_online_buddy(buddy) + + def _set_online_buddy(self, buddy): + self._online_buddy = buddy + self._online_buddy.connect('notify::color', self.__notify_color_cb) + self.notify('color') + self.notify('present') + + if buddy.nick != self.nick: + self.nick = buddy.nick + if buddy.contact_id != self.contact_id: + self.contact_id = buddy.contact_id + if buddy.account != self.account: + self.account = buddy.account + + def __buddy_removed_cb(self, model_, buddy): + if buddy.key != self.key: + return + self._online_buddy = None + self.notify('color') + self.notify('present') + + def __notify_color_cb(self, buddy, pspec): + self.notify('color') + + def is_present(self): + return self._online_buddy is not None + + present = gobject.property(type=bool, default=False, getter=is_present) + + def get_color(self): + if self._online_buddy is not None: + return self._online_buddy.color + else: + return XoColor(FriendBuddyModel._NOT_PRESENT_COLOR) + + color = gobject.property(type=object, getter=get_color) + + def get_handle(self): + if self._online_buddy is not None: + return self._online_buddy.handle + else: + return None + + handle = gobject.property(type=object, getter=get_handle) + + +class Friends(gobject.GObject): + __gsignals__ = { + 'friend-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'friend-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._friends = {} + self._path = os.path.join(env.get_profile_path(), 'friends') + + self.load() + + def has_buddy(self, buddy): + return buddy.get_key() in self._friends + + def add_friend(self, buddy_info): + self._friends[buddy_info.get_key()] = buddy_info + self.emit('friend-added', buddy_info) + + def make_friend(self, buddy): + if not self.has_buddy(buddy): + buddy = FriendBuddyModel(key=buddy.key, nick=buddy.nick, + account=buddy.account, + contact_id=buddy.contact_id) + self.add_friend(buddy) + self.save() + + def remove(self, buddy_info): + del self._friends[buddy_info.get_key()] + self.save() + self.emit('friend-removed', buddy_info.get_key()) + + def __iter__(self): + return self._friends.values().__iter__() + + def load(self): + cp = ConfigParser() + + try: + success = cp.read([self._path]) + if success: + for key in cp.sections(): + # HACK: don't screw up on old friends files + if len(key) < 20: + continue + buddy = FriendBuddyModel(key=key, nick=cp.get(key, 'nick')) + self.add_friend(buddy) + except Exception: + logging.exception('Error parsing friends file') + + def save(self): + cp = ConfigParser() + + for friend in self: + section = friend.get_key() + cp.add_section(section) + cp.set(section, 'nick', friend.get_nick()) + + fileobject = open(self._path, 'w') + cp.write(fileobject) + fileobject.close() + + +def get_model(): + global _model + if _model is None: + _model = Friends() + return _model diff --git a/src/jarabe/model/invites.py b/src/jarabe/model/invites.py new file mode 100644 index 0000000..631e49f --- /dev/null +++ b/src/jarabe/model/invites.py @@ -0,0 +1,289 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from functools import partial +import simplejson + +import gobject +import dbus +import gconf +from telepathy.interfaces import CHANNEL, \ + CHANNEL_DISPATCHER, \ + CHANNEL_DISPATCH_OPERATION, \ + CHANNEL_TYPE_CONTACT_LIST, \ + CHANNEL_TYPE_TEXT, \ + CLIENT +from telepathy.constants import HANDLE_TYPE_ROOM + +from sugar.graphics.xocolor import XoColor + +from jarabe.model import telepathyclient +from jarabe.model import bundleregistry +from jarabe.model import neighborhood +from jarabe.journal import misc + + +CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \ + 'org.laptop.Telepathy.ActivityProperties' + +_instance = None + + +class BaseInvite(object): + """Invitation to shared activity or private 1-1 Telepathy channel""" + def __init__(self, dispatch_operation_path, handle, handler): + self.dispatch_operation_path = dispatch_operation_path + self._handle = handle + self._handler = handler + + def get_bundle_id(self): + if CLIENT in self._handler: + return self._handler[len(CLIENT + '.'):] + else: + return None + + def _call_handle_with(self): + bus = dbus.Bus() + obj = bus.get_object(CHANNEL_DISPATCHER, self.dispatch_operation_path) + dispatch_operation = dbus.Interface(obj, CHANNEL_DISPATCH_OPERATION) + dispatch_operation.HandleWith(self._handler, + reply_handler=self._handle_with_reply_cb, + error_handler=self._handle_with_reply_cb) + + def _handle_with_reply_cb(self, error=None): + if error is not None: + raise error + else: + logging.debug('_handle_with_reply_cb') + + def _name_owner_changed_cb(self, name, old_owner, new_owner): + logging.debug('BaseInvite._name_owner_changed_cb %r %r %r', name, + new_owner, old_owner) + if name == self._handler and new_owner and not old_owner: + self._call_handle_with() + + +class ActivityInvite(BaseInvite): + """Invitation to a shared activity.""" + def __init__(self, dispatch_operation_path, handle, handler, + activity_properties): + BaseInvite.__init__(self, dispatch_operation_path, handle, handler) + + if activity_properties is not None: + self._activity_properties = activity_properties + else: + self._activity_properties = {} + + def get_color(self): + color = self._activity_properties.get('color', None) + return XoColor(color) + + def join(self): + logging.error('ActivityInvite.join handler %r', self._handler) + + registry = bundleregistry.get_registry() + bundle_id = self.get_bundle_id() + bundle = registry.get_bundle(bundle_id) + if bundle is None: + self._call_handle_with() + return + + bus = dbus.SessionBus() + bus.add_signal_receiver(self._name_owner_changed_cb, + 'NameOwnerChanged', + 'org.freedesktop.DBus', + arg0=self._handler) + + model = neighborhood.get_model() + activity_id = model.get_activity_by_room(self._handle).activity_id + misc.launch(bundle, color=self.get_color(), invited=True, + activity_id=activity_id) + + +class PrivateInvite(BaseInvite): + def __init__(self, dispatch_operation_path, handle, handler, + private_channel): + BaseInvite.__init__(self, dispatch_operation_path, handle, handler) + + self._private_channel = private_channel + + def get_color(self): + client = gconf.client_get_default() + return XoColor(client.get_string('/desktop/sugar/user/color')) + + def join(self): + logging.error('PrivateInvite.join handler %r', self._handler) + registry = bundleregistry.get_registry() + bundle_id = self.get_bundle_id() + bundle = registry.get_bundle(bundle_id) + + bus = dbus.SessionBus() + bus.add_signal_receiver(self._name_owner_changed_cb, + 'NameOwnerChanged', + 'org.freedesktop.DBus', + arg0=self._handler) + misc.launch(bundle, color=self.get_color(), invited=True, + uri=self._private_channel) + + +class Invites(gobject.GObject): + __gsignals__ = { + 'invite-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'invite-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._dispatch_operations = {} + + client_handler = telepathyclient.get_instance() + client_handler.got_dispatch_operation.connect( + self.__got_dispatch_operation_cb) + + def __got_dispatch_operation_cb(self, **kwargs): + logging.debug('__got_dispatch_operation_cb') + dispatch_operation_path = kwargs['dispatch_operation_path'] + channel_path, channel_properties = kwargs['channels'][0] + properties = kwargs['properties'] + channel_type = channel_properties[CHANNEL + '.ChannelType'] + handle_type = channel_properties[CHANNEL + '.TargetHandleType'] + handle = channel_properties[CHANNEL + '.TargetHandle'] + + if handle_type == HANDLE_TYPE_ROOM and \ + channel_type == CHANNEL_TYPE_TEXT: + logging.debug('May be an activity, checking its properties') + connection_path = properties[CHANNEL_DISPATCH_OPERATION + + '.Connection'] + connection_name = connection_path.replace('/', '.')[1:] + + bus = dbus.Bus() + connection = bus.get_object(connection_name, connection_path) + connection.GetProperties( + channel_properties[CHANNEL + '.TargetHandle'], + dbus_interface=CONNECTION_INTERFACE_ACTIVITY_PROPERTIES, + reply_handler=partial(self.__get_properties_cb, + handle, + dispatch_operation_path), + error_handler=partial(self.__error_handler_cb, + handle, + channel_properties, + dispatch_operation_path, + channel_path, + properties)) + else: + self._dispatch_non_sugar_invitation(handle, + channel_properties, + dispatch_operation_path, + channel_path, + properties) + + def __get_properties_cb(self, handle, dispatch_operation_path, properties): + logging.debug('__get_properties_cb %r', properties) + handler = '%s.%s' % (CLIENT, properties['type']) + self._add_invite(dispatch_operation_path, handle, handler, properties) + + def __error_handler_cb(self, handle, channel_properties, + dispatch_operation_path, channel_path, + properties, error): + logging.debug('__error_handler_cb %r', error) + exception_name = 'org.freedesktop.Telepathy.Error.NotAvailable' + if error.get_dbus_name() == exception_name: + self._dispatch_non_sugar_invitation(handle, + channel_properties, + dispatch_operation_path, + channel_path, + properties) + else: + raise error + + def _dispatch_non_sugar_invitation(self, handle, channel_properties, + dispatch_operation_path, channel_path, + properties): + handler = None + channel_type = channel_properties[CHANNEL + '.ChannelType'] + if channel_type == CHANNEL_TYPE_CONTACT_LIST: + self._handle_with(dispatch_operation_path, CLIENT + '.Sugar') + elif channel_type == CHANNEL_TYPE_TEXT: + handler = CLIENT + '.org.laptop.Chat' + self._add_private_invite(dispatch_operation_path, handle, handler, + channel_path, properties) + return + else: + self._call_handle_with(dispatch_operation_path, '') + + if handler is not None: + logging.debug('Adding an invite from a non-Sugar client') + self._add_invite(dispatch_operation_path, handle, handler) + + def _call_handle_with(self, dispatch_operation_path, handler): + logging.debug('_handle_with %r %r', dispatch_operation_path, handler) + bus = dbus.Bus() + obj = bus.get_object(CHANNEL_DISPATCHER, dispatch_operation_path) + dispatch_operation = dbus.Interface(obj, CHANNEL_DISPATCH_OPERATION) + dispatch_operation.HandleWith(handler, + reply_handler=self.__handle_with_reply_cb, + error_handler=self.__handle_with_reply_cb) + + def __handle_with_reply_cb(self, error=None): + if error is not None: + logging.error('__handle_with_reply_cb %r', error) + else: + logging.debug('__handle_with_reply_cb') + + def _add_invite(self, dispatch_operation_path, handle, handler, + activity_properties=None): + logging.debug('_add_invite %r %r %r', dispatch_operation_path, handle, + handler) + if dispatch_operation_path in self._dispatch_operations: + # there is no point to have more than one invite for the same + # dispatch operation + return + + invite = ActivityInvite(dispatch_operation_path, handle, handler, + activity_properties) + self._dispatch_operations[dispatch_operation_path] = invite + self.emit('invite-added', invite) + + def _add_private_invite(self, dispatch_operation_path, handle, handler, + channel_path, properties): + connection_path = properties[CHANNEL_DISPATCH_OPERATION + + '.Connection'] + connection_name = connection_path.replace('/', '.')[1:] + private_channel = simplejson.dumps([connection_name, + connection_path, channel_path]) + invite = PrivateInvite(dispatch_operation_path, handle, handler, + private_channel) + self._dispatch_operations[dispatch_operation_path] = invite + self.emit('invite-added', invite) + + def remove_invite(self, invite): + del self._dispatch_operations[invite.dispatch_operation_path] + self.emit('invite-removed', invite) + + def __iter__(self): + return self._dispatch_operations.values().__iter__() + + +def get_instance(): + global _instance + if not _instance: + _instance = Invites() + return _instance diff --git a/src/jarabe/model/mimeregistry.py b/src/jarabe/model/mimeregistry.py new file mode 100644 index 0000000..7fb5bcf --- /dev/null +++ b/src/jarabe/model/mimeregistry.py @@ -0,0 +1,50 @@ +# Copyright (C) 2009 Aleksey Lim +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import re + +import gconf + + +_DEFAULTS_KEY = '/desktop/sugar/journal/defaults' +_GCONF_INVALID_CHARS = re.compile('[^a-zA-Z0-9-_/.]') + +_instance = None + + +class MimeRegistry(object): + + def __init__(self): + # TODO move here all mime_type related code from jarabe modules + self._gconf = gconf.client_get_default() + + def get_default_activity(self, mime_type): + return self._gconf.get_string(_key_name(mime_type)) + + def set_default_activity(self, mime_type, bundle_id): + self._gconf.set_string(_key_name(mime_type), bundle_id) + + +def get_registry(): + global _instance + if _instance is None: + _instance = MimeRegistry() + return _instance + + +def _key_name(mime_type): + mime_type = _GCONF_INVALID_CHARS.sub('_', mime_type) + return '%s/%s' % (_DEFAULTS_KEY, mime_type) diff --git a/src/jarabe/model/neighborhood.py b/src/jarabe/model/neighborhood.py new file mode 100644 index 0000000..828cb14 --- /dev/null +++ b/src/jarabe/model/neighborhood.py @@ -0,0 +1,1084 @@ +# Copyright (C) 2010 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from functools import partial +from hashlib import sha1 + +import gobject +import gconf +import dbus +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import ACCOUNT, \ + ACCOUNT_MANAGER, \ + CHANNEL, \ + CHANNEL_INTERFACE_GROUP, \ + CHANNEL_TYPE_CONTACT_LIST, \ + CHANNEL_TYPE_FILE_TRANSFER, \ + CLIENT, \ + CONNECTION, \ + CONNECTION_INTERFACE_ALIASING, \ + CONNECTION_INTERFACE_CONTACTS, \ + CONNECTION_INTERFACE_CONTACT_CAPABILITIES, \ + CONNECTION_INTERFACE_REQUESTS, \ + CONNECTION_INTERFACE_SIMPLE_PRESENCE +from telepathy.constants import HANDLE_TYPE_CONTACT, \ + HANDLE_TYPE_LIST, \ + CONNECTION_PRESENCE_TYPE_OFFLINE, \ + CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED +from telepathy.client import Connection, Channel + +from sugar.graphics.xocolor import XoColor +from sugar.profile import get_profile + +from jarabe.model.buddy import BuddyModel, get_owner_instance +from jarabe.model import bundleregistry +from jarabe.model import shell + + +ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager' +ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager' +CHANNEL_DISPATCHER_SERVICE = 'org.freedesktop.Telepathy.ChannelDispatcher' +CHANNEL_DISPATCHER_PATH = '/org/freedesktop/Telepathy/ChannelDispatcher' +SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \ + 'org.laptop.Telepathy.ActivityProperties' + +_QUERY_DBUS_TIMEOUT = 200 +""" +Time in seconds to wait when querying contact properties. Some jabber servers +will be very slow in returning these queries, so just be patient. +""" + +_model = None + + +class ActivityModel(gobject.GObject): + __gsignals__ = { + 'current-buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'current-buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self, activity_id, room_handle): + gobject.GObject.__init__(self) + + self.activity_id = activity_id + self.room_handle = room_handle + self._bundle = None + self._color = None + self._private = True + self._name = None + self._current_buddies = [] + self._buddies = [] + + def get_color(self): + return self._color + + def set_color(self, color): + self._color = color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def get_bundle(self): + return self._bundle + + def set_bundle(self, bundle): + self._bundle = bundle + + bundle = gobject.property(type=object, getter=get_bundle, + setter=set_bundle) + + def get_name(self): + return self._name + + def set_name(self, name): + self._name = name + + name = gobject.property(type=object, getter=get_name, setter=set_name) + + def is_private(self): + return self._private + + def set_private(self, private): + self._private = private + + private = gobject.property(type=object, getter=is_private, + setter=set_private) + + def get_buddies(self): + return self._buddies + + def add_buddy(self, buddy): + self._buddies.append(buddy) + self.notify('buddies') + self.emit('buddy-added', buddy) + + def remove_buddy(self, buddy): + self._buddies.remove(buddy) + self.notify('buddies') + self.emit('buddy-removed', buddy) + + buddies = gobject.property(type=object, getter=get_buddies) + + def get_current_buddies(self): + return self._current_buddies + + def add_current_buddy(self, buddy): + self._current_buddies.append(buddy) + self.notify('current-buddies') + self.emit('current-buddy-added', buddy) + + def remove_current_buddy(self, buddy): + self._current_buddies.remove(buddy) + self.notify('current-buddies') + self.emit('current-buddy-removed', buddy) + + current_buddies = gobject.property(type=object, getter=get_current_buddies) + + +class _Account(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'activity-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object, object])), + 'buddy-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'buddy-left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'current-activity-updated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object, object])), + 'connected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'disconnected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, account_path): + gobject.GObject.__init__(self) + + self.object_path = account_path + + self._connection = None + self._buddy_handles = {} + self._activity_handles = {} + self._self_handle = None + + self._buddies_per_activity = {} + self._activities_per_buddy = {} + + self._start_listening() + + def _start_listening(self): + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Get(ACCOUNT, 'Connection', + reply_handler=self.__got_connection_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.GetConnection')) + obj.connect_to_signal( + 'AccountPropertyChanged', self.__account_property_changed_cb) + + def __error_handler_cb(self, function_name, error): + raise RuntimeError('Error when calling %s: %s' % (function_name, + error)) + + def __got_connection_cb(self, connection_path): + logging.debug('_Account.__got_connection_cb %r', connection_path) + + if connection_path == '/': + self._check_registration_error() + return + + self._prepare_connection(connection_path) + + def _check_registration_error(self): + """ + See if a previous connection attempt failed and we need to unset + the register flag. + """ + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Get(ACCOUNT, 'ConnectionError', + reply_handler=self.__got_connection_error_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.GetConnectionError')) + + def __got_connection_error_cb(self, error): + logging.debug('_Account.__got_connection_error_cb %r', error) + if error == 'org.freedesktop.Telepathy.Error.RegistrationExists': + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.UpdateParameters({'register': False}, [], + dbus_interface=ACCOUNT) + + def __account_property_changed_cb(self, properties): + logging.debug('_Account.__account_property_changed_cb %r %r %r', + self.object_path, properties.get('Connection', None), + self._connection) + if 'Connection' not in properties: + return + if properties['Connection'] == '/': + self._check_registration_error() + self._connection = None + elif self._connection is None: + self._prepare_connection(properties['Connection']) + + def _prepare_connection(self, connection_path): + connection_name = connection_path.replace('/', '.')[1:] + + self._connection = Connection(connection_name, connection_path, + ready_handler=self.__connection_ready_cb) + + def __connection_ready_cb(self, connection): + logging.debug('_Account.__connection_ready_cb %r', + connection.object_path) + connection.connect_to_signal('StatusChanged', + self.__status_changed_cb) + + connection[PROPERTIES_IFACE].Get(CONNECTION, + 'Status', + reply_handler=self.__get_status_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetStatus')) + + def __get_status_cb(self, status): + logging.debug('_Account.__get_status_cb %r %r', + self._connection.object_path, status) + self._update_status(status) + + def __status_changed_cb(self, status, reason): + logging.debug('_Account.__status_changed_cb %r %r', status, reason) + self._update_status(status) + + def _update_status(self, status): + if status == CONNECTION_STATUS_CONNECTED: + self._connection[PROPERTIES_IFACE].Get(CONNECTION, + 'SelfHandle', + reply_handler=self.__get_self_handle_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetSelfHandle')) + self.emit('connected') + else: + for contact_handle, contact_id in self._buddy_handles.items(): + if contact_id is not None: + self.emit('buddy-removed', contact_id) + + for room_handle, activity_id in self._activity_handles.items(): + self.emit('activity-removed', activity_id) + + self._buddy_handles = {} + self._activity_handles = {} + self._buddies_per_activity = {} + self._activities_per_buddy = {} + + self.emit('disconnected') + + if status == CONNECTION_STATUS_DISCONNECTED: + self._connection = None + + def __get_self_handle_cb(self, self_handle): + self._self_handle = self_handle + + if CONNECTION_INTERFACE_CONTACT_CAPABILITIES in self._connection: + interface = CONNECTION_INTERFACE_CONTACT_CAPABILITIES + connection = self._connection[interface] + client_name = CLIENT + '.Sugar.FileTransfer' + file_transfer_channel_class = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, + CHANNEL + '.TargetHandleType': HANDLE_TYPE_CONTACT} + capabilities = [] + connection.UpdateCapabilities( + [(client_name, [file_transfer_channel_class], capabilities)], + reply_handler=self.__update_capabilities_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.UpdateCapabilities')) + + connection = self._connection[CONNECTION_INTERFACE_ALIASING] + connection.connect_to_signal('AliasesChanged', + self.__aliases_changed_cb) + + connection = self._connection[CONNECTION_INTERFACE_SIMPLE_PRESENCE] + connection.connect_to_signal('PresencesChanged', + self.__presences_changed_cb) + + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.connect_to_signal('PropertiesChanged', + self.__buddy_info_updated_cb, + byte_arrays=True) + + connection.connect_to_signal('ActivitiesChanged', + self.__buddy_activities_changed_cb) + + connection.connect_to_signal('CurrentActivityChanged', + self.__current_activity_changed_cb) + home_model = shell.get_model() + home_model.connect('active-activity-changed', + self.__active_activity_changed_cb) + else: + logging.warning('Connection %s does not support OLPC buddy ' + 'properties', self._connection.object_path) + + if CONNECTION_INTERFACE_ACTIVITY_PROPERTIES in self._connection: + connection = self._connection[ + CONNECTION_INTERFACE_ACTIVITY_PROPERTIES] + connection.connect_to_signal( + 'ActivityPropertiesChanged', + self.__activity_properties_changed_cb) + else: + logging.warning('Connection %s does not support OLPC activity ' + 'properties', self._connection.object_path) + + properties = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_CONTACT_LIST, + CHANNEL + '.TargetHandleType': HANDLE_TYPE_LIST, + CHANNEL + '.TargetID': 'subscribe', + } + properties = dbus.Dictionary(properties, signature='sv') + connection = self._connection[CONNECTION_INTERFACE_REQUESTS] + is_ours, channel_path, properties = \ + connection.EnsureChannel(properties) + + channel = Channel(self._connection.service_name, channel_path) + channel[CHANNEL_INTERFACE_GROUP].connect_to_signal( + 'MembersChanged', self.__members_changed_cb) + + channel[PROPERTIES_IFACE].Get(CHANNEL_INTERFACE_GROUP, + 'Members', + reply_handler=self.__get_members_ready_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetMembers')) + + def __active_activity_changed_cb(self, model, home_activity): + room_handle = 0 + home_activity_id = home_activity.get_activity_id() + for handle, activity_id in self._activity_handles.items(): + if home_activity_id == activity_id: + room_handle = handle + break + if room_handle == 0: + home_activity_id = '' + + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.SetCurrentActivity( + home_activity_id, + room_handle, + reply_handler=self.__set_current_activity_cb, + error_handler=self.__set_current_activity_error_cb) + + def __set_current_activity_cb(self): + logging.warning('_Account.__set_current_activity_cb') + + def __set_current_activity_error_cb(self, error): + logging.debug('_Account.__set_current_activity__error_cb %r', error) + + def __update_capabilities_cb(self): + pass + + def __aliases_changed_cb(self, aliases): + logging.debug('_Account.__aliases_changed_cb') + for handle, alias in aliases: + if handle in self._buddy_handles: + logging.debug('Got handle %r with nick %r, going to update', + handle, alias) + properties = {CONNECTION_INTERFACE_ALIASING + '/alias': alias} + self.emit('buddy-updated', self._buddy_handles[handle], + properties) + + def __presences_changed_cb(self, presences): + logging.debug('_Account.__presences_changed_cb %r', presences) + for handle, presence in presences.iteritems(): + if handle in self._buddy_handles: + presence_type, status_, message_ = presence + if presence_type == CONNECTION_PRESENCE_TYPE_OFFLINE: + contact_id = self._buddy_handles[handle] + del self._buddy_handles[handle] + self.emit('buddy-removed', contact_id) + + def __buddy_info_updated_cb(self, handle, properties): + logging.debug('_Account.__buddy_info_updated_cb %r', handle) + self.emit('buddy-updated', self._buddy_handles[handle], properties) + + def __current_activity_changed_cb(self, contact_handle, activity_id, + room_handle): + logging.debug('_Account.__current_activity_changed_cb %r %r %r', + contact_handle, activity_id, room_handle) + if contact_handle in self._buddy_handles: + contact_id = self._buddy_handles[contact_handle] + if not activity_id and room_handle: + activity_id = self._activity_handles.get(room_handle, '') + self.emit('current-activity-updated', contact_id, activity_id) + + def __get_current_activity_cb(self, contact_handle, activity_id, + room_handle): + logging.debug('_Account.__get_current_activity_cb %r %r %r', + contact_handle, activity_id, room_handle) + + if contact_handle in self._buddy_handles: + contact_id = self._buddy_handles[contact_handle] + if not activity_id and room_handle: + activity_id = self._activity_handles.get(room_handle, '') + self.emit('current-activity-updated', contact_id, activity_id) + + def __buddy_activities_changed_cb(self, buddy_handle, activities): + self._update_buddy_activities(buddy_handle, activities) + + def _update_buddy_activities(self, buddy_handle, activities): + logging.debug('_Account._update_buddy_activities') + + if not buddy_handle in self._activities_per_buddy: + self._activities_per_buddy[buddy_handle] = set() + + for activity_id, room_handle in activities: + if room_handle not in self._activity_handles: + self._activity_handles[room_handle] = activity_id + + if buddy_handle == self._self_handle: + home_model = shell.get_model() + activity = home_model.get_active_activity() + if activity.get_activity_id() == activity_id: + connection = self._connection[ + CONNECTION_INTERFACE_BUDDY_INFO] + connection.SetCurrentActivity( + activity_id, + room_handle, + reply_handler=self.__set_current_activity_cb, + error_handler=self.__set_current_activity_error_cb) + + self.emit('activity-added', room_handle, activity_id) + + connection = self._connection[ + CONNECTION_INTERFACE_ACTIVITY_PROPERTIES] + connection.GetProperties(room_handle, + reply_handler=partial(self.__get_properties_cb, + room_handle), + error_handler=partial(self.__error_handler_cb, + 'ActivityProperties.GetProperties')) + + if buddy_handle != self._self_handle: + # Sometimes we'll get CurrentActivityChanged before we get + # to know about the activity so we miss the event. In that + # case, request again the current activity for this buddy. + connection = self._connection[ + CONNECTION_INTERFACE_BUDDY_INFO] + connection.GetCurrentActivity( + buddy_handle, + reply_handler=partial(self.__get_current_activity_cb, + buddy_handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetCurrentActivity')) + + if not activity_id in self._buddies_per_activity: + self._buddies_per_activity[activity_id] = set() + self._buddies_per_activity[activity_id].add(buddy_handle) + if activity_id not in self._activities_per_buddy[buddy_handle]: + self._activities_per_buddy[buddy_handle].add(activity_id) + if buddy_handle != self._self_handle: + self.emit('buddy-joined-activity', + self._buddy_handles[buddy_handle], + activity_id) + + current_activity_ids = \ + [activity_id for activity_id, room_handle in activities] + for activity_id in self._activities_per_buddy[buddy_handle].copy(): + if not activity_id in current_activity_ids: + self._remove_buddy_from_activity(buddy_handle, activity_id) + + def __get_properties_cb(self, room_handle, properties): + logging.debug('_Account.__get_properties_cb %r %r', room_handle, + properties) + if properties: + self._update_activity(room_handle, properties) + + def _remove_buddy_from_activity(self, buddy_handle, activity_id): + if buddy_handle in self._buddies_per_activity[activity_id]: + self._buddies_per_activity[activity_id].remove(buddy_handle) + + if activity_id in self._activities_per_buddy[buddy_handle]: + self._activities_per_buddy[buddy_handle].remove(activity_id) + + if buddy_handle != self._self_handle: + self.emit('buddy-left-activity', + self._buddy_handles[buddy_handle], + activity_id) + + if not self._buddies_per_activity[activity_id]: + del self._buddies_per_activity[activity_id] + + for room_handle in self._activity_handles.copy(): + if self._activity_handles[room_handle] == activity_id: + del self._activity_handles[room_handle] + break + + self.emit('activity-removed', activity_id) + + def __activity_properties_changed_cb(self, room_handle, properties): + logging.debug('_Account.__activity_properties_changed_cb %r %r', + room_handle, properties) + self._update_activity(room_handle, properties) + + def _update_activity(self, room_handle, properties): + if room_handle in self._activity_handles: + self.emit('activity-updated', self._activity_handles[room_handle], + properties) + else: + logging.debug('_Account.__activity_properties_changed_cb unknown ' + 'activity') + # We don't get ActivitiesChanged for the owner of the connection, + # so we query for its activities in order to find out. + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + handle = self._self_handle + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.GetActivities( + handle, + reply_handler=partial(self.__got_activities_cb, handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.Getactivities')) + + def __members_changed_cb(self, message, added, removed, local_pending, + remote_pending, actor, reason): + self._add_buddy_handles(added) + + def __get_members_ready_cb(self, handles): + logging.debug('_Account.__get_members_ready_cb %r', handles) + if not handles: + return + + self._add_buddy_handles(handles) + + def _add_buddy_handles(self, handles): + logging.debug('_Account._add_buddy_handles %r', handles) + interfaces = [CONNECTION, CONNECTION_INTERFACE_ALIASING] + self._connection[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes( + handles, interfaces, False, + reply_handler=self.__get_contact_attributes_cb, + error_handler=partial(self.__error_handler_cb, + 'Contacts.GetContactAttributes')) + + def __got_buddy_info_cb(self, handle, nick, properties): + logging.debug('_Account.__got_buddy_info_cb %r', handle) + self.emit('buddy-updated', self._buddy_handles[handle], properties) + + def __get_contact_attributes_cb(self, attributes): + logging.debug('_Account.__get_contact_attributes_cb %r', + attributes.keys()) + + for handle in attributes.keys(): + nick = attributes[handle][CONNECTION_INTERFACE_ALIASING + '/alias'] + + if handle == self._self_handle: + logging.debug('_Account.__get_contact_attributes_cb,' \ + ' do not add ourself %r', handle) + continue + + if handle in self._buddy_handles and \ + not self._buddy_handles[handle] is None: + logging.debug('Got handle %r with nick %r, going to update', + handle, nick) + self.emit('buddy-updated', self._buddy_handles[handle], + attributes[handle]) + else: + logging.debug('Got handle %r with nick %r, going to add', + handle, nick) + + contact_id = attributes[handle][CONNECTION + '/contact-id'] + self._buddy_handles[handle] = contact_id + + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + connection = \ + self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + + connection.GetProperties( + handle, + reply_handler=partial(self.__got_buddy_info_cb, handle, + nick), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetProperties'), + byte_arrays=True, + timeout=_QUERY_DBUS_TIMEOUT) + + connection.GetActivities( + handle, + reply_handler=partial(self.__got_activities_cb, + handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetActivities'), + timeout=_QUERY_DBUS_TIMEOUT) + + connection.GetCurrentActivity( + handle, + reply_handler=partial(self.__get_current_activity_cb, + handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetCurrentActivity'), + timeout=_QUERY_DBUS_TIMEOUT) + + self.emit('buddy-added', contact_id, nick, handle) + + def __got_activities_cb(self, buddy_handle, activities): + logging.debug('_Account.__got_activities_cb %r %r', buddy_handle, + activities) + self._update_buddy_activities(buddy_handle, activities) + + def enable(self): + logging.debug('_Account.enable %s', self.object_path) + self._set_enabled(True) + + def disable(self): + logging.debug('_Account.disable %s', self.object_path) + self._set_enabled(False) + self._connection = None + + def _set_enabled(self, value): + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Set(ACCOUNT, 'Enabled', value, + reply_handler=self.__set_enabled_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.SetEnabled'), + dbus_interface=dbus.PROPERTIES_IFACE) + + def __set_enabled_cb(self): + logging.debug('_Account.__set_enabled_cb success') + + +class Neighborhood(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._buddies = {None: get_owner_instance()} + self._activities = {} + self._link_local_account = None + self._server_account = None + self._shell_model = shell.get_model() + + client = gconf.client_get_default() + client.add_dir('/desktop/sugar/collaboration', + gconf.CLIENT_PRELOAD_NONE) + client.notify_add('/desktop/sugar/collaboration/jabber_server', + self.__jabber_server_changed_cb) + client.add_dir('/desktop/sugar/user/nick', gconf.CLIENT_PRELOAD_NONE) + client.notify_add('/desktop/sugar/user/nick', self.__nick_changed_cb) + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts', + dbus_interface=PROPERTIES_IFACE, + reply_handler=self.__got_accounts_cb, + error_handler=self.__error_handler_cb) + + def __got_accounts_cb(self, account_paths): + self._link_local_account = \ + self._ensure_link_local_account(account_paths) + self._connect_to_account(self._link_local_account) + + self._server_account = self._ensure_server_account(account_paths) + self._connect_to_account(self._server_account) + + def __error_handler_cb(self, error): + raise RuntimeError(error) + + def _connect_to_account(self, account): + account.connect('buddy-added', self.__buddy_added_cb) + account.connect('buddy-updated', self.__buddy_updated_cb) + account.connect('buddy-removed', self.__buddy_removed_cb) + account.connect('buddy-joined-activity', + self.__buddy_joined_activity_cb) + account.connect('buddy-left-activity', self.__buddy_left_activity_cb) + account.connect('activity-added', self.__activity_added_cb) + account.connect('activity-updated', self.__activity_updated_cb) + account.connect('activity-removed', self.__activity_removed_cb) + account.connect('current-activity-updated', + self.__current_activity_updated_cb) + account.connect('connected', self.__account_connected_cb) + account.connect('disconnected', self.__account_disconnected_cb) + + def __account_connected_cb(self, account): + logging.debug('__account_connected_cb %s', account.object_path) + if account == self._server_account: + self._link_local_account.disable() + + def __account_disconnected_cb(self, account): + logging.debug('__account_disconnected_cb %s', account.object_path) + if account == self._server_account: + self._link_local_account.enable() + + def _get_published_name(self): + """Construct the published name based on the public key + + Limit the name to be only 8 characters maximum. The avahi + service name has a 64 character limit. It consists of + the room name, the published name and the host name. + + """ + public_key_hash = sha1(get_profile().pubkey).hexdigest() + return public_key_hash[:8] + + def _ensure_link_local_account(self, account_paths): + for account_path in account_paths: + if 'salut' in account_path: + logging.debug('Already have a Salut account') + account = _Account(account_path) + account.enable() + return account + + logging.debug('Still dont have a Salut account, creating one') + + client = gconf.client_get_default() + nick = client.get_string('/desktop/sugar/user/nick') + + params = { + 'nickname': nick, + 'first-name': '', + 'last-name': '', + 'jid': self._get_jabber_account_id(), + 'published-name': self._get_published_name(), + } + + properties = { + ACCOUNT + '.Enabled': True, + ACCOUNT + '.Nickname': nick, + ACCOUNT + '.ConnectAutomatically': True, + } + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_path = account_manager.CreateAccount('salut', 'local-xmpp', + 'salut', params, + properties) + return _Account(account_path) + + def _ensure_server_account(self, account_paths): + for account_path in account_paths: + if 'gabble' in account_path: + logging.debug('Already have a Gabble account') + account = _Account(account_path) + account.enable() + return account + + logging.debug('Still dont have a Gabble account, creating one') + + client = gconf.client_get_default() + nick = client.get_string('/desktop/sugar/user/nick') + server = client.get_string('/desktop/sugar/collaboration' + '/jabber_server') + key_hash = get_profile().privkey_hash + + params = { + 'account': self._get_jabber_account_id(), + 'password': key_hash, + 'server': server, + 'resource': 'sugar', + 'require-encryption': True, + 'ignore-ssl-errors': True, + 'register': True, + 'old-ssl': True, + 'port': dbus.UInt32(5223), + } + + properties = { + ACCOUNT + '.Enabled': True, + ACCOUNT + '.Nickname': nick, + ACCOUNT + '.ConnectAutomatically': True, + } + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_path = account_manager.CreateAccount('gabble', 'jabber', + 'jabber', params, + properties) + return _Account(account_path) + + def _get_jabber_account_id(self): + public_key_hash = sha1(get_profile().pubkey).hexdigest() + client = gconf.client_get_default() + server = client.get_string('/desktop/sugar/collaboration' + '/jabber_server') + return '%s@%s' % (public_key_hash, server) + + def __jabber_server_changed_cb(self, client, timestamp, entry, *extra): + logging.debug('__jabber_server_changed_cb') + + bus = dbus.Bus() + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._server_account.object_path) + + server = client.get_string( + '/desktop/sugar/collaboration/jabber_server') + account_id = self._get_jabber_account_id() + params_needing_reconnect = account.UpdateParameters( + {'server': server, + 'account': account_id, + 'register': True}, + dbus.Array([], 's'), dbus_interface=ACCOUNT) + if params_needing_reconnect: + account.Reconnect() + + self._update_jid() + + def __nick_changed_cb(self, client, timestamp, entry, *extra): + logging.debug('__nick_changed_cb') + + nick = client.get_string('/desktop/sugar/user/nick') + + bus = dbus.Bus() + server_obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._server_account.object_path) + server_obj.Set(ACCOUNT, 'Nickname', nick, + dbus_interface=PROPERTIES_IFACE) + + link_local_obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._link_local_account.object_path) + link_local_obj.Set(ACCOUNT, 'Nickname', nick, + dbus_interface=PROPERTIES_IFACE) + params_needing_reconnect = link_local_obj.UpdateParameters( + {'nickname': nick, 'published-name': self._get_published_name()}, + dbus.Array([], 's'), dbus_interface=ACCOUNT) + if params_needing_reconnect: + link_local_obj.Reconnect() + + self._update_jid() + + def _update_jid(self): + bus = dbus.Bus() + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._link_local_account.object_path) + + account_id = self._get_jabber_account_id() + params_needing_reconnect = account.UpdateParameters( + {'jid': account_id}, dbus.Array([], 's'), dbus_interface=ACCOUNT) + if params_needing_reconnect: + account.Reconnect() + + def __buddy_added_cb(self, account, contact_id, nick, handle): + logging.debug('__buddy_added_cb %r', contact_id) + + if contact_id in self._buddies: + logging.debug('__buddy_added_cb buddy already tracked') + return + + buddy = BuddyModel( + nick=nick, + account=account.object_path, + contact_id=contact_id, + handle=handle) + self._buddies[contact_id] = buddy + + def __buddy_updated_cb(self, account, contact_id, properties): + logging.debug('__buddy_updated_cb %r', contact_id) + if contact_id is None: + # Don't know the contact-id yet, will get the full state later + return + + if contact_id not in self._buddies: + logging.debug('__buddy_updated_cb Unknown buddy with contact_id' + ' %r', contact_id) + return + + buddy = self._buddies[contact_id] + + is_new = buddy.props.key is None and 'key' in properties + + if 'color' in properties: + buddy.props.color = XoColor(properties['color']) + + if 'key' in properties: + buddy.props.key = properties['key'] + + nick_key = CONNECTION_INTERFACE_ALIASING + '/alias' + if nick_key in properties: + buddy.props.nick = properties[nick_key] + + if is_new: + self.emit('buddy-added', buddy) + + def __buddy_removed_cb(self, account, contact_id): + logging.debug('Neighborhood.__buddy_removed_cb %r', contact_id) + if contact_id not in self._buddies: + logging.debug('Neighborhood.__buddy_removed_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + + buddy = self._buddies[contact_id] + del self._buddies[contact_id] + + if buddy.props.key is not None: + self.emit('buddy-removed', buddy) + + def __activity_added_cb(self, account, room_handle, activity_id): + logging.debug('__activity_added_cb %r %r', room_handle, activity_id) + if activity_id in self._activities: + logging.debug('__activity_added_cb activity already tracked') + return + + activity = ActivityModel(activity_id, room_handle) + self._activities[activity_id] = activity + + def __activity_updated_cb(self, account, activity_id, properties): + logging.debug('__activity_updated_cb %r %r', activity_id, properties) + if activity_id not in self._activities: + logging.debug('__activity_updated_cb Unknown activity with ' + 'activity_id %r', activity_id) + return + + registry = bundleregistry.get_registry() + bundle = registry.get_bundle(properties['type']) + if not bundle: + logging.warning('Ignoring shared activity we don''t have') + return + + activity = self._activities[activity_id] + + is_new = activity.props.bundle is None + + activity.props.color = XoColor(properties['color']) + activity.props.bundle = bundle + activity.props.name = properties['name'] + activity.props.private = properties['private'] + + if is_new: + self._shell_model.add_shared_activity(activity_id, + activity.props.color) + self.emit('activity-added', activity) + + def __activity_removed_cb(self, account, activity_id): + logging.debug('__activity_removed_cb %r', activity_id) + if activity_id not in self._activities: + logging.debug('Unknown activity with id %s. Already removed?', + activity_id) + return + activity = self._activities[activity_id] + del self._activities[activity_id] + self._shell_model.remove_shared_activity(activity_id) + + if activity.props.bundle is not None: + self.emit('activity-removed', activity) + + def __current_activity_updated_cb(self, account, contact_id, activity_id): + logging.debug('__current_activity_updated_cb %r %r', contact_id, + activity_id) + if contact_id not in self._buddies: + logging.debug('__current_activity_updated_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + if activity_id and activity_id not in self._activities: + logging.debug('__current_activity_updated_cb Unknown activity with' + ' id %s', activity_id) + activity_id = '' + + buddy = self._buddies[contact_id] + if buddy.props.current_activity is not None: + if buddy.props.current_activity.activity_id == activity_id: + return + buddy.props.current_activity.remove_current_buddy(buddy) + + if activity_id: + activity = self._activities[activity_id] + buddy.props.current_activity = activity + activity.add_current_buddy(buddy) + else: + buddy.props.current_activity = None + + def __buddy_joined_activity_cb(self, account, contact_id, activity_id): + if contact_id not in self._buddies: + logging.debug('__buddy_joined_activity_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + + if activity_id not in self._activities: + logging.debug('__buddy_joined_activity_cb Unknown activity with ' + 'activity_id %r', activity_id) + return + + self._activities[activity_id].add_buddy(self._buddies[contact_id]) + + def __buddy_left_activity_cb(self, account, contact_id, activity_id): + if contact_id not in self._buddies: + logging.debug('__buddy_left_activity_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + + if activity_id not in self._activities: + logging.debug('__buddy_left_activity_cb Unknown activity with ' + 'activity_id %r', activity_id) + return + + self._activities[activity_id].remove_buddy(self._buddies[contact_id]) + + def get_buddies(self): + return self._buddies.values() + + def get_buddy_by_key(self, key): + for buddy in self._buddies.values(): + if buddy.key == key: + return buddy + return None + + def get_buddy_by_handle(self, contact_handle): + for buddy in self._buddies.values(): + if not buddy.is_owner() and buddy.handle == contact_handle: + return buddy + return None + + def get_activity(self, activity_id): + return self._activities.get(activity_id, None) + + def get_activity_by_room(self, room_handle): + for activity in self._activities.values(): + if activity.room_handle == room_handle: + return activity + return None + + def get_activities(self): + return self._activities.values() + + +def get_model(): + global _model + if _model is None: + _model = Neighborhood() + return _model diff --git a/src/jarabe/model/network.py b/src/jarabe/model/network.py new file mode 100644 index 0000000..cc02b58 --- /dev/null +++ b/src/jarabe/model/network.py @@ -0,0 +1,1096 @@ +# Copyright (C) 2008 Red Hat, Inc. +# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer +# Copyright (C) 2009-2010 One Laptop per Child +# Copyright (C) 2009 Paraguay Educa, Martin Abente +# Copyright (C) 2010 Plan Ceibal, Daniel Castelo +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging +import os + +import dbus +import dbus.service +import gobject +import ConfigParser +import gconf +import ctypes + +from sugar import dispatch +from sugar import env +from sugar.util import unique_id + +NM_STATE_UNKNOWN = 0 +NM_STATE_ASLEEP = 10 +NM_STATE_DISCONNECTED = 20 +NM_STATE_DISCONNECTING = 30 +NM_STATE_CONNECTING = 40 +NM_STATE_CONNECTED_LOCAL = 50 +NM_STATE_CONNECTED_SITE = 60 +NM_STATE_CONNECTED_GLOBAL = 70 + +NM_DEVICE_TYPE_UNKNOWN = 0 +NM_DEVICE_TYPE_ETHERNET = 1 +NM_DEVICE_TYPE_WIFI = 2 +NM_DEVICE_TYPE_UNUSED1 = 3 +NM_DEVICE_TYPE_UNUSED2 = 4 +NM_DEVICE_TYPE_BT = 5 +NM_DEVICE_TYPE_OLPC_MESH = 6 +NM_DEVICE_TYPE_WIMAX = 7 +NM_DEVICE_TYPE_MODEM = 8 + +NM_DEVICE_STATE_UNKNOWN = 0 +NM_DEVICE_STATE_UNMANAGED = 10 +NM_DEVICE_STATE_UNAVAILABLE = 20 +NM_DEVICE_STATE_DISCONNECTED = 30 +NM_DEVICE_STATE_PREPARE = 40 +NM_DEVICE_STATE_CONFIG = 50 +NM_DEVICE_STATE_NEED_AUTH = 60 +NM_DEVICE_STATE_IP_CONFIG = 70 +NM_DEVICE_STATE_IP_CHECK = 80 +NM_DEVICE_STATE_SECONDARIES = 90 +NM_DEVICE_STATE_ACTIVATED = 100 +NM_DEVICE_STATE_DEACTIVATING = 110 +NM_DEVICE_STATE_FAILED = 120 + +NM_CONNECTION_TYPE_802_11_WIRELESS = '802-11-wireless' +NM_CONNECTION_TYPE_GSM = 'gsm' + +NM_ACTIVE_CONNECTION_STATE_UNKNOWN = 0 +NM_ACTIVE_CONNECTION_STATE_ACTIVATING = 1 +NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2 +NM_ACTIVE_CONNECTION_STATE_DEACTIVATING = 3 + +NM_DEVICE_STATE_REASON_UNKNOWN = 0 +NM_DEVICE_STATE_REASON_NONE = 1 +NM_DEVICE_STATE_REASON_NOW_MANAGED = 2 +NM_DEVICE_STATE_REASON_NOW_UNMANAGED = 3 +NM_DEVICE_STATE_REASON_CONFIG_FAILED = 4 +NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE = 5 +NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED = 6 +NM_DEVICE_STATE_REASON_NO_SECRETS = 7 +NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 +NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED = 9 +NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED = 10 +NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT = 11 +NM_DEVICE_STATE_REASON_PPP_START_FAILED = 12 +NM_DEVICE_STATE_REASON_PPP_DISCONNECT = 13 +NM_DEVICE_STATE_REASON_PPP_FAILED = 14 +NM_DEVICE_STATE_REASON_DHCP_START_FAILED = 15 +NM_DEVICE_STATE_REASON_DHCP_ERROR = 16 +NM_DEVICE_STATE_REASON_DHCP_FAILED = 17 +NM_DEVICE_STATE_REASON_SHARED_START_FAILED = 18 +NM_DEVICE_STATE_REASON_SHARED_FAILED = 19 +NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED = 20 +NM_DEVICE_STATE_REASON_AUTOIP_ERROR = 21 +NM_DEVICE_STATE_REASON_AUTOIP_FAILED = 22 +NM_DEVICE_STATE_REASON_MODEM_BUSY = 23 +NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE = 24 +NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER = 25 +NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT = 26 +NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED = 27 +NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED = 28 +NM_DEVICE_STATE_REASON_GSM_APN_FAILED = 29 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING = 30 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED = 31 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT = 32 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED = 33 +NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED = 34 +NM_DEVICE_STATE_REASON_FIRMWARE_MISSING = 35 +NM_DEVICE_STATE_REASON_REMOVED = 36 +NM_DEVICE_STATE_REASON_SLEEPING = 37 +NM_DEVICE_STATE_REASON_CONNECTION_REMOVED = 38 +NM_DEVICE_STATE_REASON_USER_REQUESTED = 39 +NM_DEVICE_STATE_REASON_CARRIER = 40 +NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED = 41 +NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE = 42 +NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND = 43 +NM_DEVICE_STATE_REASON_BT_FAILED = 44 +NM_DEVICE_STATE_REASON_LAST = 0xFFFF + +NM_802_11_AP_FLAGS_NONE = 0x00000000 +NM_802_11_AP_FLAGS_PRIVACY = 0x00000001 + +NM_802_11_AP_SEC_NONE = 0x0 +NM_802_11_AP_SEC_PAIR_WEP40 = 0x1 +NM_802_11_AP_SEC_PAIR_WEP104 = 0x2 +NM_802_11_AP_SEC_PAIR_TKIP = 0x4 +NM_802_11_AP_SEC_PAIR_CCMP = 0x8 +NM_802_11_AP_SEC_GROUP_WEP40 = 0x10 +NM_802_11_AP_SEC_GROUP_WEP104 = 0x20 +NM_802_11_AP_SEC_GROUP_TKIP = 0x40 +NM_802_11_AP_SEC_GROUP_CCMP = 0x80 +NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x100 +NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x200 + +NM_802_11_MODE_UNKNOWN = 0 +NM_802_11_MODE_ADHOC = 1 +NM_802_11_MODE_INFRA = 2 + +NM_WIFI_DEVICE_CAP_NONE = 0x00000000 +NM_WIFI_DEVICE_CAP_CIPHER_WEP40 = 0x00000001 +NM_WIFI_DEVICE_CAP_CIPHER_WEP104 = 0x00000002 +NM_WIFI_DEVICE_CAP_CIPHER_TKIP = 0x00000004 +NM_WIFI_DEVICE_CAP_CIPHER_CCMP = 0x00000008 +NM_WIFI_DEVICE_CAP_WPA = 0x00000010 +NM_WIFI_DEVICE_CAP_RSN = 0x00000020 + +NM_BT_CAPABILITY_NONE = 0x00000000 +NM_BT_CAPABILITY_DUN = 0x00000001 +NM_BT_CAPABILITY_NAP = 0x00000002 + +NM_DEVICE_MODEM_CAPABILITY_NONE = 0x00000000 +NM_DEVICE_MODEM_CAPABILITY_POTS = 0x00000001 +NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO = 0x00000002 +NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS = 0x00000004 +NM_DEVICE_MODEM_CAPABILITY_LTE = 0x00000008 + +SETTINGS_SERVICE = 'org.freedesktop.NetworkManager' + +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_PATH = '/org/freedesktop/NetworkManager' +NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' +NM_WIRED_IFACE = 'org.freedesktop.NetworkManager.Device.Wired' +NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +NM_MODEM_IFACE = 'org.freedesktop.NetworkManager.Device.Modem' +NM_OLPC_MESH_IFACE = 'org.freedesktop.NetworkManager.Device.OlpcMesh' +NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings' +NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings' +NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection' +NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' + +NM_SECRET_AGENT_IFACE = 'org.freedesktop.NetworkManager.SecretAgent' +NM_SECRET_AGENT_PATH = '/org/freedesktop/NetworkManager/SecretAgent' +NM_AGENT_MANAGER_IFACE = 'org.freedesktop.NetworkManager.AgentManager' +NM_AGENT_MANAGER_PATH = '/org/freedesktop/NetworkManager/AgentManager' + +NM_AGENT_MANAGER_ERR_NO_SECRETS = 'org.freedesktop.NetworkManager.AgentManager.NoSecrets' + +GSM_CONNECTION_ID = 'Sugar Modem Connection' +GSM_BAUD_RATE = 115200 +GSM_USERNAME_PATH = '/desktop/sugar/network/gsm/username' +GSM_PASSWORD_PATH = '/desktop/sugar/network/gsm/password' +GSM_NUMBER_PATH = '/desktop/sugar/network/gsm/number' +GSM_APN_PATH = '/desktop/sugar/network/gsm/apn' +GSM_PIN_PATH = '/desktop/sugar/network/gsm/pin' +GSM_PUK_PATH = '/desktop/sugar/network/gsm/puk' + +ADHOC_CONNECTION_ID_PREFIX = 'Sugar Ad-hoc Network ' +MESH_CONNECTION_ID_PREFIX = 'OLPC Mesh Network ' +XS_MESH_CONNECTION_ID_PREFIX = 'OLPC XS Mesh Network ' + +_network_manager = None +_nm_settings = None +_secret_agent = None +_connections = None + +_nm_device_state_reason_description = None + + +def get_error_by_reason(reason): + global _nm_device_state_reason_description + + if _nm_device_state_reason_description is None: + _nm_device_state_reason_description = { + NM_DEVICE_STATE_REASON_UNKNOWN: + _('The reason for the device state change is unknown.'), + NM_DEVICE_STATE_REASON_NONE: + _('The state change is normal.'), + NM_DEVICE_STATE_REASON_NOW_MANAGED: + _('The device is now managed.'), + NM_DEVICE_STATE_REASON_NOW_UNMANAGED: + _('The device is no longer managed.'), + NM_DEVICE_STATE_REASON_CONFIG_FAILED: + _('The device could not be readied for configuration.'), + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE: + _('IP configuration could not be reserved ' + '(no available address, timeout, etc).'), + NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED: + _('The IP configuration is no longer valid.'), + NM_DEVICE_STATE_REASON_NO_SECRETS: + _('Secrets were required, but not provided.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT: + _('The 802.1X supplicant disconnected from ' + 'the access point or authentication server.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED: + _('Configuration of the 802.1X supplicant failed.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED: + _('The 802.1X supplicant quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT: + _('The 802.1X supplicant took too long to authenticate.'), + NM_DEVICE_STATE_REASON_PPP_START_FAILED: + _('The PPP service failed to start within the allowed time.'), + NM_DEVICE_STATE_REASON_PPP_DISCONNECT: + _('The PPP service disconnected unexpectedly.'), + NM_DEVICE_STATE_REASON_PPP_FAILED: + _('The PPP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_DHCP_START_FAILED: + _('The DHCP service failed to start within the allowed time.'), + NM_DEVICE_STATE_REASON_DHCP_ERROR: + _('The DHCP service reported an unexpected error.'), + NM_DEVICE_STATE_REASON_DHCP_FAILED: + _('The DHCP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_SHARED_START_FAILED: + _('The shared connection service failed to start.'), + NM_DEVICE_STATE_REASON_SHARED_FAILED: + _('The shared connection service quit or failed' + ' unexpectedly.'), + NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED: + _('The AutoIP service failed to start.'), + NM_DEVICE_STATE_REASON_AUTOIP_ERROR: + _('The AutoIP service reported an unexpected error.'), + NM_DEVICE_STATE_REASON_AUTOIP_FAILED: + _('The AutoIP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_MODEM_BUSY: + _('Dialing failed because the line was busy.'), + NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE: + _('Dialing failed because there was no dial tone.'), + NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER: + _('Dialing failed because there was no carrier.'), + NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT: + _('Dialing timed out.'), + NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED: + _('Dialing failed.'), + NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED: + _('Modem initialization failed.'), + NM_DEVICE_STATE_REASON_GSM_APN_FAILED: + _('Failed to select the specified GSM APN'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING: + _('Not searching for networks.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED: + _('Network registration was denied.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT: + _('Network registration timed out.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED: + _('Failed to register with the requested GSM network.'), + NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED: + _('PIN check failed.'), + NM_DEVICE_STATE_REASON_FIRMWARE_MISSING: + _('Necessary firmware for the device may be missing.'), + NM_DEVICE_STATE_REASON_REMOVED: + _('The device was removed.'), + NM_DEVICE_STATE_REASON_SLEEPING: + _('NetworkManager went to sleep.'), + NM_DEVICE_STATE_REASON_CONNECTION_REMOVED: + _("The device's active connection was removed " + "or disappeared."), + NM_DEVICE_STATE_REASON_USER_REQUESTED: + _('A user or client requested the disconnection.'), + NM_DEVICE_STATE_REASON_CARRIER: + _("The device's carrier/link changed."), + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED: + _("The device's existing connection was assumed."), + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE: + _("The supplicant is now available."), + NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND: + _("The modem could not be found."), + NM_DEVICE_STATE_REASON_BT_FAILED: + _("The Bluetooth connection failed or timed out."), + NM_DEVICE_STATE_REASON_LAST: + _("Unused."), + } + + return _nm_device_state_reason_description[reason] + + +def frequency_to_channel(frequency): + """Returns the channel matching a given radio channel frequency. If a + frequency is not in the dictionary channel 1 will be returned. + + Keyword arguments: + frequency -- The radio channel frequency in MHz. + + Return: Channel + + """ + ftoc = {2412: 1, 2417: 2, 2422: 3, 2427: 4, + 2432: 5, 2437: 6, 2442: 7, 2447: 8, + 2452: 9, 2457: 10, 2462: 11, 2467: 12, + 2472: 13} + if frequency not in ftoc: + logging.warning('The frequency %s can not be mapped to a channel, ' + 'defaulting to channel 1.', frequency) + return 1 + return ftoc[frequency] + + +def is_sugar_adhoc_network(ssid): + """Checks whether an access point is a sugar Ad-hoc network. + + Keyword arguments: + ssid -- Ssid of the access point. + + Return: Boolean + + """ + return ssid.startswith('Ad-hoc Network') + + +class WirelessSecurity(object): + def __init__(self): + self.key_mgmt = None + self.proto = None + self.group = None + self.pairwise = None + self.wep_key = None + self.psk = None + self.auth_alg = None + + def get_dict(self): + wireless_security = {} + if self.key_mgmt is not None: + wireless_security['key-mgmt'] = self.key_mgmt + if self.proto is not None: + wireless_security['proto'] = self.proto + if self.pairwise is not None: + wireless_security['pairwise'] = self.pairwise + if self.group is not None: + wireless_security['group'] = self.group + if self.wep_key is not None: + wireless_security['wep-key0'] = self.wep_key + if self.psk is not None: + wireless_security['psk'] = self.psk + if self.auth_alg is not None: + wireless_security['auth-alg'] = self.auth_alg + return wireless_security + + +class Wireless(object): + nm_name = '802-11-wireless' + + def __init__(self): + self.ssid = None + self.security = None + self.mode = None + self.band = None + self.channel = None + + def get_dict(self): + wireless = {'ssid': self.ssid} + if self.security: + wireless['security'] = self.security + if self.mode: + wireless['mode'] = self.mode + if self.band: + wireless['band'] = self.band + if self.channel: + wireless['channel'] = self.channel + return wireless + + +class OlpcMesh(object): + nm_name = '802-11-olpc-mesh' + + def __init__(self, channel, anycast_addr): + self.channel = channel + self.anycast_addr = anycast_addr + + def get_dict(self): + ret = { + 'ssid': dbus.ByteArray('olpc-mesh'), + 'channel': self.channel, + } + + if self.anycast_addr: + ret['dhcp-anycast-address'] = dbus.ByteArray(self.anycast_addr) + return ret + + +class ConnectionSettings(object): + def __init__(self): + self.id = None + self.uuid = None + self.type = None + self.autoconnect = False + self.timestamp = None + + def get_dict(self): + connection = {'id': self.id, + 'uuid': self.uuid, + 'type': self.type, + 'autoconnect': self.autoconnect} + if self.timestamp: + connection['timestamp'] = self.timestamp + return connection + + +class IP4Config(object): + def __init__(self): + self.method = None + + def get_dict(self): + ip4_config = {} + if self.method is not None: + ip4_config['method'] = self.method + return ip4_config + + +class Serial(object): + def __init__(self): + self.baud = None + + def get_dict(self): + serial = {} + + if self.baud is not None: + serial['baud'] = self.baud + + return serial + + +class Ppp(object): + def __init__(self): + pass + + def get_dict(self): + ppp = {} + return ppp + + +class Gsm(object): + def __init__(self): + self.apn = None + self.number = None + self.username = None + self.pin = None + self.password = None + + def get_dict(self): + gsm = {} + + if self.apn: + gsm['apn'] = self.apn + if self.number: + gsm['number'] = self.number + if self.username: + gsm['username'] = self.username + if self.password: + gsm['password'] = self.password + if self.pin: + gsm['pin'] = self.pin + + return gsm + + +class Settings(object): + def __init__(self, wireless_cfg=None): + self.connection = ConnectionSettings() + self.ip4_config = None + self.wireless_security = None + + if wireless_cfg is not None: + self.wireless = wireless_cfg + else: + self.wireless = Wireless() + + def get_dict(self): + settings = {} + settings['connection'] = self.connection.get_dict() + settings[self.wireless.nm_name] = self.wireless.get_dict() + if self.wireless_security is not None: + settings['802-11-wireless-security'] = \ + self.wireless_security.get_dict() + if self.ip4_config is not None: + settings['ipv4'] = self.ip4_config.get_dict() + return settings + + +class SettingsGsm(object): + def __init__(self): + self.connection = ConnectionSettings() + self.ip4_config = IP4Config() + self.serial = Serial() + self.ppp = Ppp() + self.gsm = Gsm() + + def get_dict(self): + settings = {} + + settings['connection'] = self.connection.get_dict() + settings['serial'] = self.serial.get_dict() + settings['ppp'] = self.ppp.get_dict() + settings['gsm'] = self.gsm.get_dict() + settings['ipv4'] = self.ip4_config.get_dict() + + return settings + + +class SecretsResponse(object): + """Intermediate object to report the secrets from the dialog + back to the connection object and which will inform NM + """ + def __init__(self, reply_cb, error_cb): + self._reply_cb = reply_cb + self._error_cb = error_cb + + def set_secrets(self, secrets): + self._reply_cb(secrets) + + def set_error(self, error): + self._error_cb(error) + + +def set_connected(): + try: + # try to flush resolver cache - SL#1940 + # ctypes' syntactic sugar does not work + # so we must get the func ptr explicitly + libc = ctypes.CDLL('libc.so.6') + res_init = getattr(libc, '__res_init') + res_init(None) + except: + # pylint: disable=W0702 + logging.exception('Error calling libc.__res_init') + + +class SecretAgent(dbus.service.Object): + def __init__(self): + self._bus = dbus.SystemBus() + dbus.service.Object.__init__(self, self._bus, NM_SECRET_AGENT_PATH) + self.secrets_request = dispatch.Signal() + proxy = self._bus.get_object(NM_IFACE, NM_AGENT_MANAGER_PATH) + proxy.Register("org.sugarlabs.sugar", + dbus_interface=NM_AGENT_MANAGER_IFACE, + reply_handler=self._register_reply_cb, + error_handler=self._register_error_cb) + + def _register_reply_cb(self): + logging.debug("SecretAgent registered") + + def _register_error_cb(self, error): + logging.error("Failed to register SecretAgent: %s", error) + + @dbus.service.method(NM_SECRET_AGENT_IFACE, + async_callbacks=('reply', 'error'), + in_signature='a{sa{sv}}osasb', + out_signature='a{sa{sv}}', + sender_keyword='sender', + byte_arrays=True) + def GetSecrets(self, settings, connection_path, setting_name, hints, + request_new, reply, error, sender=None): + if setting_name != '802-11-wireless-security': + raise ValueError("Unsupported setting type %s" % (setting_name,)) + if not sender: + raise Exception("Internal error: couldn't get sender") + uid = self._bus.get_unix_user(sender) + if uid != 0: + raise Exception("UID %d not authorized" % (uid,)) + + response = SecretsResponse(reply, error) + self.secrets_request.send(self, settings=settings, response=response) + + +class AccessPoint(gobject.GObject): + __gsignals__ = { + 'props-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, device, model): + self.__gobject_init__() + self.device = device + self.model = model + + self._initialized = False + self._bus = dbus.SystemBus() + + self.ssid = '' + self.strength = 0 + self.flags = 0 + self.wpa_flags = 0 + self.rsn_flags = 0 + self.mode = 0 + self.channel = 0 + + def initialize(self): + model_props = dbus.Interface(self.model, dbus.PROPERTIES_IFACE) + model_props.GetAll(NM_ACCESSPOINT_IFACE, byte_arrays=True, + reply_handler=self._ap_properties_changed_cb, + error_handler=self._get_all_props_error_cb) + + self._bus.add_signal_receiver(self._ap_properties_changed_cb, + signal_name='PropertiesChanged', + path=self.model.object_path, + dbus_interface=NM_ACCESSPOINT_IFACE, + byte_arrays=True) + + def network_hash(self): + """ + This is a hash which uniquely identifies the network that this AP + is a bridge to. i.e. its expected for 2 APs with identical SSID and + other settings to have the same network hash, because we assume that + they are a part of the same underlying network. + """ + + # based on logic from nm-applet + fl = 0 + + if self.mode == NM_802_11_MODE_INFRA: + fl |= 1 << 0 + elif self.mode == NM_802_11_MODE_ADHOC: + fl |= 1 << 1 + else: + fl |= 1 << 2 + + # Separate out no encryption, WEP-only, and WPA-capable */ + if (not (self.flags & NM_802_11_AP_FLAGS_PRIVACY)) \ + and self.wpa_flags == NM_802_11_AP_SEC_NONE \ + and self.rsn_flags == NM_802_11_AP_SEC_NONE: + fl |= 1 << 3 + elif (self.flags & NM_802_11_AP_FLAGS_PRIVACY) \ + and self.wpa_flags == NM_802_11_AP_SEC_NONE \ + and self.rsn_flags == NM_802_11_AP_SEC_NONE: + fl |= 1 << 4 + elif (not (self.flags & NM_802_11_AP_FLAGS_PRIVACY)) \ + and self.wpa_flags != NM_802_11_AP_SEC_NONE \ + and self.rsn_flags != NM_802_11_AP_SEC_NONE: + fl |= 1 << 5 + else: + fl |= 1 << 6 + + hashstr = str(fl) + '@' + self.ssid + return hash(hashstr) + + def _update_properties(self, properties): + if self._initialized: + old_hash = self.network_hash() + else: + old_hash = None + + if 'Ssid' in properties: + self.ssid = properties['Ssid'] + if 'Strength' in properties: + self.strength = properties['Strength'] + if 'Flags' in properties: + self.flags = properties['Flags'] + if 'WpaFlags' in properties: + self.wpa_flags = properties['WpaFlags'] + if 'RsnFlags' in properties: + self.rsn_flags = properties['RsnFlags'] + if 'Mode' in properties: + self.mode = properties['Mode'] + if 'Frequency' in properties: + self.channel = frequency_to_channel(properties['Frequency']) + + self._initialized = True + self.emit('props-changed', old_hash) + + def _get_all_props_error_cb(self, err): + logging.error('Error getting the access point properties: %s', err) + + def _ap_properties_changed_cb(self, properties): + self._update_properties(properties) + + def disconnect(self): + self._bus.remove_signal_receiver(self._ap_properties_changed_cb, + signal_name='PropertiesChanged', + path=self.model.object_path, + dbus_interface=NM_ACCESSPOINT_IFACE) + + +def get_manager(): + global _network_manager + if _network_manager is None: + obj = dbus.SystemBus().get_object(NM_SERVICE, NM_PATH) + _network_manager = dbus.Interface(obj, NM_IFACE) + return _network_manager + + +def _get_settings(): + global _nm_settings + if _nm_settings is None: + obj = dbus.SystemBus().get_object(NM_SERVICE, NM_SETTINGS_PATH) + _nm_settings = dbus.Interface(obj, NM_SETTINGS_IFACE) + _migrate_old_wifi_connections() + _migrate_old_gsm_connection() + return _nm_settings + + +def get_secret_agent(): + global _secret_agent + if _secret_agent is None: + _secret_agent = SecretAgent() + return _secret_agent + + +def _activate_reply_cb(connection_path): + logging.debug('Activated connection: %s', connection_path) + + +def _activate_error_cb(err): + logging.error('Failed to activate connection: %s', err) + + +def _add_and_activate_reply_cb(settings_path, connection_path): + logging.debug('Added and activated connection: %s', connection_path) + + +def _add_and_activate_error_cb(err): + logging.error('Failed to add and activate connection: %s', err) + + +class Connection(gobject.GObject): + __gsignals__ = { + 'removed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + def __init__(self, bus, path): + gobject.GObject.__init__(self) + obj = bus.get_object(NM_SERVICE, path) + self._connection = dbus.Interface(obj, NM_CONNECTION_IFACE) + self._removed_handle = self._connection.connect_to_signal( + 'Removed', self._removed_cb) + self._updated_handle = self._connection.connect_to_signal( + 'Updated', self._updated_cb) + self._settings = self._connection.GetSettings(byte_arrays=True) + + def _updated_cb(self): + self._settings = self._connection.GetSettings(byte_arrays=True) + + def _removed_cb(self): + self._updated_handle.remove() + self._removed_handle.remove() + self.emit('removed') + + def get_settings(self, stype=None): + if not stype: + return self._settings + elif stype in self._settings: + return self._settings[stype] + else: + return None + + def get_secrets(self, stype, reply_handler, error_handler): + return self._connection.GetSecrets(stype, byte_arrays=True, + reply_handler=reply_handler, + error_handler=error_handler) + + def update_settings(self, settings): + self._connection.Update(settings) + + def activate(self, device_o, reply_handler=_activate_reply_cb, + error_handler=_activate_error_cb): + activate_connection_by_path(self.get_path(), device_o, + reply_handler=reply_handler, + error_handler=error_handler) + + def delete(self): + self._connection.Delete() + + def get_ssid(self): + wifi_settings = self.get_settings('802-11-wireless') + if wifi_settings and 'ssid' in wifi_settings: + return wifi_settings['ssid'] + else: + return None + + def get_id(self): + return self.get_settings('connection')['id'] + + def get_path(self): + return self._connection.object_path + + def is_sugar_internal_connection(self): + """Returns True if this connection is a 'special' Sugar connection, + i.e. one that has been created by Sugar internals and should not be + visible to the user or deleted by connection-clearing code.""" + connection_id = self.get_id() + return connection_id == GSM_CONNECTION_ID \ + or connection_id.startswith(ADHOC_CONNECTION_ID_PREFIX) \ + or connection_id.startswith(MESH_CONNECTION_ID_PREFIX) \ + or connection_id.startswith(XS_MESH_CONNECTION_ID_PREFIX) + + +class Connections(object): + def __init__(self): + self._bus = dbus.SystemBus() + self._connections = [] + + settings = _get_settings() + settings.connect_to_signal('NewConnection', self._new_connection_cb) + + for connection_o in settings.ListConnections(): + self._monitor_connection(connection_o) + + def get_list(self): + return self._connections + + def _monitor_connection(self, connection_o): + connection = Connection(self._bus, connection_o) + connection.connect('removed', self._connection_removed_cb) + self._connections.append(connection) + + def _new_connection_cb(self, connection_o): + self._monitor_connection(connection_o) + + def _connection_removed_cb(self, connection): + connection.disconnect_by_func(self._connection_removed_cb) + self._connections.remove(connection) + + def clear(self): + """Remove all connections except Sugar-internal ones.""" + + # copy the list, to avoid problems with removing elements of a list + # while looping over it + connections = list(self._connections) + for connection in connections: + if connection.is_sugar_internal_connection(): + continue + try: + connection.delete() + except dbus.DBusException: + logging.debug("Could not remove connection %s", + connection.get_id()) + + +def get_connections(): + global _connections + if _connections is None: + _connections = Connections() + return _connections + + +def find_connection_by_ssid(ssid): + # FIXME: this check should be more extensive. + # it should look at mode (infra/adhoc), band, security, and really + # anything that is stored in the settings. + for connection in get_connections().get_list(): + if connection.get_ssid() == ssid: + return connection + return None + + +def find_connection_by_id(connection_id): + for connection in get_connections().get_list(): + if connection.get_id() == connection_id: + return connection + return None + + +def _add_connection_reply_cb(connection): + logging.debug('Added connection: %s', connection) + + +def _add_connection_error_cb(err): + logging.error('Failed to add connection: %s', err) + + +def add_connection(settings, reply_handler=_add_connection_reply_cb, + error_handler=_add_connection_error_cb): + _get_settings().AddConnection(settings.get_dict(), + reply_handler=reply_handler, + error_handler=error_handler) + + +def activate_connection_by_path(connection, device_o, + reply_handler=_activate_reply_cb, + error_handler=_activate_error_cb): + get_manager().ActivateConnection(connection, + device_o, + '/', + reply_handler=reply_handler, + error_handler=error_handler) + + +def add_and_activate_connection(device_o, settings, specific_object): + manager = get_manager() + manager.AddAndActivateConnection(settings.get_dict(), device_o, + specific_object, + reply_handler=_add_and_activate_reply_cb, + error_handler=_add_and_activate_error_cb) + + +def _migrate_old_wifi_connections(): + """Migrate connections.cfg from Sugar-0.94 and previous to NetworkManager + system-wide connections + """ + + profile_path = env.get_profile_path() + config_path = os.path.join(profile_path, 'nm', 'connections.cfg') + if not os.path.exists(config_path): + return + + config = ConfigParser.ConfigParser() + try: + if not config.read(config_path): + logging.error('Error reading the nm config file') + return + except ConfigParser.ParsingError: + logging.exception('Error reading the nm config file') + return + + for section in config.sections(): + try: + settings = Settings() + settings.connection.id = section + ssid = config.get(section, 'ssid') + settings.wireless.ssid = dbus.ByteArray(ssid) + uuid = config.get(section, 'uuid') + settings.connection.uuid = uuid + nmtype = config.get(section, 'type') + settings.connection.type = nmtype + autoconnect = bool(config.get(section, 'autoconnect')) + settings.connection.autoconnect = autoconnect + + if config.has_option(section, 'timestamp'): + timestamp = int(config.get(section, 'timestamp')) + settings.connection.timestamp = timestamp + + if config.has_option(section, 'key-mgmt'): + settings.wireless_security = WirelessSecurity() + mgmt = config.get(section, 'key-mgmt') + settings.wireless_security.key_mgmt = mgmt + security = config.get(section, 'security') + settings.wireless.security = security + key = config.get(section, 'key') + if mgmt == 'none': + settings.wireless_security.wep_key = key + auth_alg = config.get(section, 'auth-alg') + settings.wireless_security.auth_alg = auth_alg + elif mgmt == 'wpa-psk': + settings.wireless_security.psk = key + if config.has_option(section, 'proto'): + value = config.get(section, 'proto') + settings.wireless_security.proto = value + if config.has_option(section, 'group'): + value = config.get(section, 'group') + settings.wireless_security.group = value + if config.has_option(section, 'pairwise'): + value = config.get(section, 'pairwise') + settings.wireless_security.pairwise = value + except ConfigParser.Error: + logging.exception('Error reading section') + else: + add_connection(settings) + + os.unlink(config_path) + + +def create_gsm_connection(username, password, number, apn, pin): + settings = SettingsGsm() + settings.gsm.username = username + settings.gsm.number = number + settings.gsm.apn = apn + settings.gsm.pin = pin + settings.gsm.password = password + + settings.connection.id = GSM_CONNECTION_ID + settings.connection.type = NM_CONNECTION_TYPE_GSM + settings.connection.uuid = unique_id() + settings.connection.autoconnect = False + settings.ip4_config.method = 'auto' + settings.serial.baud = GSM_BAUD_RATE + + add_connection(settings) + + +def _migrate_old_gsm_connection(): + if find_gsm_connection(): + # don't attempt migration if a NM-level connection already exists + return + + client = gconf.client_get_default() + + username = client.get_string(GSM_USERNAME_PATH) or '' + password = client.get_string(GSM_PASSWORD_PATH) or '' + number = client.get_string(GSM_NUMBER_PATH) or '' + apn = client.get_string(GSM_APN_PATH) or '' + pin = client.get_string(GSM_PIN_PATH) or '' + + if apn or number: + logging.info("Migrating old GSM connection details") + try: + create_gsm_connection(username, password, number, apn, pin) + # remove old connection + for setting in (GSM_USERNAME_PATH, GSM_PASSWORD_PATH, + GSM_NUMBER_PATH, GSM_APN_PATH, GSM_PIN_PATH, + GSM_PUK_PATH): + client.set_string(setting, '') + except Exception: + logging.exception('Error adding gsm connection to NMSettings.') + + +def find_gsm_connection(): + return find_connection_by_id(GSM_CONNECTION_ID) + + +def disconnect_access_points(ap_paths): + """ + Disconnect all devices connected to any of the given access points. + """ + bus = dbus.SystemBus() + netmgr_obj = bus.get_object(NM_SERVICE, NM_PATH) + netmgr = dbus.Interface(netmgr_obj, NM_IFACE) + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + active_connection_paths = netmgr_props.Get(NM_IFACE, 'ActiveConnections') + + for conn_path in active_connection_paths: + conn_obj = bus.get_object(NM_IFACE, conn_path) + conn_props = dbus.Interface(conn_obj, dbus.PROPERTIES_IFACE) + ap_path = conn_props.Get(NM_ACTIVE_CONN_IFACE, 'SpecificObject') + if ap_path == '/' or ap_path not in ap_paths: + continue + + dev_paths = conn_props.Get(NM_ACTIVE_CONN_IFACE, 'Devices') + for dev_path in dev_paths: + dev_obj = bus.get_object(NM_SERVICE, dev_path) + dev = dbus.Interface(dev_obj, NM_DEVICE_IFACE) + dev.Disconnect() + + +def _is_non_printable(char): + """ + Return True if char is a non-printable unicode character, False otherwise + """ + return (char < u' ') or (u'~' < char < u'\xA0') or (char == u'\xAD') + + +def ssid_to_display_name(ssid): + """Convert an SSID into a unicode string for recognising Access Points + + Return a unicode string that's useful for recognising and + distinguishing between Access Points (APs). + + IEEE 802.11 defines SSIDs as arbitrary byte sequences. As random + bytes are not very user-friendly, most APs use some human-readable + character string as SSID. However, because there's no standard + specifying what encoding to use, AP vendors chose various + different encodings. Since there's also no indication of what + encoding was used for a particular SSID, the best we can do for + turning an SSID into a displayable string is to try a couple of + encodings based on some heuristic. + + We're currently using the following heuristic: + + 1. If the SSID is a valid character string consisting only of + printable characters in one of the following encodings (tried in + the given order), decode it accordingly: + UTF-8, ISO-8859-1, Windows-1251. + 2. Return a hex dump of the SSID. + """ + for encoding in ['utf-8', 'iso-8859-1', 'windows-1251']: + try: + display_name = unicode(ssid, encoding) + except UnicodeDecodeError: + continue + + if not [True for char in display_name if _is_non_printable(char)]: + # Only printable characters + return display_name + + return ':'.join(['%02x' % (ord(byte), ) for byte in ssid]) diff --git a/src/jarabe/model/notifications.py b/src/jarabe/model/notifications.py new file mode 100644 index 0000000..ec14056 --- /dev/null +++ b/src/jarabe/model/notifications.py @@ -0,0 +1,98 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import sys +import logging + +import dbus + +from sugar import dispatch + +from jarabe import config + + +_DBUS_SERVICE = 'org.freedesktop.Notifications' +_DBUS_IFACE = 'org.freedesktop.Notifications' +_DBUS_PATH = '/org/freedesktop/Notifications' + +_instance = None + + +class NotificationService(dbus.service.Object): + def __init__(self): + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus) + dbus.service.Object.__init__(self, bus_name, _DBUS_PATH) + + self._notification_counter = 0 + self.notification_received = dispatch.Signal() + self.notification_cancelled = dispatch.Signal() + + @dbus.service.method(_DBUS_IFACE, + in_signature='susssava{sv}i', out_signature='u') + def Notify(self, app_name, replaces_id, app_icon, summary, body, actions, + hints, expire_timeout): + + logging.debug('Received notification: %r', [app_name, replaces_id, + '', summary, body, actions, '', + expire_timeout]) + + if replaces_id > 0: + notification_id = replaces_id + else: + if self._notification_counter == sys.maxint: + self._notification_counter = 1 + else: + self._notification_counter += 1 + notification_id = self._notification_counter + + self.notification_received.send(self, app_name=app_name, + replaces_id=replaces_id, app_icon=app_icon, summary=summary, + body=body, actions=actions, hints=hints, + expire_timeout=expire_timeout) + + return notification_id + + @dbus.service.method(_DBUS_IFACE, in_signature='u', out_signature='') + def CloseNotification(self, notification_id): + self.notification_cancelled.send(self, notification_id=notification_id) + + @dbus.service.method(_DBUS_IFACE, in_signature='', out_signature='as') + def GetCapabilities(self): + return [] + + @dbus.service.method(_DBUS_IFACE, in_signature='', out_signature='sss') + def GetServerInformation(self, name, vendor, version): + return 'Sugar Shell', 'Sugar', config.version + + @dbus.service.signal(_DBUS_IFACE, signature='uu') + def NotificationClosed(self, notification_id, reason): + pass + + @dbus.service.signal(_DBUS_IFACE, signature='us') + def ActionInvoked(self, notification_id, action_key): + pass + + +def get_service(): + global _instance + if not _instance: + _instance = NotificationService() + return _instance + + +def init(): + get_service() diff --git a/src/jarabe/model/olpcmesh.py b/src/jarabe/model/olpcmesh.py new file mode 100644 index 0000000..6ab7ab6 --- /dev/null +++ b/src/jarabe/model/olpcmesh.py @@ -0,0 +1,228 @@ +# Copyright (C) 2009, 2010 One Laptop per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +import gobject + +from jarabe.model import network +from jarabe.model.network import Settings +from jarabe.model.network import OlpcMesh as OlpcMeshSettings +from sugar.util import unique_id + +_XS_ANYCAST = '\xc0\x27\xc0\x27\xc0\x00' + + +class OlpcMeshManager(object): + def __init__(self, mesh_device): + self._bus = dbus.SystemBus() + + # counter for how many asynchronous connection additions we are + # waiting for + self._add_connections_pending = 0 + + self.mesh_device = mesh_device + self.eth_device = self._get_companion_device() + + self._connection_queue = [] + """Stack of connections that we'll iterate through until we find one + that works. Each entry in the list specifies the channel and + whether to seek an XS or not.""" + + # Ensure that all the connections we'll use later are present + for channel in (1, 6, 11): + self._ensure_connection_exists(channel, xs_hosted=True) + self._ensure_connection_exists(channel, xs_hosted=False) + + props = dbus.Interface(self.mesh_device, dbus.PROPERTIES_IFACE) + props.Get(network.NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_mesh_state_reply_cb, + error_handler=self.__get_state_error_cb) + + props = dbus.Interface(self.eth_device, dbus.PROPERTIES_IFACE) + props.Get(network.NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_eth_state_reply_cb, + error_handler=self.__get_state_error_cb) + + self._bus.add_signal_receiver(self.__eth_device_state_changed_cb, + signal_name='StateChanged', + path=self.eth_device.object_path, + dbus_interface=network.NM_DEVICE_IFACE) + + self._bus.add_signal_receiver(self.__mshdev_state_changed_cb, + signal_name='StateChanged', + path=self.mesh_device.object_path, + dbus_interface=network.NM_DEVICE_IFACE) + + self._idle_source = 0 + self._mesh_device_state = network.NM_DEVICE_STATE_UNKNOWN + self._eth_device_state = network.NM_DEVICE_STATE_UNKNOWN + + if self._add_connections_pending == 0: + self.ready() + + def ready(self): + """Called when all connections have been added (if they were not + already present), meaning that we can start the automesh functionality. + """ + if self._have_configured_connections(): + self._start_automesh_timer() + else: + self._start_automesh() + + def _get_companion_device(self): + props = dbus.Interface(self.mesh_device, dbus.PROPERTIES_IFACE) + eth_device_o = props.Get(network.NM_OLPC_MESH_IFACE, 'Companion') + return self._bus.get_object(network.NM_SERVICE, eth_device_o) + + def _have_configured_connections(self): + return len(network.get_connections().get_list()) > 0 + + def _start_automesh_timer(self): + """Start our timer system which basically looks for 10 seconds of + inactivity on both devices, then starts automesh. + + """ + if self._idle_source != 0: + gobject.source_remove(self._idle_source) + self._idle_source = gobject.timeout_add_seconds(10, self._idle_check) + + def __get_state_error_cb(self, err): + logging.debug('Error getting the device state: %s', err) + + def __get_mesh_state_reply_cb(self, state): + self._mesh_device_state = state + self._maybe_schedule_idle_check() + + def __get_eth_state_reply_cb(self, state): + self._eth_device_state = state + self._maybe_schedule_idle_check() + + def __eth_device_state_changed_cb(self, new_state, old_state, reason): + """If a connection is activated on the eth device, stop trying our + automatic connections. + + """ + self._eth_device_state = new_state + self._maybe_schedule_idle_check() + + if new_state >= network.NM_DEVICE_STATE_PREPARE \ + and new_state <= network.NM_DEVICE_STATE_ACTIVATED \ + and len(self._connection_queue) > 0: + self._connection_queue = [] + + def __mshdev_state_changed_cb(self, new_state, old_state, reason): + self._mesh_device_state = new_state + self._maybe_schedule_idle_check() + + if new_state == network.NM_DEVICE_STATE_FAILED: + self._try_next_connection_from_queue() + elif new_state == network.NM_DEVICE_STATE_ACTIVATED \ + and len(self._connection_queue) > 0: + self._empty_connection_queue() + + def _maybe_schedule_idle_check(self): + if self._mesh_device_state == network.NM_DEVICE_STATE_DISCONNECTED \ + and self._eth_device_state == network.NM_DEVICE_STATE_DISCONNECTED: + self._start_automesh_timer() + + def _idle_check(self): + if self._mesh_device_state == network.NM_DEVICE_STATE_DISCONNECTED \ + and self._eth_device_state == network.NM_DEVICE_STATE_DISCONNECTED: + logging.debug('starting automesh due to inactivity') + self._start_automesh() + return False + + @staticmethod + def _get_connection_id(channel, xs_hosted): + if xs_hosted: + return '%s%d' % (network.XS_MESH_CONNECTION_ID_PREFIX, channel) + else: + return '%s%d' % (network.MESH_CONNECTION_ID_PREFIX, channel) + + def _connection_added(self): + if self._add_connections_pending > 0: + self._add_connections_pending = self._add_connections_pending - 1 + if self._add_connections_pending == 0: + self.ready() + + def _add_connection_reply_cb(self, connection): + logging.debug("Added connection: %s", connection) + self._connection_added() + + def _add_connection_err_cb(self, err): + logging.debug("Error adding mesh connection: %s", err) + self._connection_added() + + def _add_connection(self, channel, xs_hosted): + anycast_addr = _XS_ANYCAST if xs_hosted else None + wireless_config = OlpcMeshSettings(channel, anycast_addr) + settings = Settings(wireless_cfg=wireless_config) + if not xs_hosted: + settings.ip4_config = network.IP4Config() + settings.ip4_config.method = 'link-local' + settings.connection.id = self._get_connection_id(channel, xs_hosted) + settings.connection.autoconnect = False + settings.connection.uuid = unique_id() + settings.connection.type = '802-11-olpc-mesh' + network.add_connection(settings, + reply_handler=self._add_connection_reply_cb, + error_handler=self._add_connection_err_cb) + + def _find_connection(self, channel, xs_hosted): + connection_id = self._get_connection_id(channel, xs_hosted) + return network.find_connection_by_id(connection_id) + + def _ensure_connection_exists(self, channel, xs_hosted): + if not self._find_connection(channel, xs_hosted): + self._add_connection(channel, xs_hosted) + + def _activate_connection(self, channel, xs_hosted): + connection = self._find_connection(channel, xs_hosted) + if connection: + connection.activate(self.mesh_device.object_path) + else: + logging.warning("Could not find mesh connection") + + def _try_next_connection_from_queue(self): + if len(self._connection_queue) == 0: + return + + channel, xs_hosted = self._connection_queue.pop() + self._activate_connection(channel, xs_hosted) + + def _empty_connection_queue(self): + self._connection_queue = [] + + def user_activate_channel(self, channel): + """Activate a mesh connection on a user-specified channel. + Looks for XS first, then resorts to simple mesh.""" + self._empty_connection_queue() + self._connection_queue.append((channel, False)) + self._connection_queue.append((channel, True)) + self._try_next_connection_from_queue() + + def _start_automesh(self): + """Start meshing automatically, intended when there are no better + networks to connect to. First looks for XS on all channels, then falls + back to simple mesh on channel 1.""" + self._empty_connection_queue() + self._connection_queue.append((1, False)) + self._connection_queue.append((11, True)) + self._connection_queue.append((6, True)) + self._connection_queue.append((1, True)) + self._try_next_connection_from_queue() diff --git a/src/jarabe/model/screen.py b/src/jarabe/model/screen.py new file mode 100644 index 0000000..7d34d45 --- /dev/null +++ b/src/jarabe/model/screen.py @@ -0,0 +1,45 @@ +# Copyright (C) 2006-2008 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus + + +_HARDWARE_MANAGER_INTERFACE = 'org.freedesktop.ohm.Keystore' +_HARDWARE_MANAGER_SERVICE = 'org.freedesktop.ohm' +_HARDWARE_MANAGER_OBJECT_PATH = '/org/freedesktop/ohm/Keystore' + +_ohm_service = None + + +def _get_ohm(): + global _ohm_service + if _ohm_service is None: + bus = dbus.SystemBus() + proxy = bus.get_object(_HARDWARE_MANAGER_SERVICE, + _HARDWARE_MANAGER_OBJECT_PATH, + follow_name_owner_changes=True) + _ohm_service = dbus.Interface(proxy, _HARDWARE_MANAGER_INTERFACE) + + return _ohm_service + + +def set_dcon_freeze(frozen): + try: + _get_ohm().SetKey('display.dcon_freeze', frozen) + except dbus.DBusException: + logging.error('Cannot unfreeze the DCON') diff --git a/src/jarabe/model/session.py b/src/jarabe/model/session.py new file mode 100644 index 0000000..4e66bdc --- /dev/null +++ b/src/jarabe/model/session.py @@ -0,0 +1,113 @@ +# Copyright (C) 2008, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import dbus +import os +import signal +import sys +import logging + +from sugar import session +from sugar import env + + +_session_manager = None + + +def have_systemd(): + return os.access("/sys/fs/cgroup/systemd", 0) >= 0 + + +class SessionManager(session.SessionManager): + MODE_LOGOUT = 0 + MODE_SHUTDOWN = 1 + MODE_REBOOT = 2 + + def __init__(self): + session.SessionManager.__init__(self) + self._logout_mode = None + + def logout(self): + self._logout_mode = self.MODE_LOGOUT + self.initiate_shutdown() + + def shutdown(self): + self._logout_mode = self.MODE_SHUTDOWN + self.initiate_shutdown() + + def reboot(self): + self._logout_mode = self.MODE_REBOOT + self.initiate_shutdown() + + def shutdown_completed(self): + if env.is_emulator(): + self._close_emulator() + elif self._logout_mode != self.MODE_LOGOUT: + bus = dbus.SystemBus() + if have_systemd(): + try: + proxy = bus.get_object('org.freedesktop.login1', + '/org/freedesktop/login1') + pm = dbus.Interface(proxy, + 'org.freedesktop.login1.Manager') + + if self._logout_mode == self.MODE_SHUTDOWN: + pm.PowerOff(False) + elif self._logout_mode == self.MODE_REBOOT: + pm.Reboot(True) + except: + logging.exception('Can not stop sugar') + self.session.cancel_shutdown() + return + else: + CONSOLEKIT_DBUS_PATH = '/org/freedesktop/ConsoleKit/Manager' + try: + proxy = bus.get_object('org.freedesktop.ConsoleKit', + CONSOLEKIT_DBUS_PATH) + pm = dbus.Interface(proxy, + 'org.freedesktop.ConsoleKit.Manager') + + if self._logout_mode == self.MODE_SHUTDOWN: + pm.Stop() + elif self._logout_mode == self.MODE_REBOOT: + pm.Restart() + except: + logging.exception('Can not stop sugar') + self.session.cancel_shutdown() + return + + session.SessionManager.shutdown_completed(self) + gtk.main_quit() + + def _close_emulator(self): + gtk.main_quit() + + if 'SUGAR_EMULATOR_PID' in os.environ: + pid = int(os.environ['SUGAR_EMULATOR_PID']) + os.kill(pid, signal.SIGTERM) + + # Need to call this ASAP so the atexit handlers get called before we + # get killed by the X (dis)connection + sys.exit() + + +def get_session_manager(): + global _session_manager + + if _session_manager == None: + _session_manager = SessionManager() + return _session_manager diff --git a/src/jarabe/model/shell.py b/src/jarabe/model/shell.py new file mode 100644 index 0000000..31605f7 --- /dev/null +++ b/src/jarabe/model/shell.py @@ -0,0 +1,675 @@ +# Copyright (C) 2006-2007 Owen Williams. +# Copyright (C) 2006-2008 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import time + +import gconf +import wnck +import gobject +import gtk +import dbus + +from sugar import wm +from sugar import dispatch +from sugar.graphics.xocolor import XoColor + +from jarabe.model.bundleregistry import get_registry + +_SERVICE_NAME = 'org.laptop.Activity' +_SERVICE_PATH = '/org/laptop/Activity' +_SERVICE_INTERFACE = 'org.laptop.Activity' + +_model = None + + +class Activity(gobject.GObject): + """Activity which appears in the "Home View" of the Sugar shell + + This class stores the Sugar Shell's metadata regarding a + given activity/application in the system. It interacts with + the sugar.activity.* modules extensively in order to + accomplish its tasks. + """ + + __gtype_name__ = 'SugarHomeActivity' + + LAUNCHING = 0 + LAUNCH_FAILED = 1 + LAUNCHED = 2 + + def __init__(self, activity_info, activity_id, color, window=None): + """Initialise the HomeActivity + + activity_info -- sugar.activity.registry.ActivityInfo instance, + provides the information required to actually + create the new instance. This is, in effect, + the "type" of activity being created. + activity_id -- unique identifier for this instance + of the activity type + _windows -- WnckWindows registered for the activity. The lowest + one in the stack is the main window. + """ + gobject.GObject.__init__(self) + + self._windows = [] + self._service = None + self._activity_id = activity_id + self._activity_info = activity_info + self._launch_time = time.time() + self._launch_status = Activity.LAUNCHING + + if color is not None: + self._color = color + else: + client = gconf.client_get_default() + color = client.get_string('/desktop/sugar/user/color') + self._color = XoColor(color) + + if window is not None: + self.add_window(window) + + self._retrieve_service() + + self._name_owner_changed_handler = None + if not self._service: + bus = dbus.SessionBus() + self._name_owner_changed_handler = bus.add_signal_receiver( + self._name_owner_changed_cb, + signal_name='NameOwnerChanged', + dbus_interface='org.freedesktop.DBus') + + self._launch_completed_hid = get_model().connect('launch-completed', + self.__launch_completed_cb) + self._launch_failed_hid = get_model().connect('launch-failed', + self.__launch_failed_cb) + + def get_launch_status(self): + return self._launch_status + + launch_status = gobject.property(getter=get_launch_status) + + def add_window(self, window): + """Add a window to the windows stack.""" + if not window: + raise ValueError('window must be valid') + self._windows.append(window) + + def remove_window_by_xid(self, xid): + """Remove a window from the windows stack.""" + for wnd in self._windows: + if wnd.get_xid() == xid: + self._windows.remove(wnd) + return True + return False + + def get_service(self): + """Get the activity service + + Note that non-native Sugar applications will not have + such a service, so the return value will be None in + those cases. + """ + + return self._service + + def get_title(self): + """Retrieve the application's root window's suggested title""" + if self._windows: + return self._windows[0].get_name() + else: + return '' + + def get_icon_path(self): + """Retrieve the activity's icon (file) name""" + if self.is_journal(): + icon_theme = gtk.icon_theme_get_default() + info = icon_theme.lookup_icon('activity-journal', + gtk.ICON_SIZE_SMALL_TOOLBAR, 0) + if not info: + return None + fname = info.get_filename() + del info + return fname + elif self._activity_info: + return self._activity_info.get_icon() + else: + return None + + def get_icon_color(self): + """Retrieve the appropriate icon colour for this activity + + Uses activity_id to index into the PresenceService's + set of activity colours, if the PresenceService does not + have an entry (implying that this is not a Sugar-shared application) + uses the local user's profile colour for the icon. + """ + return self._color + + def get_activity_id(self): + """Retrieve the "activity_id" passed in to our constructor + + This is a "globally likely unique" identifier generated by + sugar.util.unique_id + """ + return self._activity_id + + def get_xid(self): + """Retrieve the X-windows ID of our root window""" + if self._windows: + return self._windows[0].get_xid() + else: + return None + + def has_xid(self, xid): + """Check if an X-window with the given xid is in the windows stack""" + if self._windows: + for wnd in self._windows: + if wnd.get_xid() == xid: + return True + return False + + def get_window(self): + """Retrieve the X-windows root window of this application + + This was stored by the add_window method, which was + called by HomeModel._add_activity, which was called + via a callback that looks for all 'window-opened' + events. + + We keep a stack of the windows. The lowest window in the + stack that is still valid we consider the main one. + + HomeModel currently uses a dbus service query on the + activity to determine to which HomeActivity the newly + launched window belongs. + """ + if self._windows: + return self._windows[0] + return None + + def get_type(self): + """Retrieve the activity bundle id for future reference""" + if not self._windows: + return None + else: + return wm.get_bundle_id(self._windows[0]) + + def is_journal(self): + """Returns boolean if the activity is of type JournalActivity""" + return self.get_type() == 'org.laptop.JournalActivity' + + def get_launch_time(self): + """Return the time at which the activity was first launched + + Format is floating-point time.time() value + (seconds since the epoch) + """ + return self._launch_time + + def get_pid(self): + """Returns the activity's PID""" + if not self._windows: + return None + return self._windows[0].get_pid() + + def get_bundle_path(self): + """Returns the activity's bundle directory""" + if self._activity_info is None: + return None + else: + return self._activity_info.get_path() + + def get_activity_name(self): + """Returns the activity's bundle name""" + if self._activity_info is None: + return None + else: + return self._activity_info.get_name() + + def equals(self, activity): + if self._activity_id and activity.get_activity_id(): + return self._activity_id == activity.get_activity_id() + if self._windows[0].get_xid() and activity.get_xid(): + return self._windows[0].get_xid() == activity.get_xid() + return False + + def _get_service_name(self): + if self._activity_id: + return _SERVICE_NAME + self._activity_id + else: + return None + + def _retrieve_service(self): + if not self._activity_id: + return + + try: + bus = dbus.SessionBus() + proxy = bus.get_object(self._get_service_name(), + _SERVICE_PATH + '/' + self._activity_id) + self._service = dbus.Interface(proxy, _SERVICE_INTERFACE) + except dbus.DBusException: + self._service = None + + def _name_owner_changed_cb(self, name, old, new): + if name == self._get_service_name(): + if old and not new: + logging.debug('Activity._name_owner_changed_cb: ' \ + 'activity %s went away', name) + self._name_owner_changed_handler.remove() + self._name_owner_changed_handler = None + self._service = None + elif not old and new: + logging.debug('Activity._name_owner_changed_cb: ' \ + 'activity %s started up', name) + self._retrieve_service() + self.set_active(True) + + def set_active(self, state): + """Propagate the current state to the activity object""" + if self._service is not None: + self._service.SetActive(state, + reply_handler=self._set_active_success, + error_handler=self._set_active_error) + + def _set_active_success(self): + pass + + def _set_active_error(self, err): + logging.error('set_active() failed: %s', err) + + def _set_launch_status(self, value): + get_model().disconnect(self._launch_completed_hid) + get_model().disconnect(self._launch_failed_hid) + self._launch_completed_hid = None + self._launch_failed_hid = None + self._launch_status = value + self.notify('launch_status') + + def __launch_completed_cb(self, model, home_activity): + if home_activity is self: + self._set_launch_status(Activity.LAUNCHED) + + def __launch_failed_cb(self, model, home_activity): + if home_activity is self: + self._set_launch_status(Activity.LAUNCH_FAILED) + + +class ShellModel(gobject.GObject): + """Model of the shell (activity management) + + The ShellModel is basically the point of registration + for all running activities within Sugar. It traps + events that tell the system there is a new activity + being created (generated by the activity factories), + or removed, as well as those which tell us that the + currently focussed activity has changed. + + The HomeModel tracks a set of HomeActivity instances, + which are tracking the window to activity mappings + the activity factories have set up. + """ + + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'active-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'tabbing-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-started': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-completed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-failed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + ZOOM_MESH = 0 + ZOOM_GROUP = 1 + ZOOM_HOME = 2 + ZOOM_ACTIVITY = 3 + + def __init__(self): + gobject.GObject.__init__(self) + + self._screen = wnck.screen_get_default() + self._screen.connect('window-opened', self._window_opened_cb) + self._screen.connect('window-closed', self._window_closed_cb) + self._screen.connect('active-window-changed', + self._active_window_changed_cb) + + self.zoom_level_changed = dispatch.Signal() + + self._desktop_level = self.ZOOM_HOME + self._zoom_level = self.ZOOM_HOME + self._current_activity = None + self._activities = [] + self._shared_activities = {} + self._active_activity = None + self._tabbing_activity = None + self._launchers = {} + + self._screen.toggle_showing_desktop(True) + + def get_launcher(self, activity_id): + return self._launchers.get(str(activity_id)) + + def register_launcher(self, activity_id, launcher): + self._launchers[activity_id] = launcher + + def unregister_launcher(self, activity_id): + if activity_id in self._launchers: + del self._launchers[activity_id] + + def _update_zoom_level(self, window): + if window.get_window_type() == wnck.WINDOW_DIALOG: + return + elif window.get_window_type() == wnck.WINDOW_NORMAL: + new_level = self.ZOOM_ACTIVITY + else: + new_level = self._desktop_level + + if self._zoom_level != new_level: + old_level = self._zoom_level + self._zoom_level = new_level + self.zoom_level_changed.send(self, old_level=old_level, + new_level=new_level) + + def set_zoom_level(self, new_level, x_event_time=0): + old_level = self.zoom_level + if old_level == new_level: + return + + if old_level != self.ZOOM_ACTIVITY: + screen = gtk.gdk.screen_get_default() + active_window_type = screen.get_active_window().get_type_hint() + if active_window_type != gtk.gdk.WINDOW_TYPE_HINT_DESKTOP: + return + + self._zoom_level = new_level + if new_level is not self.ZOOM_ACTIVITY: + self._desktop_level = new_level + + self.zoom_level_changed.send(self, old_level=old_level, + new_level=new_level) + + show_desktop = new_level is not self.ZOOM_ACTIVITY + self._screen.toggle_showing_desktop(show_desktop) + + if new_level is self.ZOOM_ACTIVITY: + # activate the window, in case it was iconified + # (e.g. during sugar launch, the Journal starts in this state) + window = self._active_activity.get_window() + if window: + window.activate(x_event_time or gtk.get_current_event_time()) + + def _get_zoom_level(self): + return self._zoom_level + + zoom_level = property(_get_zoom_level) + + def _get_activities_with_window(self): + ret = [] + for i in self._activities: + if i.get_window() is not None: + ret.append(i) + return ret + + def get_previous_activity(self, current=None): + if not current: + current = self._active_activity + + activities = self._get_activities_with_window() + i = activities.index(current) + if len(activities) == 0: + return None + elif i - 1 >= 0: + return activities[i - 1] + else: + return activities[len(activities) - 1] + + def get_next_activity(self, current=None): + if not current: + current = self._active_activity + + activities = self._get_activities_with_window() + i = activities.index(current) + if len(activities) == 0: + return None + elif i + 1 < len(activities): + return activities[i + 1] + else: + return activities[0] + + def get_active_activity(self): + """Returns the activity that the user is currently working in""" + return self._active_activity + + def add_shared_activity(self, activity_id, color): + self._shared_activities[activity_id] = color + + def remove_shared_activity(self, activity_id): + del self._shared_activities[activity_id] + + def get_tabbing_activity(self): + """Returns the activity that is currently highlighted during tabbing""" + return self._tabbing_activity + + def set_tabbing_activity(self, activity): + """Sets the activity that is currently highlighted during tabbing""" + self._tabbing_activity = activity + self.emit('tabbing-activity-changed', self._tabbing_activity) + + def _set_active_activity(self, home_activity): + if self._active_activity == home_activity: + return + + if home_activity: + home_activity.set_active(True) + + if self._active_activity: + self._active_activity.set_active(False) + + self._active_activity = home_activity + self.emit('active-activity-changed', self._active_activity) + + def __iter__(self): + return iter(self._activities) + + def __len__(self): + return len(self._activities) + + def __getitem__(self, i): + return self._activities[i] + + def index(self, obj): + return self._activities.index(obj) + + def _window_opened_cb(self, screen, window): + """Handle the callback for the 'window opened' event. + + Most activities will register 2 windows during + their lifetime: the launcher window, and the 'main' + app window. + + When the main window appears, we send a signal to + the launcher window to close. + + Some activities (notably non-native apps) open several + windows during their lifetime, switching from one to + the next as the 'main' window. We use a stack to track + them. + + """ + if window.get_window_type() == wnck.WINDOW_NORMAL: + home_activity = None + + activity_id = wm.get_activity_id(window) + + service_name = wm.get_bundle_id(window) + if service_name: + registry = get_registry() + activity_info = registry.get_bundle(service_name) + else: + activity_info = None + + if activity_id: + home_activity = self.get_activity_by_id(activity_id) + + xid = window.get_xid() + gdk_window = gtk.gdk.window_foreign_new(xid) + gdk_window.set_decorations(0) + + window.maximize() + + if not home_activity: + logging.debug('first window registered for %s', activity_id) + color = self._shared_activities.get(activity_id, None) + home_activity = Activity(activity_info, activity_id, + color, window) + self._add_activity(home_activity) + else: + logging.debug('window registered for %s', activity_id) + home_activity.add_window(window) + + if wm.get_sugar_window_type(window) != 'launcher' \ + and home_activity.get_launch_status() == Activity.LAUNCHING: + self.emit('launch-completed', home_activity) + startup_time = time.time() - home_activity.get_launch_time() + logging.debug('%s launched in %f seconds.', + activity_id, startup_time) + + if self._active_activity is None: + self._set_active_activity(home_activity) + + def _window_closed_cb(self, screen, window): + if window.get_window_type() == wnck.WINDOW_NORMAL: + xid = window.get_xid() + activity = self._get_activity_by_xid(xid) + if activity is not None: + activity.remove_window_by_xid(xid) + if activity.get_window() is None: + logging.debug('last window gone - remove activity %s', + activity) + self._remove_activity(activity) + + def _get_activity_by_xid(self, xid): + for home_activity in self._activities: + if home_activity.has_xid(xid): + return home_activity + return None + + def get_activity_by_id(self, activity_id): + for home_activity in self._activities: + if home_activity.get_activity_id() == activity_id: + return home_activity + return None + + def _active_window_changed_cb(self, screen, previous_window=None): + window = screen.get_active_window() + if window is None: + return + + if window.get_window_type() != wnck.WINDOW_DIALOG: + while window.get_transient() is not None: + window = window.get_transient() + + act = self._get_activity_by_xid(window.get_xid()) + if act is not None: + self._set_active_activity(act) + + self._update_zoom_level(window) + + def _add_activity(self, home_activity): + self._activities.append(home_activity) + self.emit('activity-added', home_activity) + + def _remove_activity(self, home_activity): + if home_activity == self._active_activity: + windows = wnck.screen_get_default().get_windows_stacked() + windows.reverse() + for window in windows: + new_activity = self._get_activity_by_xid(window.get_xid()) + if new_activity is not None: + self._set_active_activity(new_activity) + break + else: + logging.error('No activities are running') + self._set_active_activity(None) + + self.emit('activity-removed', home_activity) + self._activities.remove(home_activity) + + def notify_launch(self, activity_id, service_name): + registry = get_registry() + activity_info = registry.get_bundle(service_name) + if not activity_info: + raise ValueError("Activity service name '%s'" \ + " was not found in the bundle registry." + % service_name) + color = self._shared_activities.get(activity_id, None) + home_activity = Activity(activity_info, activity_id, color) + self._add_activity(home_activity) + + self._set_active_activity(home_activity) + + self.emit('launch-started', home_activity) + + # FIXME: better learn about finishing processes by receiving a signal. + # Now just check whether an activity has a window after ~90sec + gobject.timeout_add_seconds(90, self._check_activity_launched, + activity_id) + + def notify_launch_failed(self, activity_id): + home_activity = self.get_activity_by_id(activity_id) + if home_activity: + logging.debug('Activity %s (%s) launch failed', activity_id, + home_activity.get_type()) + if self.get_launcher(activity_id) is not None: + self.emit('launch-failed', home_activity) + else: + # activity sent failure notification after closing launcher + self._remove_activity(home_activity) + else: + logging.error('Model for activity id %s does not exist.', + activity_id) + + def _check_activity_launched(self, activity_id): + home_activity = self.get_activity_by_id(activity_id) + + if not home_activity: + logging.debug('Activity %s has been closed already.', activity_id) + return False + + if self.get_launcher(activity_id) is not None: + logging.debug('Activity %s still launching, assuming it failed.', + activity_id) + self.notify_launch_failed(activity_id) + return False + + +def get_model(): + global _model + if _model is None: + _model = ShellModel() + return _model diff --git a/src/jarabe/model/sound.py b/src/jarabe/model/sound.py new file mode 100644 index 0000000..9e1e748 --- /dev/null +++ b/src/jarabe/model/sound.py @@ -0,0 +1,65 @@ +# Copyright (C) 2006-2008 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gconf + +from sugar import env +from sugar import _sugarext +from sugar import dispatch + + +VOLUME_STEP = 10 + +muted_changed = dispatch.Signal() +volume_changed = dispatch.Signal() + +_volume = _sugarext.VolumeAlsa() + + +def get_muted(): + return _volume.get_mute() + + +def get_volume(): + return _volume.get_volume() + + +def set_volume(new_volume): + old_volume = _volume.get_volume() + _volume.set_volume(new_volume) + + volume_changed.send(None) + save() + + +def set_muted(new_state): + old_state = _volume.get_mute() + _volume.set_mute(new_state) + + muted_changed.send(None) + save() + + +def save(): + if env.is_emulator() is False: + client = gconf.client_get_default() + client.set_int('/desktop/sugar/sound/volume', get_volume()) + + +def restore(): + if env.is_emulator() is False: + client = gconf.client_get_default() + set_volume(client.get_int('/desktop/sugar/sound/volume')) diff --git a/src/jarabe/model/speech.py b/src/jarabe/model/speech.py new file mode 100644 index 0000000..1cb0ad4 --- /dev/null +++ b/src/jarabe/model/speech.py @@ -0,0 +1,232 @@ +# Copyright (C) 2011 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +import gconf +import gst +import gtk +import gobject + + +DEFAULT_PITCH = 0 + + +DEFAULT_RATE = 0 + +_speech_manager = None + + +class SpeechManager(gobject.GObject): + + __gtype_name__ = 'SpeechManager' + + __gsignals__ = { + 'play': (gobject.SIGNAL_RUN_FIRST, None, []), + 'pause': (gobject.SIGNAL_RUN_FIRST, None, []), + 'stop': (gobject.SIGNAL_RUN_FIRST, None, []) + } + + MIN_PITCH = -100 + MAX_PITCH = 100 + + MIN_RATE = -100 + MAX_RATE = 100 + + def __init__(self, **kwargs): + gobject.GObject.__init__(self, **kwargs) + self._player = _GstSpeechPlayer() + self._player.connect('play', self._update_state, 'play') + self._player.connect('stop', self._update_state, 'stop') + self._player.connect('pause', self._update_state, 'pause') + self._voice_name = self._player.get_default_voice() + self._pitch = DEFAULT_PITCH + self._rate = DEFAULT_RATE + self._is_playing = False + self._is_paused = False + self.restore() + + def _update_state(self, player, signal): + self._is_playing = (signal == 'play') + self._is_paused = (signal == 'pause') + self.emit(signal) + + def get_is_playing(self): + return self._is_playing + + is_playing = gobject.property(type=bool, getter=get_is_playing, + setter=None, default=False) + + def get_is_paused(self): + return self._is_paused + + is_paused = gobject.property(type=bool, getter=get_is_paused, + setter=None, default=False) + + def get_pitch(self): + return self._pitch + + def get_rate(self): + return self._rate + + def set_pitch(self, pitch): + self._pitch = pitch + self.save() + + def set_rate(self, rate): + self._rate = rate + self.save() + + def say_text(self, text): + if text: + self._player.speak(self._pitch, self._rate, self._voice_name, text) + + def say_selected_text(self): + clipboard = gtk.clipboard_get(selection='PRIMARY') + clipboard.request_text(self.__primary_selection_cb) + + def pause(self): + self._player.pause_sound_device() + + def restart(self): + self._player.restart_sound_device() + + def stop(self): + self._player.stop_sound_device() + + def __primary_selection_cb(self, clipboard, text, user_data): + self.say_text(text) + + def save(self): + client = gconf.client_get_default() + client.set_int('/desktop/sugar/speech/pitch', self._pitch) + client.set_int('/desktop/sugar/speech/rate', self._rate) + logging.debug('saving speech configuration pitch %s rate %s', + self._pitch, self._rate) + + def restore(self): + client = gconf.client_get_default() + self._pitch = client.get_int('/desktop/sugar/speech/pitch') + self._rate = client.get_int('/desktop/sugar/speech/rate') + logging.debug('loading speech configuration pitch %s rate %s', + self._pitch, self._rate) + + +class _GstSpeechPlayer(gobject.GObject): + + __gsignals__ = { + 'play': (gobject.SIGNAL_RUN_FIRST, None, []), + 'pause': (gobject.SIGNAL_RUN_FIRST, None, []), + 'stop': (gobject.SIGNAL_RUN_FIRST, None, []) + } + + def __init__(self): + gobject.GObject.__init__(self) + self._pipeline = None + + def restart_sound_device(self): + if self._pipeline is None: + logging.debug('Trying to restart not initialized sound device') + return + + self._pipeline.set_state(gst.STATE_PLAYING) + self.emit('play') + + def pause_sound_device(self): + if self._pipeline is None: + return + + self._pipeline.set_state(gst.STATE_PAUSED) + self.emit('pause') + + def stop_sound_device(self): + if self._pipeline is None: + return + + self._pipeline.set_state(gst.STATE_NULL) + self.emit('stop') + + def make_pipeline(self, command): + if self._pipeline is not None: + self.stop_sound_device() + del self._pipeline + + self._pipeline = gst.parse_launch(command) + + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self.__pipe_message_cb) + + def __pipe_message_cb(self, bus, message): + if message.type == gst.MESSAGE_EOS: + self._pipeline.set_state(gst.STATE_NULL) + self.emit('stop') + elif message.type == gst.MESSAGE_ERROR: + self._pipeline.set_state(gst.STATE_NULL) + self.emit('stop') + + def speak(self, pitch, rate, voice_name, text): + # TODO workaround for http://bugs.sugarlabs.org/ticket/1801 + if not [i for i in text if i.isalnum()]: + return + + self.make_pipeline('espeak name=espeak ! autoaudiosink') + src = self._pipeline.get_by_name('espeak') + + src.props.text = text + src.props.pitch = pitch + src.props.rate = rate + src.props.voice = voice_name + src.props.track = 2 # track for marks + + self.restart_sound_device() + + def get_all_voices(self): + all_voices = {} + for voice in gst.element_factory_make('espeak').props.voices: + name, language, dialect = voice + if dialect != 'none': + all_voices[language + '_' + dialect] = name + else: + all_voices[language] = name + return all_voices + + def get_default_voice(self): + """Try to figure out the default voice, from the current locale ($LANG) + Fall back to espeak's voice called Default.""" + voices = self.get_all_voices() + + locale = os.environ.get('LANG', '') + language_location = locale.split('.', 1)[0].lower() + language = language_location.split('_')[0] + # if the language is es but not es_es default to es_la (latin voice) + if language == 'es' and language_location != 'es_es': + language_location = 'es_la' + + best = voices.get(language_location) or voices.get(language) \ + or 'default' + logging.debug('Best voice for LANG %s seems to be %s', + locale, best) + return best + + +def get_speech_manager(): + global _speech_manager + + if _speech_manager is None: + _speech_manager = SpeechManager() + return _speech_manager diff --git a/src/jarabe/model/telepathyclient.py b/src/jarabe/model/telepathyclient.py new file mode 100644 index 0000000..2604af6 --- /dev/null +++ b/src/jarabe/model/telepathyclient.py @@ -0,0 +1,126 @@ +# Copyright (C) 2010 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import CLIENT, \ + CHANNEL, \ + CHANNEL_TYPE_TEXT, \ + CLIENT_APPROVER, \ + CLIENT_HANDLER, \ + CLIENT_INTERFACE_REQUESTS +from telepathy.server import DBusProperties + +from telepathy.constants import CONNECTION_HANDLE_TYPE_ROOM +from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT + +from sugar import dispatch + + +SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +_instance = None + + +class TelepathyClient(dbus.service.Object, DBusProperties): + def __init__(self): + self._interfaces = set([CLIENT, CLIENT_HANDLER, + CLIENT_INTERFACE_REQUESTS, PROPERTIES_IFACE, + CLIENT_APPROVER]) + + bus = dbus.Bus() + bus_name = dbus.service.BusName(SUGAR_CLIENT_SERVICE, bus=bus) + + dbus.service.Object.__init__(self, bus_name, SUGAR_CLIENT_PATH) + DBusProperties.__init__(self) + + self._implement_property_get(CLIENT, { + 'Interfaces': lambda: list(self._interfaces), + }) + self._implement_property_get(CLIENT_HANDLER, { + 'HandlerChannelFilter': self.__get_filters_handler_cb, + }) + self._implement_property_get(CLIENT_APPROVER, { + 'ApproverChannelFilter': self.__get_filters_approver_cb, + }) + + self.got_channel = dispatch.Signal() + self.got_dispatch_operation = dispatch.Signal() + + def __get_filters_handler_cb(self): + filter_dict = dbus.Dictionary({}, signature='sv') + return dbus.Array([filter_dict], signature='a{sv}') + + def __get_filters_approver_cb(self): + activity_invitation = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_TEXT, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_ROOM, + } + filter_dict = dbus.Dictionary(activity_invitation, signature='sv') + filters = dbus.Array([filter_dict], signature='a{sv}') + + text_invitation = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_TEXT, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, + } + filter_dict = dbus.Dictionary(text_invitation, signature='sv') + filters.append(filter_dict) + + logging.debug('__get_filters_approver_cb %r', filters) + + return filters + + @dbus.service.method(dbus_interface=CLIENT_HANDLER, + in_signature='ooa(oa{sv})aota{sv}', out_signature='') + def HandleChannels(self, account, connection, channels, requests_satisfied, + user_action_time, handler_info): + logging.debug('HandleChannels\n%r\n%r\n%r\n%r\n%r\n%r\n', account, + connection, channels, requests_satisfied, + user_action_time, handler_info) + for channel in channels: + self.got_channel.send(self, account=account, + connection=connection, channel=channel) + + @dbus.service.method(dbus_interface=CLIENT_INTERFACE_REQUESTS, + in_signature='oa{sv}', out_signature='') + def AddRequest(self, request, properties): + logging.debug('AddRequest\n%r\n%r', request, properties) + + @dbus.service.method(dbus_interface=CLIENT_APPROVER, + in_signature='a(oa{sv})oa{sv}', out_signature='', + async_callbacks=('success_cb', 'error_cb_')) + def AddDispatchOperation(self, channels, dispatch_operation_path, + properties, success_cb, error_cb_): + success_cb() + try: + logging.debug('AddDispatchOperation\n%r\n%r\n%r', channels, + dispatch_operation_path, properties) + + self.got_dispatch_operation.send(self, channels=channels, + dispatch_operation_path=dispatch_operation_path, + properties=properties) + except Exception, e: + logging.exception(e) + + +def get_instance(): + global _instance + if not _instance: + _instance = TelepathyClient() + return _instance diff --git a/src/jarabe/util/Makefile.am b/src/jarabe/util/Makefile.am new file mode 100644 index 0000000..8bda3d6 --- /dev/null +++ b/src/jarabe/util/Makefile.am @@ -0,0 +1,7 @@ +SUBDIRS = \ + telepathy + +sugardir = $(pythondir)/jarabe/util +sugar_PYTHON = \ + __init__.py \ + emulator.py diff --git a/src/jarabe/util/Makefile.in b/src/jarabe/util/Makefile.in new file mode 100644 index 0000000..30f46ac --- /dev/null +++ b/src/jarabe/util/Makefile.in @@ -0,0 +1,646 @@ +# 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/util +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 = +RECURSIVE_TARGETS = all-recursive check-recursive dvi-recursive \ + html-recursive info-recursive install-data-recursive \ + install-dvi-recursive install-exec-recursive \ + install-html-recursive install-info-recursive \ + install-pdf-recursive install-ps-recursive install-recursive \ + installcheck-recursive installdirs-recursive pdf-recursive \ + ps-recursive uninstall-recursive +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 +RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \ + distclean-recursive maintainer-clean-recursive +AM_RECURSIVE_TARGETS = $(RECURSIVE_TARGETS:-recursive=) \ + $(RECURSIVE_CLEAN_TARGETS:-recursive=) tags TAGS ctags CTAGS \ + distdir +ETAGS = etags +CTAGS = ctags +DIST_SUBDIRS = $(SUBDIRS) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +am__relativize = \ + dir0=`pwd`; \ + sed_first='s,^\([^/]*\)/.*$$,\1,'; \ + sed_rest='s,^[^/]*/*,,'; \ + sed_last='s,^.*/\([^/]*\)$$,\1,'; \ + sed_butlast='s,/*[^/]*$$,,'; \ + while test -n "$$dir1"; do \ + first=`echo "$$dir1" | sed -e "$$sed_first"`; \ + if test "$$first" != "."; then \ + if test "$$first" = ".."; then \ + dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \ + dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \ + else \ + first2=`echo "$$dir2" | sed -e "$$sed_first"`; \ + if test "$$first2" = "$$first"; then \ + dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \ + else \ + dir2="../$$dir2"; \ + fi; \ + dir0="$$dir0"/"$$first"; \ + fi; \ + fi; \ + dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \ + done; \ + reldir="$$dir2" +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@ +SUBDIRS = \ + telepathy + +sugardir = $(pythondir)/jarabe/util +sugar_PYTHON = \ + __init__.py \ + emulator.py + +all: all-recursive + +.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/util/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/util/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 + +# This directory's subdirectories are mostly independent; you can cd +# into them and run `make' without going through this Makefile. +# To change the values of `make' variables: instead of editing Makefiles, +# (1) if the variable is set in `config.status', edit `config.status' +# (which will cause the Makefiles to be regenerated when you run `make'); +# (2) otherwise, pass the desired values on the `make' command line. +$(RECURSIVE_TARGETS): + @fail= failcom='exit 1'; \ + for f in x $$MAKEFLAGS; do \ + case $$f in \ + *=* | --[!k]*);; \ + *k*) failcom='fail=yes';; \ + esac; \ + done; \ + dot_seen=no; \ + target=`echo $@ | sed s/-recursive//`; \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + echo "Making $$target in $$subdir"; \ + if test "$$subdir" = "."; then \ + dot_seen=yes; \ + local_target="$$target-am"; \ + else \ + local_target="$$target"; \ + fi; \ + ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ + || eval $$failcom; \ + done; \ + if test "$$dot_seen" = "no"; then \ + $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \ + fi; test -z "$$fail" + +$(RECURSIVE_CLEAN_TARGETS): + @fail= failcom='exit 1'; \ + for f in x $$MAKEFLAGS; do \ + case $$f in \ + *=* | --[!k]*);; \ + *k*) failcom='fail=yes';; \ + esac; \ + done; \ + dot_seen=no; \ + case "$@" in \ + distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \ + *) list='$(SUBDIRS)' ;; \ + esac; \ + rev=''; for subdir in $$list; do \ + if test "$$subdir" = "."; then :; else \ + rev="$$subdir $$rev"; \ + fi; \ + done; \ + rev="$$rev ."; \ + target=`echo $@ | sed s/-recursive//`; \ + for subdir in $$rev; do \ + echo "Making $$target in $$subdir"; \ + if test "$$subdir" = "."; then \ + local_target="$$target-am"; \ + else \ + local_target="$$target"; \ + fi; \ + ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ + || eval $$failcom; \ + done && test -z "$$fail" +tags-recursive: + list='$(SUBDIRS)'; for subdir in $$list; do \ + test "$$subdir" = . || ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) tags); \ + done +ctags-recursive: + list='$(SUBDIRS)'; for subdir in $$list; do \ + test "$$subdir" = . || ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) ctags); \ + done + +ID: $(HEADERS) $(SOURCES) $(LISP) $(TAGS_FILES) + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + mkid -fID $$unique +tags: TAGS + +TAGS: tags-recursive $(HEADERS) $(SOURCES) $(TAGS_DEPENDENCIES) \ + $(TAGS_FILES) $(LISP) + set x; \ + here=`pwd`; \ + if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \ + include_option=--etags-include; \ + empty_fix=.; \ + else \ + include_option=--include; \ + empty_fix=; \ + fi; \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + test ! -f $$subdir/TAGS || \ + set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \ + fi; \ + done; \ + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: CTAGS +CTAGS: ctags-recursive $(HEADERS) $(SOURCES) $(TAGS_DEPENDENCIES) \ + $(TAGS_FILES) $(LISP) + list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | \ + $(AWK) '{ files[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in files) print i; }; }'`; \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +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 + @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + test -d "$(distdir)/$$subdir" \ + || $(MKDIR_P) "$(distdir)/$$subdir" \ + || exit 1; \ + fi; \ + done + @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + dir1=$$subdir; dir2="$(distdir)/$$subdir"; \ + $(am__relativize); \ + new_distdir=$$reldir; \ + dir1=$$subdir; dir2="$(top_distdir)"; \ + $(am__relativize); \ + new_top_distdir=$$reldir; \ + echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \ + echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \ + ($(am__cd) $$subdir && \ + $(MAKE) $(AM_MAKEFLAGS) \ + top_distdir="$$new_top_distdir" \ + distdir="$$new_distdir" \ + am__remove_distdir=: \ + am__skip_length_check=: \ + am__skip_mode_fix=: \ + distdir) \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-recursive +all-am: Makefile +installdirs: installdirs-recursive +installdirs-am: + for dir in "$(DESTDIR)$(sugardir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-recursive +install-exec: install-exec-recursive +install-data: install-data-recursive +uninstall: uninstall-recursive + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-recursive +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-recursive + +clean-am: clean-generic mostlyclean-am + +distclean: distclean-recursive + -rm -f Makefile +distclean-am: clean-am distclean-generic distclean-tags + +dvi: dvi-recursive + +dvi-am: + +html: html-recursive + +html-am: + +info: info-recursive + +info-am: + +install-data-am: install-sugarPYTHON + +install-dvi: install-dvi-recursive + +install-dvi-am: + +install-exec-am: + +install-html: install-html-recursive + +install-html-am: + +install-info: install-info-recursive + +install-info-am: + +install-man: + +install-pdf: install-pdf-recursive + +install-pdf-am: + +install-ps: install-ps-recursive + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-recursive + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-recursive + +mostlyclean-am: mostlyclean-generic + +pdf: pdf-recursive + +pdf-am: + +ps: ps-recursive + +ps-am: + +uninstall-am: uninstall-sugarPYTHON + +.MAKE: $(RECURSIVE_CLEAN_TARGETS) $(RECURSIVE_TARGETS) ctags-recursive \ + install-am install-strip tags-recursive + +.PHONY: $(RECURSIVE_CLEAN_TARGETS) $(RECURSIVE_TARGETS) CTAGS GTAGS \ + all all-am check check-am clean clean-generic ctags \ + ctags-recursive distclean distclean-generic distclean-tags \ + 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 installdirs-am maintainer-clean \ + maintainer-clean-generic mostlyclean mostlyclean-generic pdf \ + pdf-am ps ps-am tags tags-recursive 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/util/__init__.py b/src/jarabe/util/__init__.py new file mode 100644 index 0000000..9c80ecb --- /dev/null +++ b/src/jarabe/util/__init__.py @@ -0,0 +1,18 @@ +"""OLPC Sugar Jarabe utility modules +""" + +# Copyright (C) 2008, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/jarabe/util/emulator.py b/src/jarabe/util/emulator.py new file mode 100644 index 0000000..fda1b59 --- /dev/null +++ b/src/jarabe/util/emulator.py @@ -0,0 +1,185 @@ +# Copyright (C) 2006-2008, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import signal +import subprocess +import sys +import time +from optparse import OptionParser +from gettext import gettext as _ + +import gtk +import gobject + +from sugar import env + + +ERROR_NO_DISPLAY = 30 +ERROR_NO_SERVER = 31 +default_dimensions = (800, 600) + + +def _run_xephyr(display, dpi, dimensions, fullscreen): + cmd = ['Xephyr'] + cmd.append(':%d' % display) + cmd.append('-ac') + cmd += ['-title', _('Sugar in a window')] + + screen_size = (gtk.gdk.screen_width(), gtk.gdk.screen_height()) + + if (not dimensions) and (fullscreen is None) and \ + (screen_size <= default_dimensions): + # no forced settings, screen too small => fit screen + fullscreen = True + elif not dimensions: + # screen is big enough or user has en/disabled fullscreen manually + # => use default size (will get ignored for fullscreen) + dimensions = '%dx%d' % default_dimensions + + if not dpi: + dpi = gtk.settings_get_default().get_property('gtk-xft-dpi') / 1024 + + if fullscreen: + cmd.append('-fullscreen') + + if dimensions: + cmd.append('-screen') + cmd.append(dimensions) + + if dpi: + cmd.append('-dpi') + cmd.append('%d' % dpi) + + cmd.append('-noreset') + + try: + pipe = subprocess.Popen(cmd) + + except OSError, exc: + sys.stderr.write('Error executing server: %s\n' % (exc, )) + return None + + return pipe + + +def _check_server(display): + result = subprocess.call(['xdpyinfo', '-display', ':%d' % display], + stdout=open(os.devnull, 'w'), + stderr=open(os.devnull, 'w')) + return result == 0 + + +def _kill_pipe(pipe): + """Terminate and wait for child process.""" + try: + os.kill(pipe.pid, signal.SIGTERM) + except OSError: + pass + + pipe.wait() + + +def _start_xephyr(dpi, dimensions, fullscreen): + for display in range(30, 40): + if not _check_server(display): + pipe = _run_xephyr(display, dpi, dimensions, fullscreen) + if pipe is None: + return None, None + + for i_ in range(10): + if _check_server(display): + return pipe, display + + time.sleep(0.1) + + _kill_pipe(pipe) + + return None, None + + +def _start_window_manager(): + cmd = ['metacity'] + + cmd.extend(['--no-force-fullscreen']) + + gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) + + +def _setup_env(display, scaling, emulator_pid): + os.environ['SUGAR_EMULATOR'] = 'yes' + os.environ['GABBLE_LOGFILE'] = os.path.join( + env.get_profile_path(), 'logs', 'telepathy-gabble.log') + os.environ['SALUT_LOGFILE'] = os.path.join( + env.get_profile_path(), 'logs', 'telepathy-salut.log') + os.environ['MC_LOGFILE'] = os.path.join( + env.get_profile_path(), 'logs', 'mission-control.log') + os.environ['STREAM_ENGINE_LOGFILE'] = os.path.join( + env.get_profile_path(), 'logs', 'telepathy-stream-engine.log') + os.environ['DISPLAY'] = ':%d' % (display) + os.environ['SUGAR_EMULATOR_PID'] = emulator_pid + os.environ['MC_ACCOUNT_DIR'] = os.path.join( + env.get_profile_path(), 'accounts') + + if scaling: + os.environ['SUGAR_SCALING'] = scaling + + +def main(): + """Script-level operations""" + + parser = OptionParser() + parser.add_option('-d', '--dpi', dest='dpi', type='int', + help='Emulator dpi') + parser.add_option('-s', '--scaling', dest='scaling', + help='Sugar scaling in %') + parser.add_option('-i', '--dimensions', dest='dimensions', + help='Emulator dimensions (ex. 1200x900)') + parser.add_option('-f', '--fullscreen', dest='fullscreen', + action='store_true', default=None, + help='Run emulator in fullscreen mode') + parser.add_option('-F', '--no-fullscreen', dest='fullscreen', + action='store_false', + help='Do not run emulator in fullscreen mode') + (options, args) = parser.parse_args() + + if not os.environ.get('DISPLAY'): + sys.stderr.write('DISPLAY not set, cannot connect to host X server.\n') + return ERROR_NO_DISPLAY + + server, display = _start_xephyr(options.dpi, options.dimensions, + options.fullscreen) + if server is None: + sys.stderr.write('Failed to start server. Please check output above' + ' for any error message.\n') + return ERROR_NO_SERVER + + _setup_env(display, options.scaling, str(server.pid)) + + command = ['dbus-launch', '--exit-with-session'] + + if not args: + command.append('sugar') + else: + _start_window_manager() + + if args[0].endswith('.py'): + command.append('python') + + command.append(args[0]) + + subprocess.call(command) + _kill_pipe(server) diff --git a/src/jarabe/util/telepathy/Makefile.am b/src/jarabe/util/telepathy/Makefile.am new file mode 100644 index 0000000..d40349d --- /dev/null +++ b/src/jarabe/util/telepathy/Makefile.am @@ -0,0 +1,4 @@ +sugardir = $(pythondir)/jarabe/util/telepathy +sugar_PYTHON = \ + __init__.py \ + connection_watcher.py diff --git a/src/jarabe/util/telepathy/Makefile.in b/src/jarabe/util/telepathy/Makefile.in new file mode 100644 index 0000000..62f6c00 --- /dev/null +++ b/src/jarabe/util/telepathy/Makefile.in @@ -0,0 +1,441 @@ +# 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/util/telepathy +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/util/telepathy +sugar_PYTHON = \ + __init__.py \ + connection_watcher.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/util/telepathy/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/util/telepathy/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/util/telepathy/__init__.py b/src/jarabe/util/telepathy/__init__.py new file mode 100644 index 0000000..eee4abb --- /dev/null +++ b/src/jarabe/util/telepathy/__init__.py @@ -0,0 +1,18 @@ +"""OLPC Sugar Jarabe utility telepathy modules +""" + +# Copyright (C) 2008, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/jarabe/util/telepathy/connection_watcher.py b/src/jarabe/util/telepathy/connection_watcher.py new file mode 100644 index 0000000..96af1cf --- /dev/null +++ b/src/jarabe/util/telepathy/connection_watcher.py @@ -0,0 +1,122 @@ +# This should eventually land in telepathy-python, so has the same license: +# Copyright (C) 2008 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# FIXME: this sould go upstream, in telepathy-python + +import logging + +import dbus +import dbus.mainloop.glib +import gobject + +from telepathy.client import Connection +from telepathy.interfaces import CONN_INTERFACE +from telepathy.constants import CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED + + +_instance = None + + +class ConnectionWatcher(gobject.GObject): + __gsignals__ = { + 'connection-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'connection-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, bus=None): + gobject.GObject.__init__(self) + + if bus is None: + self.bus = dbus.Bus() + else: + self.bus = bus + + # D-Bus path -> Connection + self._connections = {} + + self.bus.add_signal_receiver(self._status_changed_cb, + dbus_interface=CONN_INTERFACE, signal_name='StatusChanged', + path_keyword='path') + + for conn in Connection.get_connections(bus): + conn.call_when_ready(self._conn_ready_cb) + + def _status_changed_cb(self, *args, **kwargs): + path = kwargs['path'] + if not path.startswith('/org/freedesktop/Telepathy/Connection/'): + return + + status, reason_ = args + service_name = path.replace('/', '.')[1:] + + if status == CONNECTION_STATUS_CONNECTED: + self._add_connection(service_name, path) + elif status == CONNECTION_STATUS_DISCONNECTED: + self._remove_connection(service_name, path) + + def _conn_ready_cb(self, conn): + if conn.object_path in self._connections: + return + + self._connections[conn.object_path] = conn + self.emit('connection-added', conn) + + def _add_connection(self, service_name, path): + if path in self._connections: + return + + try: + Connection(service_name, path, ready_handler=self._conn_ready_cb) + except dbus.exceptions.DBusException: + logging.debug('%s is propably already gone.', service_name) + + def _remove_connection(self, service_name, path): + conn = self._connections.pop(path, None) + if conn is None: + return + + self.emit('connection-removed', conn) + + def get_connections(self): + return self._connections.values() + + +def get_instance(): + global _instance + if _instance is None: + _instance = ConnectionWatcher() + return _instance + + +if __name__ == '__main__': + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + def connection_added_cb(conn_watcher, conn): + print 'new connection', conn.service_name + + def connection_removed_cb(conn_watcher, conn): + print 'removed connection', conn.service_name + + watcher = ConnectionWatcher() + watcher.connect('connection-added', connection_added_cb) + watcher.connect('connection-removed', connection_removed_cb) + + loop = gobject.MainLoop() + loop.run() diff --git a/src/jarabe/view/Makefile.am b/src/jarabe/view/Makefile.am new file mode 100644 index 0000000..630f184 --- /dev/null +++ b/src/jarabe/view/Makefile.am @@ -0,0 +1,13 @@ +sugardir = $(pythondir)/jarabe/view +sugar_PYTHON = \ + __init__.py \ + buddyicon.py \ + buddymenu.py \ + customizebundle.py \ + keyhandler.py \ + launcher.py \ + palettes.py \ + pulsingicon.py \ + service.py \ + tabbinghandler.py \ + viewsource.py diff --git a/src/jarabe/view/Makefile.in b/src/jarabe/view/Makefile.in new file mode 100644 index 0000000..2ef1572 --- /dev/null +++ b/src/jarabe/view/Makefile.in @@ -0,0 +1,450 @@ +# 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/view +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/view +sugar_PYTHON = \ + __init__.py \ + buddyicon.py \ + buddymenu.py \ + customizebundle.py \ + keyhandler.py \ + launcher.py \ + palettes.py \ + pulsingicon.py \ + service.py \ + tabbinghandler.py \ + viewsource.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/view/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/view/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/view/__init__.py b/src/jarabe/view/__init__.py new file mode 100644 index 0000000..85f6a24 --- /dev/null +++ b/src/jarabe/view/__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/view/buddyicon.py b/src/jarabe/view/buddyicon.py new file mode 100644 index 0000000..e0e8b3f --- /dev/null +++ b/src/jarabe/view/buddyicon.py @@ -0,0 +1,65 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style + +from jarabe.view.buddymenu import BuddyMenu + +_FILTERED_ALPHA = 0.33 + + +class BuddyIcon(CanvasIcon): + def __init__(self, buddy, size=style.STANDARD_ICON_SIZE): + CanvasIcon.__init__(self, icon_name='computer-xo', size=size) + + self._filtered = False + self._buddy = buddy + self._buddy.connect('notify::present', self.__buddy_notify_present_cb) + self._buddy.connect('notify::color', self.__buddy_notify_color_cb) + + self.palette_invoker.cache_palette = False + + self._update_color() + + def create_palette(self): + return BuddyMenu(self._buddy) + + def __buddy_notify_present_cb(self, buddy, pspec): + # Update the icon's color when the buddy comes and goes + self._update_color() + + def __buddy_notify_color_cb(self, buddy, pspec): + self._update_color() + + def _update_color(self): + # keep the icon in the palette in sync with the view + palette = self.get_palette() + self.props.xo_color = self._buddy.get_color() + if self._filtered: + self.alpha = _FILTERED_ALPHA + if palette is not None: + palette.props.icon.props.stroke_color = self.props.stroke_color + palette.props.icon.props.fill_color = self.props.fill_color + else: + self.alpha = 1.0 + if palette is not None: + palette.props.icon.props.xo_color = self._buddy.get_color() + + def set_filter(self, query): + self._filtered = (self._buddy.get_nick().lower().find(query) == -1) \ + and not self._buddy.is_owner() + self._update_color() diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py new file mode 100644 index 0000000..de5a772 --- /dev/null +++ b/src/jarabe/view/buddymenu.py @@ -0,0 +1,180 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ + +import gtk +import gconf +import glib +import dbus + +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon + +from jarabe.model import shell +from jarabe.model import friends +from jarabe.model.session import get_session_manager +from jarabe.controlpanel.gui import ControlPanel +import jarabe.desktop.homewindow + + +class BuddyMenu(Palette): + def __init__(self, buddy): + self._buddy = buddy + + buddy_icon = Icon(icon_name='computer-xo', + xo_color=buddy.get_color(), + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + nick = buddy.get_nick() + Palette.__init__(self, None, + primary_text=glib.markup_escape_text(nick), + icon=buddy_icon) + self._invite_menu = None + self._active_activity_changed_hid = None + self.connect('destroy', self.__destroy_cb) + + self._buddy.connect('notify::nick', self.__buddy_notify_nick_cb) + + if buddy.is_owner(): + self._add_my_items() + else: + self._add_buddy_items() + + def __destroy_cb(self, menu): + if self._active_activity_changed_hid is not None: + home_model = shell.get_model() + home_model.disconnect(self._active_activity_changed_hid) + self._buddy.disconnect_by_func(self.__buddy_notify_nick_cb) + + def _add_buddy_items(self): + if friends.get_model().has_buddy(self._buddy): + menu_item = MenuItem(_('Remove friend'), 'list-remove') + menu_item.connect('activate', self._remove_friend_cb) + else: + menu_item = MenuItem(_('Make friend'), 'list-add') + menu_item.connect('activate', self._make_friend_cb) + + self.menu.append(menu_item) + menu_item.show() + + self._invite_menu = MenuItem('') + self._invite_menu.connect('activate', self._invite_friend_cb) + self.menu.append(self._invite_menu) + + home_model = shell.get_model() + self._active_activity_changed_hid = home_model.connect( + 'active-activity-changed', self._cur_activity_changed_cb) + activity = home_model.get_active_activity() + self._update_invite_menu(activity) + + def _add_my_items(self): + item = MenuItem(_('Shutdown'), 'system-shutdown') + item.connect('activate', self.__shutdown_activate_cb) + self.menu.append(item) + item.show() + + client = gconf.client_get_default() + + if client.get_bool('/desktop/sugar/show_restart'): + item = MenuItem(_('Restart'), 'system-restart') + item.connect('activate', self.__reboot_activate_cb) + self.menu.append(item) + item.show() + + if client.get_bool('/desktop/sugar/show_logout'): + item = MenuItem(_('Logout'), 'system-logout') + item.connect('activate', self.__logout_activate_cb) + self.menu.append(item) + item.show() + + item = MenuItem(_('My Settings'), 'preferences-system') + item.connect('activate', self.__controlpanel_activate_cb) + self.menu.append(item) + item.show() + + def _quit(self, action): + home_window = jarabe.desktop.homewindow.get_instance() + home_window.busy_during_delayed_action(action) + + def __logout_activate_cb(self, menu_item): + self._quit(get_session_manager().logout) + + def __reboot_activate_cb(self, menu_item): + self._quit(get_session_manager().reboot) + + def __shutdown_activate_cb(self, menu_item): + self._quit(get_session_manager().shutdown) + + def __controlpanel_activate_cb(self, menu_item): + panel = ControlPanel() + panel.set_transient_for(self.get_toplevel()) + panel.show() + + def _update_invite_menu(self, activity): + buddy_activity = self._buddy.props.current_activity + if buddy_activity is not None: + buddy_activity_id = buddy_activity.activity_id + else: + buddy_activity_id = None + + if activity is None or activity.is_journal() or \ + activity.get_activity_id() == buddy_activity_id: + self._invite_menu.hide() + else: + title = activity.get_title() + label = self._invite_menu.get_children()[0] + label.set_text(_('Invite to %s') % title) + + icon = Icon(file=activity.get_icon_path()) + icon.props.xo_color = activity.get_icon_color() + self._invite_menu.set_image(icon) + icon.show() + + self._invite_menu.show() + + def _cur_activity_changed_cb(self, home_model, activity_model): + self._update_invite_menu(activity_model) + + def __buddy_notify_nick_cb(self, buddy, pspec): + self.set_primary_text(glib.markup_escape_text(buddy.props.nick)) + + def _make_friend_cb(self, menuitem): + friends.get_model().make_friend(self._buddy) + + def _remove_friend_cb(self, menuitem): + friends.get_model().remove(self._buddy) + + def _invite_friend_cb(self, menuitem): + activity = shell.get_model().get_active_activity() + service = activity.get_service() + if service: + try: + service.InviteContact(self._buddy.props.account, + self._buddy.props.contact_id) + except dbus.DBusException, e: + expected_exceptions = [ + 'org.freedesktop.DBus.Error.UnknownMethod', + 'org.freedesktop.DBus.Python.NotImplementedError'] + if e.get_dbus_name() in expected_exceptions: + logging.warning('Trying deprecated Activity.Invite') + service.Invite(self._buddy.props.key) + else: + raise + else: + logging.error('Invite failed, activity service not ') diff --git a/src/jarabe/view/customizebundle.py b/src/jarabe/view/customizebundle.py new file mode 100644 index 0000000..42da1b3 --- /dev/null +++ b/src/jarabe/view/customizebundle.py @@ -0,0 +1,217 @@ +# Copyright (C) 2011 Walter Bender +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import glob +import hashlib + +import gtk + +from sugar import profile +from sugar.activity import bundlebuilder +from sugar.datastore import datastore +from sugar.env import get_user_activities_path + +import logging +_logger = logging.getLogger('ViewSource') + + +BADGE_SUBPATH = 'emblems/emblem-view-source.svg' +BADGE_TRANSFORM = ' \n' +ICON_TRANSFORM = ' \n' +XML_HEADER = ' \ +\n\ +\n]>\n' +SVG_START = '\n' +SVG_END = '\n' + + +def generate_unique_id(): + """Generate an id based on the user's nick name and their public key + (Based on schema used by IRC activity). + + """ + nick = profile.get_nick_name() + pubkey = profile.get_pubkey() + m = hashlib.sha1() + m.update(pubkey) + hexhash = m.hexdigest() + + nick_letters = "".join([x for x in nick if x.isalpha()]) + + if not nick_letters: + nick_letters = 'XO' + + return nick_letters + '_' + hexhash[:4] + + +def generate_bundle(nick, new_basename): + """Generate a new .xo bundle for the activity and copy it into the + Journal. + + """ + new_activity_name = _customize_activity_info( + nick, new_basename) + + user_activities_path = get_user_activities_path() + if os.path.exists(os.path.join(user_activities_path, new_basename, + 'dist')): + for path in glob.glob(os.path.join(user_activities_path, new_basename, + 'dist', '*')): + os.remove(path) + + config = bundlebuilder.Config(source_dir=os.path.join( + user_activities_path, new_basename), + dist_name='%s-1.xo' % (new_activity_name)) + bundlebuilder.cmd_dist_xo(config, None) + + dsobject = datastore.create() + dsobject.metadata['title'] = '%s-1.xo' % (new_activity_name) + dsobject.metadata['mime_type'] = 'application/vnd.olpc-sugar' + dsobject.set_file_path(os.path.join( + user_activities_path, new_basename, 'dist', + '%s-1.xo' % (new_activity_name))) + datastore.write(dsobject) + dsobject.destroy() + + +def _customize_activity_info(nick, new_basename): + """Modify bundle_id in new activity.info file: + (1) change the bundle_id to bundle_id_[NICKNAME]; + (2) change the activity_icon [NICKNAME]-activity-icon.svg; + (3) set activity_version to 1; + (4) modify the activity icon by applying a customize overlay. + + """ + new_activity_name = '' + user_activities_path = get_user_activities_path() + + info_old = open(os.path.join(user_activities_path, new_basename, + 'activity', 'activity.info'), 'r') + info_new = open(os.path.join(user_activities_path, new_basename, + 'activity', 'new_activity.info'), 'w') + + for line in info_old: + if line.find('=') < 0: + info_new.write(line) + continue + name, value = [token.strip() for token in line.split('=', 1)] + if name == 'bundle_id': + new_value = '%s_%s' % (value, nick) + elif name == 'activity_version': + new_value = '1' + elif name == 'icon': + new_value = value + icon_name = value + elif name == 'name': + new_value = '%s_copy_of_%s' % (nick, value) + new_activity_name = new_value + else: + info_new.write(line) + continue + + info_new.write('%s = %s\n' % (name, new_value)) + + info_old.close() + info_new.close() + + os.rename(os.path.join(user_activities_path, new_basename, + 'activity', 'new_activity.info'), + os.path.join(user_activities_path, new_basename, + 'activity', 'activity.info')) + + _create_custom_icon(new_basename, icon_name) + + return new_activity_name + + +def _create_custom_icon(new_basename, icon_name): + """Modify activity icon by overlaying a badge: + (1) Extract the payload from the badge icon; + (2) Add a transform to resize it and position it; + (3) Insert it into the activity icon. + + """ + user_activities_path = get_user_activities_path() + badge_path = None + for path in gtk.icon_theme_get_default().get_search_path(): + if os.path.exists(os.path.join(path, 'sugar', 'scalable', + BADGE_SUBPATH)): + badge_path = path + break + + if badge_path is None: + _logger.debug('%s not found', BADGE_SUBPATH) + return + + badge_fd = open(os.path.join(badge_path, 'sugar', 'scalable', + BADGE_SUBPATH), 'r') + badge_payload = _extract_svg_payload(badge_fd) + badge_fd.close() + + badge_svg = BADGE_TRANSFORM + badge_payload + '\n' + + icon_path = os.path.join(user_activities_path, new_basename, 'activity', + icon_name + '.svg') + icon_fd = open(icon_path, 'r') + icon_payload = _extract_svg_payload(icon_fd) + icon_fd.close() + + icon_svg = ICON_TRANSFORM + icon_payload + '\n' + + tmp_path = os.path.join(user_activities_path, new_basename, 'activity', + 'tmp.svg') + tmp_icon_fd = open(tmp_path, 'w') + tmp_icon_fd.write(XML_HEADER) + tmp_icon_fd.write(SVG_START) + tmp_icon_fd.write(icon_svg) + tmp_icon_fd.write(badge_svg) + tmp_icon_fd.write(SVG_END) + tmp_icon_fd.close() + + os.remove(icon_path) + os.rename(tmp_path, icon_path) + + +def _extract_svg_payload(fd): + """Returns everything between and """ + payload = '' + looking_for_start_svg_token = True + looking_for_close_token = True + looking_for_end_svg_token = True + for line in fd: + if looking_for_start_svg_token: + if line.find('') < 0: + continue + looking_for_close_token = False + line = line.split('>', 1)[1] + if looking_for_end_svg_token: + if line.find('') < 0: + payload += line + continue + payload += line.split('')[0] + break + return payload diff --git a/src/jarabe/view/keyhandler.py b/src/jarabe/view/keyhandler.py new file mode 100644 index 0000000..530da75 --- /dev/null +++ b/src/jarabe/view/keyhandler.py @@ -0,0 +1,216 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# Copyright (C) 2009 Simon Schampijer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +import dbus +import gtk + +from sugar._sugarext import KeyGrabber + +from jarabe.model import sound +from jarabe.model import shell +from jarabe.model import session +from jarabe.view.tabbinghandler import TabbingHandler +from jarabe.model.shell import ShellModel +from jarabe import config +from jarabe.journal import journalactivity + + +_VOLUME_STEP = sound.VOLUME_STEP +_VOLUME_MAX = 100 +_TABBING_MODIFIER = gtk.gdk.MOD1_MASK + + +_actions_table = { + 'F1': 'zoom_mesh', + 'F2': 'zoom_group', + 'F3': 'zoom_home', + 'F4': 'zoom_activity', + 'F5': 'open_search', + 'F6': 'frame', + 'XF86AudioMute': 'volume_mute', + 'F11': 'volume_down', + 'XF86AudioLowerVolume': 'volume_down', + 'F12': 'volume_up', + 'XF86AudioRaiseVolume': 'volume_up', + 'F11': 'volume_min', + 'F12': 'volume_max', + 'XF86MenuKB': 'frame', + 'Tab': 'next_window', + 'Tab': 'previous_window', + 'Escape': 'close_window', + 'XF86WebCam': 'open_search', +# the following are intended for emulator users + 'f': 'frame', + 'q': 'quit_emulator', + 'XF86Search': 'open_search', + 'o': 'open_search' +} + + +_instance = None + + +class KeyHandler(object): + def __init__(self, frame): + self._frame = frame + self._key_pressed = None + self._keycode_pressed = 0 + self._keystate_pressed = 0 + + self._key_grabber = KeyGrabber() + self._key_grabber.connect('key-pressed', + self._key_pressed_cb) + self._key_grabber.connect('key-released', + self._key_released_cb) + + self._tabbing_handler = TabbingHandler(self._frame, _TABBING_MODIFIER) + + for f in os.listdir(os.path.join(config.ext_path, 'globalkey')): + if f.endswith('.py') and not f.startswith('__'): + module_name = f[:-3] + try: + logging.debug('Loading module %r', module_name) + module = __import__('globalkey.' + module_name, globals(), + locals(), [module_name]) + for key in module.BOUND_KEYS: + if key in _actions_table: + raise ValueError('Key %r is already bound' % key) + _actions_table[key] = module + except Exception: + logging.exception('Exception while loading extension:') + + self._key_grabber.grab_keys(_actions_table.keys()) + + def _change_volume(self, step=None, value=None): + if step is not None: + volume = sound.get_volume() + step + elif value is not None: + volume = value + + volume = min(max(0, volume), _VOLUME_MAX) + + sound.set_volume(volume) + sound.set_muted(volume == 0) + + def handle_previous_window(self, event_time): + self._tabbing_handler.previous_activity(event_time) + + def handle_next_window(self, event_time): + self._tabbing_handler.next_activity(event_time) + + def handle_close_window(self, event_time): + active_activity = shell.get_model().get_active_activity() + if active_activity.is_journal(): + return + + active_activity.get_window().close() + + def handle_zoom_mesh(self, event_time): + shell.get_model().set_zoom_level(ShellModel.ZOOM_MESH, event_time) + + def handle_zoom_group(self, event_time): + shell.get_model().set_zoom_level(ShellModel.ZOOM_GROUP, event_time) + + def handle_zoom_home(self, event_time): + shell.get_model().set_zoom_level(ShellModel.ZOOM_HOME, event_time) + + def handle_zoom_activity(self, event_time): + shell.get_model().set_zoom_level(ShellModel.ZOOM_ACTIVITY, event_time) + + def handle_volume_max(self, event_time): + self._change_volume(value=_VOLUME_MAX) + + def handle_volume_min(self, event_time): + self._change_volume(value=0) + + def handle_volume_mute(self, event_time): + if sound.get_muted() is True: + sound.set_muted(False) + else: + sound.set_muted(True) + + def handle_volume_up(self, event_time): + self._change_volume(step=_VOLUME_STEP) + + def handle_volume_down(self, event_time): + self._change_volume(step=-_VOLUME_STEP) + + def handle_frame(self, event_time): + self._frame.notify_key_press() + + def handle_quit_emulator(self, event_time): + session.get_session_manager().shutdown() + + def handle_open_search(self, event_time): + journalactivity.get_journal().focus_search() + + def _key_pressed_cb(self, grabber, keycode, state, event_time): + key = grabber.get_key(keycode, state) + logging.debug('_key_pressed_cb: %i %i %s', keycode, state, key) + if key is not None: + self._key_pressed = key + self._keycode_pressed = keycode + self._keystate_pressed = state + + action = _actions_table[key] + if self._tabbing_handler.is_tabbing(): + # Only accept window tabbing events, everything else + # cancels the tabbing operation. + if not action in ['next_window', 'previous_window']: + self._tabbing_handler.stop(event_time) + return True + + if hasattr(action, 'handle_key_press'): + action.handle_key_press(key) + elif isinstance(action, basestring): + method = getattr(self, 'handle_' + action) + method(event_time) + else: + raise TypeError('Invalid action %r' % action) + + return True + else: + # If this is not a registered key, then cancel tabbing. + if self._tabbing_handler.is_tabbing(): + if not grabber.is_modifier(keycode): + self._tabbing_handler.stop(event_time) + return True + + return False + + def _key_released_cb(self, grabber, keycode, state, event_time): + logging.debug('_key_released_cb: %i %i', keycode, state) + if self._tabbing_handler.is_tabbing(): + # We stop tabbing and switch to the new window as soon as the + # modifier key is raised again. + if grabber.is_modifier(keycode, mask=_TABBING_MODIFIER): + self._tabbing_handler.stop(event_time) + + return True + return False + + +def setup(frame): + global _instance + + if _instance: + del _instance + + _instance = KeyHandler(frame) diff --git a/src/jarabe/view/launcher.py b/src/jarabe/view/launcher.py new file mode 100644 index 0000000..5c645c4 --- /dev/null +++ b/src/jarabe/view/launcher.py @@ -0,0 +1,172 @@ +# Copyright (C) 2008, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ + +import gtk +import gobject + +from sugar import wm +from sugar.graphics import style + +from jarabe.model import shell +from jarabe.view.pulsingicon import PulsingIcon + + +class LaunchWindow(gtk.Window): + + def __init__(self, activity_id, icon_path, icon_color): + gobject.GObject.__init__(self) + + self.props.type_hint = gtk.gdk.WINDOW_TYPE_HINT_NORMAL + self.props.decorated = False + self.modify_bg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color()) + + canvas = gtk.VBox() + canvas.show() + self.add(canvas) + + bar_size = gtk.gdk.screen_height() / 5 * 2 + + header = gtk.VBox() + header.set_size_request(-1, bar_size) + header.show() + canvas.pack_start(header, expand=False) + + self._activity_id = activity_id + + self._activity_icon = PulsingIcon(file=icon_path, + pixel_size=style.XLARGE_ICON_SIZE) + self._activity_icon.set_base_color(icon_color) + self._activity_icon.set_zooming(style.SMALL_ICON_SIZE, + style.XLARGE_ICON_SIZE, 10) + self._activity_icon.set_pulsing(True) + self._activity_icon.show() + canvas.pack_start(self._activity_icon) + + footer = gtk.VBox(spacing=style.DEFAULT_SPACING) + footer.set_size_request(-1, bar_size) + footer.show() + canvas.pack_end(footer, expand=False) + + self.error_text = gtk.Label() + self.error_text.props.use_markup = True + footer.pack_start(self.error_text, expand=False) + + button_box = gtk.Alignment(xalign=0.5) + button_box.show() + footer.pack_start(button_box, expand=False) + self.cancel_button = gtk.Button(stock=gtk.STOCK_STOP) + button_box.add(self.cancel_button) + + self.connect('realize', self.__realize_cb) + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self.__size_changed_cb) + + self._home = shell.get_model() + self._home.connect('active-activity-changed', + self.__active_activity_changed_cb) + + self.connect('destroy', self.__destroy_cb) + + self._update_size() + + def show(self): + self.present() + + def _update_size(self): + self.resize(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + + def __realize_cb(self, widget): + wm.set_activity_id(widget.window, str(self._activity_id)) + widget.window.property_change('_SUGAR_WINDOW_TYPE', 'STRING', 8, + gtk.gdk.PROP_MODE_REPLACE, 'launcher') + + def __size_changed_cb(self, screen): + self._update_size() + + def __active_activity_changed_cb(self, model, activity): + if activity.get_activity_id() == self._activity_id: + self._activity_icon.props.paused = False + else: + self._activity_icon.props.paused = True + + def __destroy_cb(self, box): + self._activity_icon.props.pulsing = False + self._home.disconnect_by_func(self.__active_activity_changed_cb) + + +def setup(): + model = shell.get_model() + model.connect('launch-started', __launch_started_cb) + model.connect('launch-failed', __launch_failed_cb) + model.connect('launch-completed', __launch_completed_cb) + + +def add_launcher(activity_id, icon_path, icon_color): + model = shell.get_model() + + if model.get_launcher(activity_id) is not None: + return + + launch_window = LaunchWindow(activity_id, icon_path, icon_color) + launch_window.show() + + model.register_launcher(activity_id, launch_window) + + +def __launch_started_cb(home_model, home_activity): + add_launcher(home_activity.get_activity_id(), + home_activity.get_icon_path(), home_activity.get_icon_color()) + + +def __launch_failed_cb(home_model, home_activity): + activity_id = home_activity.get_activity_id() + launcher = shell.get_model().get_launcher(activity_id) + + if launcher is None: + logging.error('Launcher for %s is missing', activity_id) + else: + launcher.error_text.props.label = _('%s failed to start.') % \ + home_activity.get_activity_name() + launcher.error_text.show() + + launcher.cancel_button.connect('clicked', + __cancel_button_clicked_cb, home_activity) + launcher.cancel_button.show() + + +def __cancel_button_clicked_cb(button, home_activity): + _destroy_launcher(home_activity) + + +def __launch_completed_cb(home_model, home_activity): + _destroy_launcher(home_activity) + + +def _destroy_launcher(home_activity): + activity_id = home_activity.get_activity_id() + + launcher = shell.get_model().get_launcher(activity_id) + if launcher is None: + if not home_activity.is_journal(): + logging.error('Launcher was not registered for %s', activity_id) + return + + shell.get_model().unregister_launcher(activity_id) + launcher.destroy() diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py new file mode 100644 index 0000000..3195c0c --- /dev/null +++ b/src/jarabe/view/palettes.py @@ -0,0 +1,255 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import statvfs +from gettext import gettext as _ +import logging + +import gconf +import glib +import gtk + +from sugar import env +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor +from sugar.activity.i18n import pgettext + +from jarabe.model import shell +from jarabe.view.viewsource import setup_view_source +from jarabe.journal import misc + + +class BasePalette(Palette): + def __init__(self, home_activity): + Palette.__init__(self) + + self._notify_launch_hid = None + + if home_activity.props.launch_status == shell.Activity.LAUNCHING: + self._notify_launch_hid = home_activity.connect( \ + 'notify::launch-status', self.__notify_launch_status_cb) + self.set_primary_text(glib.markup_escape_text(_('Starting...'))) + elif home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() + else: + self.setup_palette() + + def setup_palette(self): + raise NotImplementedError + + def _on_failed_launch(self): + message = _('Activity failed to start') + self.set_primary_text(glib.markup_escape_text(message)) + + def __notify_launch_status_cb(self, home_activity, pspec): + home_activity.disconnect(self._notify_launch_hid) + self._notify_launch_hid = None + if home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() + else: + self.setup_palette() + + +class CurrentActivityPalette(BasePalette): + def __init__(self, home_activity): + self._home_activity = home_activity + BasePalette.__init__(self, home_activity) + + def setup_palette(self): + activity_name = self._home_activity.get_activity_name() + if activity_name: + self.props.primary_text = glib.markup_escape_text(activity_name) + + title = self._home_activity.get_title() + if title and title != activity_name: + self.props.secondary_text = glib.markup_escape_text(title) + + menu_item = MenuItem(_('Resume'), 'activity-start') + menu_item.connect('activate', self.__resume_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + # TODO: share-with, keep + + menu_item = MenuItem(_('View Source'), 'view-source') + # TODO Make this accelerator translatable + menu_item.props.accelerator = 'v' + menu_item.connect('activate', self.__view_source__cb) + self.menu.append(menu_item) + menu_item.show() + + separator = gtk.SeparatorMenuItem() + self.menu.append(separator) + separator.show() + + menu_item = MenuItem(_('Stop'), 'activity-stop') + menu_item.connect('activate', self.__stop_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __resume_activate_cb(self, menu_item): + self._home_activity.get_window().activate(gtk.get_current_event_time()) + + def __view_source__cb(self, menu_item): + setup_view_source(self._home_activity) + shell_model = shell.get_model() + if self._home_activity is not shell_model.get_active_activity(): + self._home_activity.get_window().activate( \ + gtk.get_current_event_time()) + + def __stop_activate_cb(self, menu_item): + self._home_activity.get_window().close(1) + + +class ActivityPalette(Palette): + __gtype_name__ = 'SugarActivityPalette' + + def __init__(self, activity_info): + self._activity_info = activity_info + + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + activity_icon = Icon(file=activity_info.get_icon(), + xo_color=color, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) + + name = activity_info.get_name() + Palette.__init__(self, primary_text=glib.markup_escape_text(name), + icon=activity_icon) + + xo_color = XoColor('%s,%s' % (style.COLOR_WHITE.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + menu_item = MenuItem(text_label=_('Start new'), + file_name=activity_info.get_icon(), + xo_color=xo_color) + menu_item.connect('activate', self.__start_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + # TODO: start-with + + def __start_activate_cb(self, menu_item): + self.popdown(immediate=True) + misc.launch(self._activity_info) + + +class JournalPalette(BasePalette): + def __init__(self, home_activity): + self._home_activity = home_activity + self._progress_bar = None + self._free_space_label = None + + BasePalette.__init__(self, home_activity) + + def setup_palette(self): + title = self._home_activity.get_title() + self.set_primary_text(glib.markup_escape_text(title)) + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self._progress_bar = gtk.ProgressBar() + vbox.add(self._progress_bar) + self._progress_bar.show() + + self._free_space_label = gtk.Label() + self._free_space_label.set_alignment(0.5, 0.5) + vbox.add(self._free_space_label) + self._free_space_label.show() + + self.connect('popup', self.__popup_cb) + + menu_item = MenuItem(_('Show contents')) + + icon = Icon(file=self._home_activity.get_icon_path(), + icon_size=gtk.ICON_SIZE_MENU, + xo_color=self._home_activity.get_icon_color()) + menu_item.set_image(icon) + icon.show() + + menu_item.connect('activate', self.__open_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __open_activate_cb(self, menu_item): + self._home_activity.get_window().activate(gtk.get_current_event_time()) + + def __popup_cb(self, palette): + stat = os.statvfs(env.get_profile_path()) + free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL] + total_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BLOCKS] + + fraction = (total_space - free_space) / float(total_space) + self._progress_bar.props.fraction = fraction + self._free_space_label.props.label = _('%(free_space)d MB Free') % \ + {'free_space': free_space / (1024 * 1024)} + + +class VolumePalette(Palette): + def __init__(self, mount): + Palette.__init__(self, label=mount.get_name()) + self._mount = mount + + path = mount.get_root().get_path() + self.props.secondary_text = glib.markup_escape_text(path) + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self._progress_bar = gtk.ProgressBar() + vbox.add(self._progress_bar) + self._progress_bar.show() + + self._free_space_label = gtk.Label() + self._free_space_label.set_alignment(0.5, 0.5) + vbox.add(self._free_space_label) + self._free_space_label.show() + + self.connect('popup', self.__popup_cb) + + menu_item = MenuItem(pgettext('Volume', 'Remove')) + + icon = Icon(icon_name='media-eject', icon_size=gtk.ICON_SIZE_MENU) + menu_item.set_image(icon) + icon.show() + + menu_item.connect('activate', self.__unmount_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + def __unmount_activate_cb(self, menu_item): + self._mount.unmount(self.__unmount_cb) + + def __unmount_cb(self, mount, result): + logging.debug('__unmount_cb %r %r', mount, result) + mount.unmount_finish(result) + + def __popup_cb(self, palette): + mount_point = self._mount.get_root().get_path() + stat = os.statvfs(mount_point) + free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL] + total_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BLOCKS] + + fraction = (total_space - free_space) / float(total_space) + self._progress_bar.props.fraction = fraction + self._free_space_label.props.label = _('%(free_space)d MB Free') % \ + {'free_space': free_space / (1024 * 1024)} diff --git a/src/jarabe/view/pulsingicon.py b/src/jarabe/view/pulsingicon.py new file mode 100644 index 0000000..9a98a80 --- /dev/null +++ b/src/jarabe/view/pulsingicon.py @@ -0,0 +1,237 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import math + +import gobject + +from sugar.graphics.icon import Icon, CanvasIcon +from sugar.graphics import style + +_INTERVAL = 100 +_STEP = math.pi / 10 # must be a fraction of pi, for clean caching +_MINIMAL_ALPHA_VALUE = 0.33 + + +class Pulser(object): + def __init__(self, icon): + self._pulse_hid = None + self._icon = icon + self._phase = 0 + self._start_scale = 1.0 + self._end_scale = 1.0 + self._zoom_steps = 1 + self._current_zoom_step = 1 + self._current_scale_step = 1 + + def set_zooming(self, start_scale, end_scale, zoom_steps): + """ Set start and end scale and number of steps in zoom animation """ + self._start_scale = start_scale + self._end_scale = end_scale + self._zoom_steps = zoom_steps + self._current_scale_step = abs(self._start_scale - self._end_scale) / \ + self._zoom_steps + self._icon.scale = self._start_scale + + def start(self, restart=False): + if restart: + self._phase = 0 + if self._pulse_hid is None: + self._pulse_hid = gobject.timeout_add(_INTERVAL, self.__pulse_cb) + if self._start_scale != self._end_scale: + self._icon.scale = self._start_scale + \ + self._current_scale_step * self._current_zoom_step + + def stop(self): + if self._pulse_hid is not None: + gobject.source_remove(self._pulse_hid) + self._pulse_hid = None + self._icon.xo_color = self._icon.get_base_color() + self._phase = 0 + self._icon.alpha = 1.0 + + def update(self): + self._icon.xo_color = self._icon.base_color + self._icon.alpha = _MINIMAL_ALPHA_VALUE + \ + (1 - _MINIMAL_ALPHA_VALUE) * (math.cos(self._phase) + 1) / 2 + + def __pulse_cb(self): + self._phase += _STEP + if self._current_zoom_step <= self._zoom_steps and \ + self._start_scale != self._end_scale: + self._icon.scale = self._start_scale + \ + self._current_scale_step * self._current_zoom_step + self._current_zoom_step += 1 + self.update() + return True + + +class PulsingIcon(Icon): + __gtype_name__ = 'SugarPulsingIcon' + + def __init__(self, **kwargs): + self._pulser = Pulser(self) + self._base_color = None + self._pulse_color = None + self._paused = False + self._pulsing = False + + Icon.__init__(self, **kwargs) + + self._palette = None + self.connect('destroy', self.__destroy_cb) + + def set_pulse_color(self, pulse_color): + self._pulse_color = pulse_color + self._pulser.update() + + def get_pulse_color(self): + return self._pulse_color + + pulse_color = gobject.property( + type=object, getter=get_pulse_color, setter=set_pulse_color) + + def set_base_color(self, base_color): + self._base_color = base_color + self._pulser.update() + + def get_base_color(self): + return self._base_color + + def set_zooming(self, start_size=style.SMALL_ICON_SIZE, + end_size=style.XLARGE_ICON_SIZE, + zoom_steps=10): + if start_size > end_size: + start_scale = 1.0 + end_scale = float(end_size) / start_size + else: + start_scale = float(start_size) / end_size + end_scale = 1.0 + self._pulser.set_zooming(start_scale, end_scale, zoom_steps) + + base_color = gobject.property( + type=object, getter=get_base_color, setter=set_base_color) + + def set_paused(self, paused): + self._paused = paused + + if self._paused: + self._pulser.stop() + else: + self._pulser.start(restart=False) + + def get_paused(self): + return self._paused + + paused = gobject.property( + type=bool, default=False, getter=get_paused, setter=set_paused) + + def set_pulsing(self, pulsing): + self._pulsing = pulsing + + if self._pulsing: + self._pulser.start(restart=True) + else: + self._pulser.stop() + + def get_pulsing(self): + return self._pulsing + + pulsing = gobject.property( + type=bool, default=False, getter=get_pulsing, setter=set_pulsing) + + def _get_palette(self): + return self._palette + + def _set_palette(self, palette): + if self._palette is not None: + self._palette.props.invoker = None + self._palette = palette + + palette = property(_get_palette, _set_palette) + + def __destroy_cb(self, icon): + self._pulser.stop() + if self._palette is not None: + self._palette.destroy() + + +class CanvasPulsingIcon(CanvasIcon): + __gtype_name__ = 'SugarCanvasPulsingIcon' + + def __init__(self, **kwargs): + self._pulser = Pulser(self) + self._base_color = None + self._pulse_color = None + self._paused = False + self._pulsing = False + + CanvasIcon.__init__(self, **kwargs) + + self.connect('destroy', self.__destroy_cb) + + def __destroy_cb(self, box): + self._pulser.stop() + + def set_pulse_color(self, pulse_color): + self._pulse_color = pulse_color + self._pulser.update() + + def get_pulse_color(self): + return self._pulse_color + + pulse_color = gobject.property( + type=object, getter=get_pulse_color, setter=set_pulse_color) + + def set_base_color(self, base_color): + self._base_color = base_color + self._pulser.update() + + def get_base_color(self): + return self._base_color + + base_color = gobject.property( + type=object, getter=get_base_color, setter=set_base_color) + + def set_paused(self, paused): + self._paused = paused + + if self._paused: + self._pulser.stop() + elif self._pulsing: + self._pulser.start(restart=False) + + def get_paused(self): + return self._paused + + paused = gobject.property( + type=bool, default=False, getter=get_paused, setter=set_paused) + + def set_pulsing(self, pulsing): + self._pulsing = pulsing + if self._paused: + return + + if self._pulsing: + self._pulser.start(restart=True) + else: + self._pulser.stop() + + def get_pulsing(self): + return self._pulsing + + pulsing = gobject.property( + type=bool, default=False, getter=get_pulsing, setter=set_pulsing) diff --git a/src/jarabe/view/service.py b/src/jarabe/view/service.py new file mode 100644 index 0000000..29e46b2 --- /dev/null +++ b/src/jarabe/view/service.py @@ -0,0 +1,90 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""D-bus service providing access to the shell's functionality""" + +import dbus +import gtk + +from jarabe.model import shell +from jarabe.model import bundleregistry + + +_DBUS_SERVICE = 'org.laptop.Shell' +_DBUS_SHELL_IFACE = 'org.laptop.Shell' +_DBUS_PATH = '/org/laptop/Shell' + + +class UIService(dbus.service.Object): + """Provides d-bus service to script the shell's operations + + Uses a shell_model object to observe events such as changes to: + + * nickname + * colour + * icon + * currently active activity + + and pass the event off to the methods in the dbus signature. + + Key method here at the moment is add_bundle, which is used to + do a run-time registration of a bundle using it's application path. + + XXX At the moment the d-bus service methods do not appear to do + anything other than add_bundle + """ + + def __init__(self): + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus) + dbus.service.Object.__init__(self, bus_name, _DBUS_PATH) + + self._shell_model = shell.get_model() + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature='s', out_signature='s') + def GetBundlePath(self, bundle_id): + bundle = bundleregistry.get_registry().get_bundle(bundle_id) + if bundle: + return bundle.get_path() + else: + return '' + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature='s', out_signature='b') + def ActivateActivity(self, activity_id): + """Switch to the window related to this activity_id and return a + boolean indicating if there is a real (ie. not a launcher window) + activity already open. + """ + activity = self._shell_model.get_activity_by_id(activity_id) + + if activity is not None and activity.get_window() is not None: + activity.get_window().activate(gtk.get_current_event_time()) + return self._shell_model.get_launcher(activity_id) is None + + return False + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature='ss', out_signature='') + def NotifyLaunch(self, bundle_id, activity_id): + shell.get_model().notify_launch(activity_id, bundle_id) + + @dbus.service.method(_DBUS_SHELL_IFACE, + in_signature='s', out_signature='') + def NotifyLaunchFailure(self, activity_id): + shell.get_model().notify_launch_failed(activity_id) diff --git a/src/jarabe/view/tabbinghandler.py b/src/jarabe/view/tabbinghandler.py new file mode 100644 index 0000000..0889792 --- /dev/null +++ b/src/jarabe/view/tabbinghandler.py @@ -0,0 +1,149 @@ +# Copyright (C) 2008, Benjamin Berg +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gobject +import gtk + +from jarabe.model import shell + + +_RAISE_DELAY = 250 + + +class TabbingHandler(object): + def __init__(self, frame, modifier): + self._frame = frame + self._tabbing = False + self._modifier = modifier + self._timeout = None + + def _start_tabbing(self): + if not self._tabbing: + logging.debug('Grabing the input.') + + screen = gtk.gdk.screen_get_default() + window = screen.get_root_window() + keyboard_grab_result = gtk.gdk.keyboard_grab(window) + pointer_grab_result = gtk.gdk.pointer_grab(window) + + self._tabbing = (keyboard_grab_result == gtk.gdk.GRAB_SUCCESS and + pointer_grab_result == gtk.gdk.GRAB_SUCCESS) + + # Now test that the modifier is still active to prevent race + # conditions. We also test if one of the grabs failed. + mask = window.get_pointer()[2] + if not self._tabbing or not (mask & self._modifier): + logging.debug('Releasing grabs again.') + + # ungrab keyboard/pointer if the grab was successfull. + if keyboard_grab_result == gtk.gdk.GRAB_SUCCESS: + gtk.gdk.keyboard_ungrab() + if pointer_grab_result == gtk.gdk.GRAB_SUCCESS: + gtk.gdk.pointer_ungrab() + + self._tabbing = False + else: + self._frame.show(self._frame.MODE_NON_INTERACTIVE) + + def __timeout_cb(self, event_time): + self._activate_current(event_time) + self._timeout = None + return False + + def _start_timeout(self, event_time): + self._cancel_timeout() + self._timeout = gobject.timeout_add(_RAISE_DELAY, + lambda: self.__timeout_cb(event_time)) + + def _cancel_timeout(self): + if self._timeout: + gobject.source_remove(self._timeout) + self._timeout = None + + def _activate_current(self, event_time): + home_model = shell.get_model() + activity = home_model.get_tabbing_activity() + if activity and activity.get_window(): + activity.get_window().activate(event_time) + + def next_activity(self, event_time): + if not self._tabbing: + first_switch = True + self._start_tabbing() + else: + first_switch = False + + if self._tabbing: + shell_model = shell.get_model() + zoom_level = shell_model.zoom_level + zoom_activity = (zoom_level == shell.ShellModel.ZOOM_ACTIVITY) + + if not zoom_activity and first_switch: + activity = shell_model.get_active_activity() + else: + activity = shell_model.get_tabbing_activity() + activity = shell_model.get_next_activity(current=activity) + + shell_model.set_tabbing_activity(activity) + self._start_timeout(event_time) + else: + self._activate_next_activity(event_time) + + def previous_activity(self, event_time): + if not self._tabbing: + first_switch = True + self._start_tabbing() + else: + first_switch = False + + if self._tabbing: + shell_model = shell.get_model() + zoom_level = shell_model.zoom_level + zoom_activity = (zoom_level == shell.ShellModel.ZOOM_ACTIVITY) + + if not zoom_activity and first_switch: + activity = shell_model.get_active_activity() + else: + activity = shell_model.get_tabbing_activity() + activity = shell_model.get_previous_activity(current=activity) + + shell_model.set_tabbing_activity(activity) + self._start_timeout(event_time) + else: + self._activate_next_activity(event_time) + + def _activate_next_activity(self, event_time): + next_activity = shell.get_model().get_next_activity() + if next_activity: + next_activity.get_window().activate(event_time) + + def stop(self, event_time): + gtk.gdk.keyboard_ungrab() + gtk.gdk.pointer_ungrab() + self._tabbing = False + + self._frame.hide() + + self._cancel_timeout() + self._activate_current(event_time) + + home_model = shell.get_model() + home_model.set_tabbing_activity(None) + + def is_tabbing(self): + return self._tabbing diff --git a/src/jarabe/view/viewsource.py b/src/jarabe/view/viewsource.py new file mode 100644 index 0000000..1285e69 --- /dev/null +++ b/src/jarabe/view/viewsource.py @@ -0,0 +1,570 @@ +# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer +# Copyright (C) 2011 Walter Bender +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import shutil +import sys +import logging +from gettext import gettext as _ + +import gobject +import pango +import gtk +import gtksourceview2 +import dbus +import gconf + +from sugar.graphics import style +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.bundle.activitybundle import ActivityBundle +from sugar.datastore import datastore +from sugar.env import get_user_activities_path +from sugar import mime + +from jarabe.view import customizebundle + +_EXCLUDE_EXTENSIONS = ('.pyc', '.pyo', '.so', '.o', '.a', '.la', '.mo', '~', + '.xo', '.tar', '.bz2', '.zip', '.gz') +_EXCLUDE_NAMES = ['.deps', '.libs'] + +_SOURCE_FONT = pango.FontDescription('Monospace %d' % style.FONT_SIZE) + +_logger = logging.getLogger('ViewSource') +map_activity_to_window = {} + + +def setup_view_source(activity): + service = activity.get_service() + if service is not None: + try: + service.HandleViewSource() + return + except dbus.DBusException, e: + expected_exceptions = ['org.freedesktop.DBus.Error.UnknownMethod', + 'org.freedesktop.DBus.Python.NotImplementedError'] + if e.get_dbus_name() not in expected_exceptions: + logging.exception('Exception occured in HandleViewSource():') + except Exception: + logging.exception('Exception occured in HandleViewSource():') + + window_xid = activity.get_xid() + if window_xid is None: + _logger.error('Activity without a window xid') + return + + bundle_path = activity.get_bundle_path() + + if window_xid in map_activity_to_window: + _logger.debug('Viewsource window already open for %s %s', window_xid, + bundle_path) + return + + document_path = None + if service is not None: + try: + document_path = service.GetDocumentPath() + except dbus.DBusException, e: + expected_exceptions = ['org.freedesktop.DBus.Error.UnknownMethod', + 'org.freedesktop.DBus.Python.NotImplementedError'] + if e.get_dbus_name() not in expected_exceptions: + logging.exception('Exception occured in GetDocumentPath():') + except Exception: + logging.exception('Exception occured in GetDocumentPath():') + + if bundle_path is None and document_path is None: + _logger.debug('Activity without bundle_path nor document_path') + return + + sugar_toolkit_path = None + for path in sys.path: + if path.endswith('site-packages'): + if os.path.exists(os.path.join(path, 'sugar')): + sugar_toolkit_path = os.path.join(path, 'sugar') + break + + view_source = ViewSource(window_xid, bundle_path, document_path, + sugar_toolkit_path, activity.get_title()) + map_activity_to_window[window_xid] = view_source + view_source.show() + + +class ViewSource(gtk.Window): + __gtype_name__ = 'SugarViewSource' + + def __init__(self, window_xid, bundle_path, document_path, + sugar_toolkit_path, title): + gtk.Window.__init__(self) + + _logger.debug('ViewSource paths: %r %r %r', bundle_path, + document_path, sugar_toolkit_path) + + self.set_decorated(False) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_border_width(style.LINE_WIDTH) + + width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2 + height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2 + self.set_size_request(width, height) + + self._parent_window_xid = window_xid + self._sugar_toolkit_path = sugar_toolkit_path + + self.connect('realize', self.__realize_cb) + self.connect('destroy', self.__destroy_cb, document_path) + self.connect('key-press-event', self.__key_press_event_cb) + + vbox = gtk.VBox() + self.add(vbox) + vbox.show() + + toolbar = Toolbar(title, bundle_path, document_path, + sugar_toolkit_path) + vbox.pack_start(toolbar, expand=False) + toolbar.connect('stop-clicked', self.__stop_clicked_cb) + toolbar.connect('source-selected', self.__source_selected_cb) + toolbar.show() + + pane = gtk.HPaned() + vbox.pack_start(pane) + pane.show() + + self._selected_bundle_file = None + self._selected_sugar_file = None + file_name = '' + + activity_bundle = ActivityBundle(bundle_path) + command = activity_bundle.get_command() + if len(command.split(' ')) > 1: + name = command.split(' ')[1].split('.')[-1] + tmppath = command.split(' ')[1].replace('.', '/') + file_name = tmppath[0:-(len(name) + 1)] + '.py' + path = os.path.join(activity_bundle.get_path(), file_name) + self._selected_bundle_file = path + + # Split the tree pane into two vertical panes, one of which + # will be hidden + tree_panes = gtk.VPaned() + tree_panes.show() + + self._bundle_source_viewer = FileViewer(bundle_path, file_name) + self._bundle_source_viewer.connect('file-selected', + self.__file_selected_cb) + tree_panes.add1(self._bundle_source_viewer) + self._bundle_source_viewer.show() + + file_name = 'env.py' + self._selected_sugar_file = os.path.join(sugar_toolkit_path, file_name) + self._sugar_source_viewer = FileViewer(sugar_toolkit_path, file_name) + self._sugar_source_viewer.connect('file-selected', + self.__file_selected_cb) + tree_panes.add2(self._sugar_source_viewer) + self._sugar_source_viewer.hide() + + pane.add1(tree_panes) + + self._source_display = SourceDisplay() + pane.add2(self._source_display) + self._source_display.show() + self._source_display.file_path = self._selected_bundle_file + + if document_path is not None: + self._select_source(document_path) + + def _calculate_char_width(self, char_count): + widget = gtk.Label('') + context = widget.get_pango_context() + pango_font = context.load_font(_SOURCE_FONT) + metrics = pango_font.get_metrics() + return pango.PIXELS(metrics.get_approximate_char_width()) * char_count + + def __realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + + parent = gtk.gdk.window_foreign_new(self._parent_window_xid) + self.window.set_transient_for(parent) + + def __stop_clicked_cb(self, widget): + self.destroy() + + def __source_selected_cb(self, widget, path): + self._select_source(path) + + def _select_source(self, path): + if os.path.isfile(path): + _logger.debug('_select_source called with file: %r', path) + self._source_display.file_path = path + self._bundle_source_viewer.hide() + self._sugar_source_viewer.hide() + elif path == self._sugar_toolkit_path: + _logger.debug('_select_source called with sugar toolkit path: %r', + path) + self._sugar_source_viewer.set_path(path) + self._source_display.file_path = self._selected_sugar_file + self._sugar_source_viewer.show() + self._bundle_source_viewer.hide() + else: + _logger.debug('_select_source called with path: %r', path) + self._bundle_source_viewer.set_path(path) + self._source_display.file_path = self._selected_bundle_file + self._bundle_source_viewer.show() + self._sugar_source_viewer.hide() + + def __destroy_cb(self, window, document_path): + del map_activity_to_window[self._parent_window_xid] + if document_path is not None and os.path.exists(document_path): + os.unlink(document_path) + + def __key_press_event_cb(self, window, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == 'Escape': + self.destroy() + + def __file_selected_cb(self, file_viewer, file_path): + if file_path is not None and os.path.isfile(file_path): + self._source_display.file_path = file_path + if file_viewer == self._bundle_source_viewer: + self._selected_bundle_file = file_path + else: + self._selected_sugar_file = file_path + else: + self._source_display.file_path = None + + +class DocumentButton(RadioToolButton): + __gtype_name__ = 'SugarDocumentButton' + + def __init__(self, file_name, document_path, title, bundle=False): + RadioToolButton.__init__(self) + + self._document_path = document_path + self._title = title + self._jobject = None + + self.props.tooltip = _('Instance Source') + + client = gconf.client_get_default() + self._color = client.get_string('/desktop/sugar/user/color') + icon = Icon(file=file_name, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR, + xo_color=XoColor(self._color)) + self.set_icon_widget(icon) + icon.show() + + if bundle: + menu_item = MenuItem(_('Duplicate')) + icon = Icon(icon_name='edit-duplicate', + icon_size=gtk.ICON_SIZE_MENU, + xo_color=XoColor(self._color)) + menu_item.connect('activate', self.__copy_to_home_cb) + else: + menu_item = MenuItem(_('Keep')) + icon = Icon(icon_name='document-save', + icon_size=gtk.ICON_SIZE_MENU, + xo_color=XoColor(self._color)) + menu_item.connect('activate', self.__keep_in_journal_cb) + + menu_item.set_image(icon) + + self.props.palette.menu.append(menu_item) + menu_item.show() + + def __copy_to_home_cb(self, menu_item): + """Make a local copy of the activity bundle in user_activities_path""" + user_activities_path = get_user_activities_path() + nick = customizebundle.generate_unique_id() + new_basename = '%s_copy_of_%s' % ( + nick, os.path.basename(self._document_path)) + if not os.path.exists(os.path.join(user_activities_path, + new_basename)): + shutil.copytree(self._document_path, + os.path.join(user_activities_path, new_basename), + symlinks=True) + customizebundle.generate_bundle(nick, new_basename) + else: + _logger.debug('%s already exists', new_basename) + + def __keep_in_journal_cb(self, menu_item): + mime_type = mime.get_from_file_name(self._document_path) + if mime_type == 'application/octet-stream': + mime_type = mime.get_for_file(self._document_path) + + self._jobject = datastore.create() + title = _('Source') + ': ' + self._title + self._jobject.metadata['title'] = title + self._jobject.metadata['keep'] = '0' + self._jobject.metadata['buddies'] = '' + self._jobject.metadata['preview'] = '' + self._jobject.metadata['icon-color'] = self._color + self._jobject.metadata['mime_type'] = mime_type + self._jobject.metadata['source'] = '1' + self._jobject.file_path = self._document_path + datastore.write(self._jobject, transfer_ownership=True, + reply_handler=self.__internal_save_cb, + error_handler=self.__internal_save_error_cb) + + def __internal_save_cb(self): + _logger.debug('Saved Source object to datastore.') + self._jobject.destroy() + + def __internal_save_error_cb(self, err): + _logger.debug('Error saving Source object to datastore: %s', err) + self._jobject.destroy() + + +class Toolbar(gtk.Toolbar): + __gtype_name__ = 'SugarViewSourceToolbar' + + __gsignals__ = { + 'stop-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'source-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + } + + def __init__(self, title, bundle_path, document_path, sugar_toolkit_path): + gtk.Toolbar.__init__(self) + + document_button = None + self.bundle_path = bundle_path + self.sugar_toolkit_path = sugar_toolkit_path + + self._add_separator() + + activity_bundle = ActivityBundle(bundle_path) + file_name = activity_bundle.get_icon() + + if document_path is not None and os.path.exists(document_path): + document_button = DocumentButton(file_name, document_path, title) + document_button.connect('toggled', self.__button_toggled_cb, + document_path) + self.insert(document_button, -1) + document_button.show() + self._add_separator() + + if bundle_path is not None and os.path.exists(bundle_path): + activity_button = DocumentButton(file_name, bundle_path, title, + bundle=True) + icon = Icon(file=file_name, + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR, + fill_color=style.COLOR_TRANSPARENT.get_svg(), + stroke_color=style.COLOR_WHITE.get_svg()) + activity_button.set_icon_widget(icon) + icon.show() + if document_button is not None: + activity_button.props.group = document_button + activity_button.props.tooltip = _('Activity Bundle Source') + activity_button.connect('toggled', self.__button_toggled_cb, + bundle_path) + self.insert(activity_button, -1) + activity_button.show() + self._add_separator() + + if sugar_toolkit_path is not None: + sugar_button = RadioToolButton() + icon = Icon(icon_name='computer-xo', + icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR, + fill_color=style.COLOR_TRANSPARENT.get_svg(), + stroke_color=style.COLOR_WHITE.get_svg()) + sugar_button.set_icon_widget(icon) + icon.show() + if document_button is not None: + sugar_button.props.group = document_button + else: + sugar_button.props.group = activity_button + sugar_button.props.tooltip = _('Sugar Toolkit Source') + sugar_button.connect('toggled', self.__button_toggled_cb, + sugar_toolkit_path) + self.insert(sugar_button, -1) + sugar_button.show() + self._add_separator() + + self.activity_title_text = _('View source: %s') % title + self.sugar_toolkit_title_text = _('View source: %r') % 'Sugar Toolkit' + self.label = gtk.Label() + self.label.set_markup('%s' % self.activity_title_text) + self.label.set_alignment(0, 0.5) + self._add_widget(self.label) + + self._add_separator(True) + + stop = ToolButton(icon_name='dialog-cancel') + stop.set_tooltip(_('Close')) + stop.connect('clicked', self.__stop_clicked_cb) + self.insert(stop, -1) + stop.show() + + def _add_separator(self, expand=False): + separator = gtk.SeparatorToolItem() + separator.props.draw = False + if expand: + separator.set_expand(True) + else: + separator.set_size_request(style.DEFAULT_SPACING, -1) + self.insert(separator, -1) + separator.show() + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + def __stop_clicked_cb(self, button): + self.emit('stop-clicked') + + def __button_toggled_cb(self, button, path): + if button.props.active: + self.emit('source-selected', path) + if path == self.sugar_toolkit_path: + self.label.set_markup('%s' % self.sugar_toolkit_title_text) + else: # Use activity title for either bundle path or document path + self.label.set_markup('%s' % self.activity_title_text) + + +class FileViewer(gtk.ScrolledWindow): + __gtype_name__ = 'SugarFileViewer' + + __gsignals__ = { + 'file-selected': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + } + + def __init__(self, path, initial_filename): + gtk.ScrolledWindow.__init__(self) + + self.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC + self.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC + self.set_size_request(style.GRID_CELL_SIZE * 3, -1) + + self._path = None + self._initial_filename = initial_filename + + self._tree_view = gtk.TreeView() + self.add(self._tree_view) + self._tree_view.show() + + self._tree_view.props.headers_visible = False + selection = self._tree_view.get_selection() + selection.connect('changed', self.__selection_changed_cb) + + cell = gtk.CellRendererText() + column = gtk.TreeViewColumn() + column.pack_start(cell, True) + column.add_attribute(cell, 'text', 0) + self._tree_view.append_column(column) + self._tree_view.set_search_column(0) + + self.set_path(path) + + def set_path(self, path): + self.emit('file-selected', None) + if self._path == path: + return + + self._path = path + self._tree_view.set_model(gtk.TreeStore(str, str)) + self._model = self._tree_view.get_model() + self._add_dir_to_model(path) + + def _add_dir_to_model(self, dir_path, parent=None): + for f in os.listdir(dir_path): + if f.endswith(_EXCLUDE_EXTENSIONS) or f in _EXCLUDE_NAMES: + continue + + full_path = os.path.join(dir_path, f) + if os.path.isdir(full_path): + new_iter = self._model.append(parent, [f, full_path]) + self._add_dir_to_model(full_path, new_iter) + else: + current_iter = self._model.append(parent, [f, full_path]) + if f == self._initial_filename: + selection = self._tree_view.get_selection() + selection.select_iter(current_iter) + + def __selection_changed_cb(self, selection): + model, tree_iter = selection.get_selected() + if tree_iter is None: + file_path = None + else: + file_path = model.get_value(tree_iter, 1) + self.emit('file-selected', file_path) + + +class SourceDisplay(gtk.ScrolledWindow): + __gtype_name__ = 'SugarSourceDisplay' + + def __init__(self): + gtk.ScrolledWindow.__init__(self) + + self.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC + self.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC + + self._buffer = gtksourceview2.Buffer() + self._buffer.set_highlight_syntax(True) + + self._source_view = gtksourceview2.View(self._buffer) + self._source_view.set_editable(False) + self._source_view.set_cursor_visible(True) + self._source_view.set_show_line_numbers(True) + self._source_view.set_show_right_margin(True) + self._source_view.set_right_margin_position(80) + #self._source_view.set_highlight_current_line(True) #FIXME: Ugly color + self._source_view.modify_font(_SOURCE_FONT) + self.add(self._source_view) + self._source_view.show() + + self._file_path = None + + def _set_file_path(self, file_path): + self._file_path = file_path + + if self._file_path is None: + self._buffer.set_text('') + return + + mime_type = mime.get_for_file(self._file_path) + _logger.debug('Detected mime type: %r', mime_type) + + language_manager = gtksourceview2.language_manager_get_default() + detected_language = None + for language_id in language_manager.get_language_ids(): + language = language_manager.get_language(language_id) + if mime_type in language.get_mime_types(): + detected_language = language + break + + if detected_language is not None: + _logger.debug('Detected language: %r', + detected_language.get_name()) + + self._buffer.set_language(detected_language) + self._buffer.set_text(open(self._file_path, 'r').read()) + + def _get_file_path(self): + return self._file_path + + file_path = property(_get_file_path, _set_file_path) -- cgit v0.9.1