Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/jarabe/journal
diff options
context:
space:
mode:
Diffstat (limited to 'src/jarabe/journal')
-rw-r--r--src/jarabe/journal/Makefile.am18
-rw-r--r--src/jarabe/journal/Makefile.in455
-rw-r--r--src/jarabe/journal/__init__.py15
-rw-r--r--src/jarabe/journal/detailview.py119
-rw-r--r--src/jarabe/journal/expandedentry.py440
-rw-r--r--src/jarabe/journal/journalactivity.py375
-rw-r--r--src/jarabe/journal/journalentrybundle.py94
-rw-r--r--src/jarabe/journal/journaltoolbox.py572
-rw-r--r--src/jarabe/journal/journalwindow.py33
-rw-r--r--src/jarabe/journal/keepicon.py64
-rw-r--r--src/jarabe/journal/listmodel.py243
-rw-r--r--src/jarabe/journal/listview.py670
-rw-r--r--src/jarabe/journal/misc.py315
-rw-r--r--src/jarabe/journal/modalalert.py96
-rw-r--r--src/jarabe/journal/model.py818
-rw-r--r--src/jarabe/journal/objectchooser.py199
-rw-r--r--src/jarabe/journal/palettes.py383
-rw-r--r--src/jarabe/journal/volumestoolbar.py404
18 files changed, 5313 insertions, 0 deletions
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('<b>%s</b>' % (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('<b>%s</b>' % _('Your Journal is full'))
+ self._vbox.pack_start(self._title, False)
+ self._title.show()
+
+ self._message = gtk.Label(_('Please delete some old Journal'
+ ' entries to make space for new ones.'))
+ self._message.modify_fg(gtk.STATE_NORMAL,
+ style.COLOR_WHITE.get_gdk_color())
+ self._vbox.pack_start(self._message, False)
+ self._message.show()
+
+ alignment = gtk.Alignment(xalign=0.5, yalign=0.5)
+ self._vbox.pack_start(alignment, expand=False)
+ alignment.show()
+
+ self._show_journal = gtk.Button()
+ self._show_journal.set_label(_('Show Journal'))
+ alignment.add(self._show_journal)
+ self._show_journal.show()
+ self._show_journal.connect('clicked', self.__show_journal_cb)
+
+ self.add(self._main_view)
+ self._main_view.show()
+
+ self.connect('realize', self.__realize_cb)
+
+ def __realize_cb(self, widget):
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+
+ def __show_journal_cb(self, button):
+ """The opener will listen on the destroy signal"""
+ self.destroy()
diff --git a/src/jarabe/journal/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('<b>%s</b>' % _('Choose an object'))
+ label.set_alignment(0, 0.5)
+ self._add_widget(label, expand=True)
+
+ self.close_button = ToolButton(icon_name='dialog-cancel')
+ self.close_button.set_tooltip(_('Close'))
+ self.insert(self.close_button, -1)
+ self.close_button.show()
+
+ def _add_widget(self, widget, expand=False):
+ tool_item = gtk.ToolItem()
+ tool_item.set_expand(expand)
+
+ tool_item.add(widget)
+ widget.show()
+
+ self.insert(tool_item, -1)
+ tool_item.show()
+
+
+class ChooserListView(BaseListView):
+ __gtype_name__ = 'ChooserListView'
+
+ __gsignals__ = {
+ 'entry-activated': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([str])),
+ }
+
+ def __init__(self):
+ BaseListView.__init__(self)
+
+ self.cell_icon.props.show_palette = False
+ self.tree_view.props.hover_selection = True
+
+ self.tree_view.connect('button-release-event',
+ self.__button_release_event_cb)
+
+ def __entry_activated_cb(self, entry):
+ self.emit('entry-activated', entry)
+
+ def __button_release_event_cb(self, tree_view, event):
+ if event.window != tree_view.get_bin_window():
+ return False
+
+ pos = tree_view.get_path_at_pos(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