From b6db5b8af0af7ebfdd44cb53b09d63b819989338 Mon Sep 17 00:00:00 2001 From: Julio Reyes Date: Fri, 05 Jul 2013 14:13:11 +0000 Subject: Initial Commit --- (limited to 'src/jarabe/frame') diff --git a/src/jarabe/frame/Makefile.am b/src/jarabe/frame/Makefile.am new file mode 100644 index 0000000..e5c445f --- /dev/null +++ b/src/jarabe/frame/Makefile.am @@ -0,0 +1,18 @@ +sugardir = $(pythondir)/jarabe/frame +sugar_PYTHON = \ + __init__.py \ + activitiestray.py \ + clipboard.py \ + clipboardicon.py \ + clipboardmenu.py \ + clipboardobject.py \ + clipboardpanelwindow.py \ + clipboardtray.py \ + devicestray.py \ + frameinvoker.py \ + friendstray.py \ + eventarea.py \ + frame.py \ + notification.py \ + framewindow.py \ + zoomtoolbar.py diff --git a/src/jarabe/frame/Makefile.in b/src/jarabe/frame/Makefile.in new file mode 100644 index 0000000..8da62b8 --- /dev/null +++ b/src/jarabe/frame/Makefile.in @@ -0,0 +1,455 @@ +# Makefile.in generated by automake 1.11.3 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, +# 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Free Software +# Foundation, Inc. +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ +VPATH = @srcdir@ +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +subdir = src/jarabe/frame +DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in \ + $(sugar_PYTHON) +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(install_sh) -d +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +SOURCES = +DIST_SOURCES = +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +am__py_compile = PYTHON=$(PYTHON) $(SHELL) $(py_compile) +am__installdirs = "$(DESTDIR)$(sugardir)" +py_compile = $(top_srcdir)/py-compile +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +ALL_LINGUAS = @ALL_LINGUAS@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +CATALOGS = @CATALOGS@ +CATOBJEXT = @CATOBJEXT@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DATADIRNAME = @DATADIRNAME@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +GCONFTOOL = @GCONFTOOL@ +GCONF_SCHEMA_CONFIG_SOURCE = @GCONF_SCHEMA_CONFIG_SOURCE@ +GCONF_SCHEMA_FILE_DIR = @GCONF_SCHEMA_FILE_DIR@ +GETTEXT_PACKAGE = @GETTEXT_PACKAGE@ +GMOFILES = @GMOFILES@ +GMSGFMT = @GMSGFMT@ +GREP = @GREP@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +INSTOBJEXT = @INSTOBJEXT@ +INTLLIBS = @INTLLIBS@ +INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@ +INTLTOOL_MERGE = @INTLTOOL_MERGE@ +INTLTOOL_PERL = @INTLTOOL_PERL@ +INTLTOOL_UPDATE = @INTLTOOL_UPDATE@ +INTLTOOL_V_MERGE = @INTLTOOL_V_MERGE@ +INTLTOOL_V_MERGE_OPTIONS = @INTLTOOL_V_MERGE_OPTIONS@ +INTLTOOL__v_MERGE_ = @INTLTOOL__v_MERGE_@ +INTLTOOL__v_MERGE_0 = @INTLTOOL__v_MERGE_0@ +LDFLAGS = @LDFLAGS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LTLIBOBJS = @LTLIBOBJS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MKDIR_P = @MKDIR_P@ +MKINSTALLDIRS = @MKINSTALLDIRS@ +MSGFMT = @MSGFMT@ +MSGFMT_OPTS = @MSGFMT_OPTS@ +MSGMERGE = @MSGMERGE@ +OBJEXT = @OBJEXT@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ +PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +POFILES = @POFILES@ +POSUB = @POSUB@ +PO_IN_DATADIR_FALSE = @PO_IN_DATADIR_FALSE@ +PO_IN_DATADIR_TRUE = @PO_IN_DATADIR_TRUE@ +PYTHON = @PYTHON@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_VERSION = @PYTHON_VERSION@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +SHELL_CFLAGS = @SHELL_CFLAGS@ +SHELL_LIBS = @SHELL_LIBS@ +STRIP = @STRIP@ +SUCROSE_VERSION = @SUCROSE_VERSION@ +USE_NLS = @USE_NLS@ +VERSION = @VERSION@ +XGETTEXT = @XGETTEXT@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_CC = @ac_ct_CC@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build_alias = @build_alias@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host_alias = @host_alias@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +intltool__v_merge_options_ = @intltool__v_merge_options_@ +intltool__v_merge_options_0 = @intltool__v_merge_options_0@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +sugardir = $(pythondir)/jarabe/frame +sugar_PYTHON = \ + __init__.py \ + activitiestray.py \ + clipboard.py \ + clipboardicon.py \ + clipboardmenu.py \ + clipboardobject.py \ + clipboardpanelwindow.py \ + clipboardtray.py \ + devicestray.py \ + frameinvoker.py \ + friendstray.py \ + eventarea.py \ + frame.py \ + notification.py \ + framewindow.py \ + zoomtoolbar.py + +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/jarabe/frame/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/frame/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): +install-sugarPYTHON: $(sugar_PYTHON) + @$(NORMAL_INSTALL) + test -z "$(sugardir)" || $(MKDIR_P) "$(DESTDIR)$(sugardir)" + @list='$(sugar_PYTHON)'; dlist=; list2=; test -n "$(sugardir)" || list=; \ + for p in $$list; do \ + if test -f "$$p"; then b=; else b="$(srcdir)/"; fi; \ + if test -f $$b$$p; then \ + $(am__strip_dir) \ + dlist="$$dlist $$f"; \ + list2="$$list2 $$b$$p"; \ + else :; fi; \ + done; \ + for file in $$list2; do echo $$file; done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(sugardir)'"; \ + $(INSTALL_DATA) $$files "$(DESTDIR)$(sugardir)" || exit $$?; \ + done || exit $$?; \ + if test -n "$$dlist"; then \ + $(am__py_compile) --destdir "$(DESTDIR)" \ + --basedir "$(sugardir)" $$dlist; \ + else :; fi + +uninstall-sugarPYTHON: + @$(NORMAL_UNINSTALL) + @list='$(sugar_PYTHON)'; test -n "$(sugardir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + test -n "$$files" || exit 0; \ + dir='$(DESTDIR)$(sugardir)'; \ + filesc=`echo "$$files" | sed 's|$$|c|'`; \ + fileso=`echo "$$files" | sed 's|$$|o|'`; \ + st=0; \ + for files in "$$files" "$$filesc" "$$fileso"; do \ + $(am__uninstall_files_from_dir) || st=$$?; \ + done; \ + exit $$st +tags: TAGS +TAGS: + +ctags: CTAGS +CTAGS: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile +installdirs: + for dir in "$(DESTDIR)$(sugardir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-sugarPYTHON + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-sugarPYTHON + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-generic distclean \ + distclean-generic distdir dvi dvi-am html html-am info info-am \ + install install-am install-data install-data-am install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-pdf install-pdf-am install-ps install-ps-am \ + install-strip install-sugarPYTHON installcheck installcheck-am \ + installdirs maintainer-clean maintainer-clean-generic \ + mostlyclean mostlyclean-generic pdf pdf-am ps ps-am uninstall \ + uninstall-am uninstall-sugarPYTHON + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/src/jarabe/frame/__init__.py b/src/jarabe/frame/__init__.py new file mode 100644 index 0000000..b3e4b80 --- /dev/null +++ b/src/jarabe/frame/__init__.py @@ -0,0 +1,27 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from jarabe.frame.frame import Frame + + +_view = None + + +def get_view(): + global _view + if not _view: + _view = Frame() + return _view diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py new file mode 100644 index 0000000..9590bce --- /dev/null +++ b/src/jarabe/frame/activitiestray.py @@ -0,0 +1,769 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2010 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import tempfile +import os + +import gobject +import gconf +import gio +import glib +import gtk + +from sugar.graphics import style +from sugar.graphics.tray import HTray +from sugar.graphics.xocolor import XoColor +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.icon import Icon, get_icon_file_name +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.datastore import datastore +from sugar import mime +from sugar import env + +from jarabe.model import shell +from jarabe.model import invites +from jarabe.model import bundleregistry +from jarabe.model import filetransfer +from jarabe.view.palettes import JournalPalette, CurrentActivityPalette +from jarabe.view.pulsingicon import PulsingIcon +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.frame.notification import NotificationIcon +import jarabe.frame + + +class ActivityButton(RadioToolButton): + def __init__(self, home_activity, group): + RadioToolButton.__init__(self, group=group) + + self.set_palette_invoker(FrameWidgetInvoker(self)) + self.palette_invoker.cache_palette = False + + self._home_activity = home_activity + self._notify_launch_hid = None + + self._icon = PulsingIcon() + self._icon.props.base_color = home_activity.get_icon_color() + self._icon.props.pulse_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TOOLBAR_GREY.get_svg())) + if home_activity.get_icon_path(): + self._icon.props.file = home_activity.get_icon_path() + else: + self._icon.props.icon_name = 'image-missing' + self.set_icon_widget(self._icon) + self._icon.show() + + if home_activity.props.launch_status == shell.Activity.LAUNCHING: + self._icon.props.pulsing = True + self._notify_launch_hid = home_activity.connect( \ + 'notify::launch-status', self.__notify_launch_status_cb) + elif home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() + + def create_palette(self): + if self._home_activity.is_journal(): + palette = JournalPalette(self._home_activity) + else: + palette = CurrentActivityPalette(self._home_activity) + palette.set_group_id('frame') + self.set_palette(palette) + + def _on_failed_launch(self): + # TODO http://bugs.sugarlabs.org/ticket/2007 + pass + + def __notify_launch_status_cb(self, home_activity, pspec): + home_activity.disconnect(self._notify_launch_hid) + self._notify_launch_hid = None + if home_activity.props.launch_status == shell.Activity.LAUNCH_FAILED: + self._on_failed_launch() + else: + self._icon.props.pulsing = False + + +class InviteButton(ToolButton): + """Invite to shared activity""" + + __gsignals__ = { + 'remove-invite': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, invite): + ToolButton.__init__(self) + + self._invite = invite + + self.connect('clicked', self.__clicked_cb) + self.connect('destroy', self.__destroy_cb) + + bundle_registry = bundleregistry.get_registry() + bundle = bundle_registry.get_bundle(invite.get_bundle_id()) + + self._icon = Icon() + self._icon.props.xo_color = invite.get_color() + if bundle is not None: + self._icon.props.file = bundle.get_icon() + else: + self._icon.props.icon_name = 'image-missing' + self.set_icon_widget(self._icon) + self._icon.show() + + palette = InvitePalette(invite) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + palette.connect('remove-invite', self.__remove_invite_cb) + self.set_palette(palette) + + self._notif_icon = NotificationIcon() + self._notif_icon.connect('button-release-event', + self.__button_release_event_cb) + + self._notif_icon.props.xo_color = invite.get_color() + if bundle is not None: + self._notif_icon.props.icon_filename = bundle.get_icon() + else: + self._notif_icon.props.icon_name = 'image-missing' + + frame = jarabe.frame.get_view() + frame.add_notification(self._notif_icon, gtk.CORNER_TOP_LEFT) + + def __button_release_event_cb(self, icon, event): + if self._notif_icon is not None: + frame = jarabe.frame.get_view() + frame.remove_notification(self._notif_icon) + self._notif_icon = None + self._invite.join() + self.emit('remove-invite') + + def __clicked_cb(self, button): + self.palette.popup(immediate=True, state=Palette.SECONDARY) + + def __remove_invite_cb(self, palette): + self.emit('remove-invite') + + def __destroy_cb(self, button): + if self._notif_icon is not None: + frame = jarabe.frame.get_view() + frame.remove_notification(self._notif_icon) + self._notif_icon = None + + +class InvitePalette(Palette): + """Palette for frame or notification icon for invites.""" + + __gsignals__ = { + 'remove-invite': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, invite): + Palette.__init__(self, '') + + self._invite = invite + + menu_item = MenuItem(_('Join'), icon_name='dialog-ok') + menu_item.connect('activate', self.__join_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__decline_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + bundle_id = invite.get_bundle_id() + + registry = bundleregistry.get_registry() + self._bundle = registry.get_bundle(bundle_id) + if self._bundle: + name = self._bundle.get_name() + else: + name = bundle_id + + self.set_primary_text(glib.markup_escape_text(name)) + + def __join_activate_cb(self, menu_item): + self._invite.join() + self.emit('remove-invite') + + def __decline_activate_cb(self, menu_item): + self.emit('remove-invite') + + +class ActivitiesTray(HTray): + def __init__(self): + HTray.__init__(self) + + self._buttons = {} + self._invite_to_item = {} + self._freeze_button_clicks = False + + self._home_model = shell.get_model() + self._home_model.connect('activity-added', self.__activity_added_cb) + self._home_model.connect('activity-removed', + self.__activity_removed_cb) + self._home_model.connect('active-activity-changed', + self.__activity_changed_cb) + self._home_model.connect('tabbing-activity-changed', + self.__tabbing_activity_changed_cb) + + self._invites = invites.get_instance() + for invite in self._invites: + self._add_invite(invite) + self._invites.connect('invite-added', self.__invite_added_cb) + self._invites.connect('invite-removed', self.__invite_removed_cb) + + filetransfer.new_file_transfer.connect(self.__new_file_transfer_cb) + + def __activity_added_cb(self, home_model, home_activity): + logging.debug('__activity_added_cb: %r', home_activity) + if self.get_children(): + group = self.get_children()[0] + else: + group = None + + button = ActivityButton(home_activity, group) + self.add_item(button) + self._buttons[home_activity] = button + button.connect('clicked', self.__activity_clicked_cb, home_activity) + button.show() + + def __activity_removed_cb(self, home_model, home_activity): + logging.debug('__activity_removed_cb: %r', home_activity) + button = self._buttons[home_activity] + self.remove_item(button) + del self._buttons[home_activity] + + def _activate_activity(self, home_activity): + button = self._buttons[home_activity] + self._freeze_button_clicks = True + button.props.active = True + self._freeze_button_clicks = False + + self.scroll_to_item(button) + # Redraw immediately. + # The widget may not be realized yet, and then there is no window. + if self.window: + self.window.process_updates(True) + + def __activity_changed_cb(self, home_model, home_activity): + logging.debug('__activity_changed_cb: %r', home_activity) + + # Only select the new activity, if there is no tabbing activity. + if home_model.get_tabbing_activity() is None: + self._activate_activity(home_activity) + + def __tabbing_activity_changed_cb(self, home_model, home_activity): + logging.debug('__tabbing_activity_changed_cb: %r', home_activity) + # If the tabbing_activity is set to None just do nothing. + # The active activity will be updated a bit later (and it will + # be set to the activity that is currently selected). + if home_activity is None: + return + + self._activate_activity(home_activity) + + def __activity_clicked_cb(self, button, home_activity): + if not self._freeze_button_clicks and button.props.active: + logging.debug('ActivitiesTray.__activity_clicked_cb') + window = home_activity.get_window() + if window: + window.activate(gtk.get_current_event_time()) + + def __remove_invite_cb(self, icon, invite): + self._invites.remove_invite(invite) + + def __invite_added_cb(self, invites_model, invite): + self._add_invite(invite) + + def __invite_removed_cb(self, invites_model, invite): + self._remove_invite(invite) + + def _add_invite(self, invite): + """Add an invite""" + item = InviteButton(invite) + item.connect('remove-invite', self.__remove_invite_cb, invite) + self.add_item(item) + item.show() + self._invite_to_item[invite] = item + + def _remove_invite(self, invite): + self.remove_item(self._invite_to_item[invite]) + self._invite_to_item[invite].destroy() + del self._invite_to_item[invite] + + def __new_file_transfer_cb(self, **kwargs): + file_transfer = kwargs['file_transfer'] + logging.debug('__new_file_transfer_cb %r', file_transfer) + + if isinstance(file_transfer, filetransfer.IncomingFileTransfer): + button = IncomingTransferButton(file_transfer) + elif isinstance(file_transfer, filetransfer.OutgoingFileTransfer): + button = OutgoingTransferButton(file_transfer) + + self.add_item(button) + button.show() + + +class BaseTransferButton(ToolButton): + """Button with a notification attached + """ + def __init__(self, file_transfer): + ToolButton.__init__(self) + + self.file_transfer = file_transfer + file_transfer.connect('notify::state', self.__notify_state_cb) + + icon = Icon() + self.props.icon_widget = icon + icon.show() + + self.notif_icon = NotificationIcon() + self.notif_icon.connect('button-release-event', + self.__button_release_event_cb) + + self.connect('clicked', self.__button_clicked_cb) + + def __button_release_event_cb(self, icon, event): + if self.notif_icon is not None: + frame = jarabe.frame.get_view() + frame.remove_notification(self.notif_icon) + self.notif_icon = None + + def __button_clicked_cb(self, button): + self.palette.popup(immediate=True, state=Palette.SECONDARY) + + def remove(self): + frame = jarabe.frame.get_view() + frame.remove_notification(self.notif_icon) + self.props.parent.remove(self) + + def __notify_state_cb(self, file_transfer, pspec): + logging.debug('_update state: %r %r', file_transfer.props.state, + file_transfer.reason_last_change) + if file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + if file_transfer.reason_last_change == \ + filetransfer.FT_REASON_LOCAL_STOPPED: + self.remove() + + +class IncomingTransferButton(BaseTransferButton): + """UI element representing an ongoing incoming file transfer + """ + def __init__(self, file_transfer): + BaseTransferButton.__init__(self, file_transfer) + + self._ds_object = datastore.create() + + file_transfer.connect('notify::state', self.__notify_state_cb) + file_transfer.connect('notify::transferred-bytes', + self.__notify_transferred_bytes_cb) + + icons = gio.content_type_get_icon(file_transfer.mime_type).props.names + icons.append('application-octet-stream') + for icon_name in icons: + icon_name = 'transfer-from-%s' % icon_name + file_name = get_icon_file_name(icon_name) + if file_name is not None: + self.props.icon_widget.props.icon_name = icon_name + self.notif_icon.props.icon_name = icon_name + break + + icon_color = file_transfer.buddy.props.color + self.props.icon_widget.props.xo_color = icon_color + self.notif_icon.props.xo_color = icon_color + + frame = jarabe.frame.get_view() + frame.add_notification(self.notif_icon, + gtk.CORNER_TOP_LEFT) + + def create_palette(self): + palette = IncomingTransferPalette(self.file_transfer) + palette.connect('dismiss-clicked', self.__dismiss_clicked_cb) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + return palette + + def __notify_state_cb(self, file_transfer, pspec): + if file_transfer.props.state == filetransfer.FT_STATE_OPEN: + logging.debug('__notify_state_cb OPEN') + self._ds_object.metadata['title'] = file_transfer.title + self._ds_object.metadata['description'] = file_transfer.description + self._ds_object.metadata['progress'] = '0' + self._ds_object.metadata['keep'] = '0' + self._ds_object.metadata['buddies'] = '' + self._ds_object.metadata['preview'] = '' + self._ds_object.metadata['icon-color'] = \ + file_transfer.buddy.props.color.to_string() + self._ds_object.metadata['mime_type'] = file_transfer.mime_type + elif file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: + logging.debug('__notify_state_cb COMPLETED') + self._ds_object.metadata['progress'] = '100' + self._ds_object.file_path = file_transfer.destination_path + datastore.write(self._ds_object, transfer_ownership=True, + reply_handler=self.__reply_handler_cb, + error_handler=self.__error_handler_cb) + elif file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + logging.debug('__notify_state_cb CANCELLED') + object_id = self._ds_object.object_id + if object_id is not None: + self._ds_object.destroy() + datastore.delete(object_id) + self._ds_object = None + + def __notify_transferred_bytes_cb(self, file_transfer, pspec): + progress = file_transfer.props.transferred_bytes / \ + file_transfer.file_size + self._ds_object.metadata['progress'] = str(progress * 100) + datastore.write(self._ds_object, update_mtime=False) + + def __reply_handler_cb(self): + logging.debug('__reply_handler_cb %r', self._ds_object.object_id) + + def __error_handler_cb(self, error): + logging.debug('__error_handler_cb %r %s', self._ds_object.object_id, + error) + + def __dismiss_clicked_cb(self, palette): + self.remove() + + +class OutgoingTransferButton(BaseTransferButton): + """UI element representing an ongoing outgoing file transfer + """ + def __init__(self, file_transfer): + BaseTransferButton.__init__(self, file_transfer) + + icons = gio.content_type_get_icon(file_transfer.mime_type).props.names + icons.append('application-octet-stream') + for icon_name in icons: + icon_name = 'transfer-to-%s' % icon_name + file_name = get_icon_file_name(icon_name) + if file_name is not None: + self.props.icon_widget.props.icon_name = icon_name + self.notif_icon.props.icon_name = icon_name + break + + client = gconf.client_get_default() + icon_color = XoColor(client.get_string('/desktop/sugar/user/color')) + self.props.icon_widget.props.xo_color = icon_color + self.notif_icon.props.xo_color = icon_color + + frame = jarabe.frame.get_view() + frame.add_notification(self.notif_icon, + gtk.CORNER_TOP_LEFT) + + def create_palette(self): + palette = OutgoingTransferPalette(self.file_transfer) + palette.connect('dismiss-clicked', self.__dismiss_clicked_cb) + palette.props.invoker = FrameWidgetInvoker(self) + palette.set_group_id('frame') + return palette + + def __dismiss_clicked_cb(self, palette): + self.remove() + + +class BaseTransferPalette(Palette): + """Base palette class for frame or notification icon for file transfers + """ + __gtype_name__ = 'SugarBaseTransferPalette' + + __gsignals__ = { + 'dismiss-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, file_transfer): + Palette.__init__(self, glib.markup_escape_text(file_transfer.title)) + + self.file_transfer = file_transfer + + self.progress_bar = None + self.progress_label = None + self._notify_transferred_bytes_handler = None + + self.connect('popup', self.__popup_cb) + self.connect('popdown', self.__popdown_cb) + + def __popup_cb(self, palette): + self.update_progress() + self._notify_transferred_bytes_handler = \ + self.file_transfer.connect('notify::transferred_bytes', + self.__notify_transferred_bytes_cb) + + def __popdown_cb(self, palette): + if self._notify_transferred_bytes_handler is not None: + self.file_transfer.disconnect( + self._notify_transferred_bytes_handler) + self._notify_transferred_bytes_handler = None + + def __notify_transferred_bytes_cb(self, file_transfer, pspec): + self.update_progress() + + def _format_size(self, size): + if size < 1024: + return _('%dB') % size + elif size < 1048576: + return _('%dKB') % (size / 1024) + else: + return _('%dMB') % (size / 1048576) + + def update_progress(self): + logging.debug('update_progress: %r', + self.file_transfer.props.transferred_bytes) + + if self.progress_bar is None: + return + + self.progress_bar.props.fraction = \ + self.file_transfer.props.transferred_bytes / \ + float(self.file_transfer.file_size) + logging.debug('update_progress: %r', self.progress_bar.props.fraction) + + transferred = self._format_size( + self.file_transfer.props.transferred_bytes) + total = self._format_size(self.file_transfer.file_size) + # TRANS: file transfer, bytes transferred, e.g. 128 of 1024 + self.progress_label.props.label = _('%s of %s') % (transferred, total) + + +class IncomingTransferPalette(BaseTransferPalette): + """Palette for frame or notification icon for incoming file transfers + """ + __gtype_name__ = 'SugarIncomingTransferPalette' + + def __init__(self, file_transfer): + BaseTransferPalette.__init__(self, file_transfer) + + self.file_transfer.connect('notify::state', self.__notify_state_cb) + + nick = str(self.file_transfer.buddy.props.nick) + label = glib.markup_escape_text(_('Transfer from %s') % (nick,)) + self.props.secondary_text = label + + self._update() + + def __notify_state_cb(self, file_transfer, pspec): + self._update() + + def _update(self): + logging.debug('_update state: %r', self.file_transfer.props.state) + if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING: + menu_item = MenuItem(_('Accept'), icon_name='dialog-ok') + menu_item.connect('activate', self.__accept_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + menu_item = MenuItem(_('Decline'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__decline_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + if self.file_transfer.description: + label = gtk.Label(self.file_transfer.description) + vbox.add(label) + label.show() + + mime_type = self.file_transfer.mime_type + type_description = mime.get_mime_description(mime_type) + + size = self._format_size(self.file_transfer.file_size) + label = gtk.Label('%s (%s)' % (size, type_description)) + vbox.add(label) + label.show() + + elif self.file_transfer.props.state in \ + [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self.progress_bar = gtk.ProgressBar() + vbox.add(self.progress_bar) + self.progress_bar.show() + + self.progress_label = gtk.Label('') + vbox.add(self.progress_label) + self.progress_label.show() + + self.update_progress() + + elif self.file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Dismiss'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__dismiss_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + self.update_progress() + elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: + + for item in self.menu.get_children(): + self.menu.remove(item) + + if self.file_transfer.reason_last_change == \ + filetransfer.FT_REASON_REMOTE_STOPPED: + menu_item = MenuItem(_('Dismiss'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__dismiss_activate_cb) + self.menu.append(menu_item) + menu_item.show() + text = _('The other participant canceled the file transfer') + label = gtk.Label(text) + self.set_content(label) + label.show() + + def __accept_activate_cb(self, menu_item): + #TODO: figure out the best place to get rid of that temp file + extension = mime.get_primary_extension(self.file_transfer.mime_type) + if extension is None: + extension = '.bin' + fd, file_path = tempfile.mkstemp(suffix=extension, + prefix=self._sanitize(self.file_transfer.title), + dir=os.path.join(env.get_profile_path(), 'data')) + os.close(fd) + os.unlink(file_path) + + self.file_transfer.accept(file_path) + + def _sanitize(self, file_name): + file_name = file_name.replace('/', '_') + file_name = file_name.replace('.', '_') + file_name = file_name.replace('?', '_') + return file_name + + def __decline_activate_cb(self, menu_item): + self.file_transfer.cancel() + + def __cancel_activate_cb(self, menu_item): + self.file_transfer.cancel() + + def __dismiss_activate_cb(self, menu_item): + self.emit('dismiss-clicked') + + +class OutgoingTransferPalette(BaseTransferPalette): + """Palette for frame or notification icon for outgoing file transfers + """ + __gtype_name__ = 'SugarOutgoingTransferPalette' + + def __init__(self, file_transfer): + BaseTransferPalette.__init__(self, file_transfer) + + self.progress_bar = None + self.progress_label = None + + self.file_transfer.connect('notify::state', self.__notify_state_cb) + + nick = str(file_transfer.buddy.props.nick) + label = glib.markup_escape_text(_('Transfer to %s') % (nick,)) + self.props.secondary_text = label + + self._update() + + def __notify_state_cb(self, file_transfer, pspec): + self._update() + + def _update(self): + new_state = self.file_transfer.props.state + logging.debug('_update state: %r', new_state) + if new_state == filetransfer.FT_STATE_PENDING: + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + if self.file_transfer.description: + label = gtk.Label(self.file_transfer.description) + vbox.add(label) + label.show() + + mime_type = self.file_transfer.mime_type + type_description = mime.get_mime_description(mime_type) + + size = self._format_size(self.file_transfer.file_size) + label = gtk.Label('%s (%s)' % (size, type_description)) + vbox.add(label) + label.show() + + elif new_state in [filetransfer.FT_STATE_ACCEPTED, + filetransfer.FT_STATE_OPEN]: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__cancel_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + vbox = gtk.VBox() + self.set_content(vbox) + vbox.show() + + self.progress_bar = gtk.ProgressBar() + vbox.add(self.progress_bar) + self.progress_bar.show() + + self.progress_label = gtk.Label('') + vbox.add(self.progress_label) + self.progress_label.show() + + self.update_progress() + + elif new_state in [filetransfer.FT_STATE_COMPLETED, + filetransfer.FT_STATE_CANCELLED]: + + for item in self.menu.get_children(): + self.menu.remove(item) + + menu_item = MenuItem(_('Dismiss'), icon_name='dialog-cancel') + menu_item.connect('activate', self.__dismiss_activate_cb) + self.menu.append(menu_item) + menu_item.show() + + self.update_progress() + + def __cancel_activate_cb(self, menu_item): + self.file_transfer.cancel() + + def __dismiss_activate_cb(self, menu_item): + self.emit('dismiss-clicked') diff --git a/src/jarabe/frame/clipboard.py b/src/jarabe/frame/clipboard.py new file mode 100644 index 0000000..a09ac5b --- /dev/null +++ b/src/jarabe/frame/clipboard.py @@ -0,0 +1,178 @@ +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import os +import shutil +import urlparse +import tempfile + +import gobject +import gtk + +from sugar import mime + +from jarabe.frame.clipboardobject import ClipboardObject, Format + + +_instance = None + + +class Clipboard(gobject.GObject): + + __gsignals__ = { + 'object-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'object-deleted': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([long])), + 'object-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([long])), + 'object-state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._objects = {} + self._next_id = 0 + + def _get_next_object_id(self): + self._next_id += 1 + return self._next_id + + def add_object(self, name, data_hash=None): + """ Add a object to the clipboard + + Keyword arguments: + name -- object name + data_hash -- hash to check if the object is already + in the clipboard, generated with hash() + over the data to be added + + Return: object_id or None if the object is not added + + """ + logging.debug('Clipboard.add_object: hash %s', data_hash) + if data_hash is None: + object_id = self._get_next_object_id() + else: + object_id = data_hash + if object_id in self._objects: + logging.debug('Clipboard.add_object: object already in clipboard,' + ' selecting previous entry instead') + self.emit('object-selected', object_id) + return None + self._objects[object_id] = ClipboardObject(object_id, name) + self.emit('object-added', self._objects[object_id]) + return object_id + + def add_object_format(self, object_id, format_type, data, on_disk): + logging.debug('Clipboard.add_object_format') + cb_object = self._objects[object_id] + + if format_type == 'XdndDirectSave0': + format_ = Format('text/uri-list', data + '\r\n', on_disk) + format_.owns_disk_data = True + cb_object.add_format(format_) + elif on_disk and cb_object.get_percent() == 100: + new_uri = self._copy_file(data) + cb_object.add_format(Format(format_type, new_uri, on_disk)) + logging.debug('Added format of type ' + format_type + + ' with path at ' + new_uri) + else: + cb_object.add_format(Format(format_type, data, on_disk)) + logging.debug('Added in-memory format of type %s.', format_type) + + self.emit('object-state-changed', cb_object) + + def delete_object(self, object_id): + cb_object = self._objects.pop(object_id) + cb_object.destroy() + if not self._objects: + gtk_clipboard = gtk.Clipboard() + gtk_clipboard.clear() + self.emit('object-deleted', object_id) + logging.debug('Deleted object with object_id %r', object_id) + + def set_object_percent(self, object_id, percent): + cb_object = self._objects[object_id] + if percent < 0 or percent > 100: + raise ValueError('invalid percentage') + if cb_object.get_percent() > percent: + raise ValueError('invalid percentage; less than current percent') + if cb_object.get_percent() == percent: + # ignore setting same percentage + return + + cb_object.set_percent(percent) + + if percent == 100: + self._process_object(cb_object) + + self.emit('object-state-changed', cb_object) + + def _process_object(self, cb_object): + formats = cb_object.get_formats() + for format_name, format_ in formats.iteritems(): + if format_.is_on_disk() and not format_.owns_disk_data: + new_uri = self._copy_file(format_.get_data()) + format_.set_data(new_uri) + + # Add a text/plain format to objects that are text but lack it + if 'text/plain' not in formats.keys(): + if 'UTF8_STRING' in formats.keys(): + self.add_object_format( + cb_object.get_id(), 'text/plain', + data=formats['UTF8_STRING'].get_data(), on_disk=False) + elif 'text/unicode' in formats.keys(): + self.add_object_format( + cb_object.get_id(), 'text/plain', + data=formats['UTF8_STRING'].get_data(), on_disk=False) + + def get_object(self, object_id): + logging.debug('Clipboard.get_object') + return self._objects[object_id] + + def get_object_data(self, object_id, format_type): + logging.debug('Clipboard.get_object_data') + cb_object = self._objects[object_id] + format_ = cb_object.get_formats()[format_type] + return format_ + + def _copy_file(self, original_uri): + uri = urlparse.urlparse(original_uri) + path = uri.path # pylint: disable=E1101 + directory_, file_name = os.path.split(path) + + root, ext = os.path.splitext(file_name) + if not ext or ext == '.': + mime_type = mime.get_for_file(path) + ext = '.' + mime.get_primary_extension(mime_type) + + f_, new_file_path = tempfile.mkstemp(ext, root) + del f_ + shutil.copyfile(path, new_file_path) + os.chmod(new_file_path, 0644) + + return 'file://' + new_file_path + + +def get_instance(): + global _instance + if not _instance: + _instance = Clipboard() + return _instance diff --git a/src/jarabe/frame/clipboardicon.py b/src/jarabe/frame/clipboardicon.py new file mode 100644 index 0000000..315cdaa --- /dev/null +++ b/src/jarabe/frame/clipboardicon.py @@ -0,0 +1,170 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import gconf + +import gtk + +from sugar.graphics.radiotoolbutton import RadioToolButton +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.graphics import style + +from jarabe.frame import clipboard +from jarabe.frame.clipboardmenu import ClipboardMenu +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.frame.notification import NotificationIcon +import jarabe.frame + + +class ClipboardIcon(RadioToolButton): + __gtype_name__ = 'SugarClipboardIcon' + + def __init__(self, cb_object, group): + RadioToolButton.__init__(self, group=group) + + self.props.palette_invoker = FrameWidgetInvoker(self) + + self._cb_object = cb_object + self.owns_clipboard = False + self.props.sensitive = False + self.props.active = False + self._notif_icon = None + self._current_percent = None + + self._icon = Icon() + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + self._icon.props.xo_color = color + self.set_icon_widget(self._icon) + self._icon.show() + + cb_service = clipboard.get_instance() + cb_service.connect('object-state-changed', + self._object_state_changed_cb) + cb_service.connect('object-selected', self._object_selected_cb) + + child = self.get_child() + child.connect('drag_data_get', self._drag_data_get_cb) + self.connect('notify::active', self._notify_active_cb) + + def create_palette(self): + palette = ClipboardMenu(self._cb_object) + palette.set_group_id('frame') + return palette + + def get_object_id(self): + return self._cb_object.get_id() + + def _drag_data_get_cb(self, widget, context, selection, target_type, + event_time): + logging.debug('_drag_data_get_cb: requested target %s', + selection.target) + data = self._cb_object.get_formats()[selection.target].get_data() + selection.set(selection.target, 8, data) + + def _put_in_clipboard(self): + logging.debug('ClipboardIcon._put_in_clipboard') + + if self._cb_object.get_percent() < 100: + raise ValueError('Object is not complete, cannot be put into the' + ' clipboard.') + + targets = self._get_targets() + if targets: + x_clipboard = gtk.Clipboard() + if not x_clipboard.set_with_data(targets, + self._clipboard_data_get_cb, + self._clipboard_clear_cb, + targets): + logging.error('GtkClipboard.set_with_data failed!') + else: + self.owns_clipboard = True + + def _clipboard_data_get_cb(self, x_clipboard, selection, info, targets): + if not selection.target in [target[0] for target in targets]: + logging.warning('ClipboardIcon._clipboard_data_get_cb: asked %s' \ + ' but only have %r.', selection.target, targets) + return + data = self._cb_object.get_formats()[selection.target].get_data() + selection.set(selection.target, 8, data) + + def _clipboard_clear_cb(self, x_clipboard, targets): + logging.debug('ClipboardIcon._clipboard_clear_cb') + self.owns_clipboard = False + + def _object_state_changed_cb(self, cb_service, cb_object): + if cb_object != self._cb_object: + return + + if cb_object.get_icon(): + self._icon.props.icon_name = cb_object.get_icon() + else: + self._icon.props.icon_name = 'application-octet-stream' + + child = self.get_child() + child.connect('drag-begin', self._drag_begin_cb) + child.drag_source_set(gtk.gdk.BUTTON1_MASK, + self._get_targets(), + gtk.gdk.ACTION_COPY) + + if cb_object.get_percent() == 100: + self.props.sensitive = True + + # Clipboard object became complete. Make it the active one. + if self._current_percent < 100 and cb_object.get_percent() == 100: + self.props.active = True + self.show_notification() + + self._current_percent = cb_object.get_percent() + + def _object_selected_cb(self, cb_service, object_id): + if object_id != self._cb_object.get_id(): + return + self.props.active = True + self.show_notification() + logging.debug('ClipboardIcon: %r was selected', object_id) + + def show_notification(self): + self._notif_icon = NotificationIcon() + self._notif_icon.props.icon_name = self._icon.props.icon_name + self._notif_icon.props.xo_color = \ + XoColor('%s,%s' % (self._icon.props.stroke_color, + self._icon.props.fill_color)) + frame = jarabe.frame.get_view() + frame.add_notification(self._notif_icon, gtk.CORNER_BOTTOM_LEFT) + + def _drag_begin_cb(self, widget, context): + # TODO: We should get the pixbuf from the icon, with colors, etc. + icon_theme = gtk.icon_theme_get_default() + pixbuf = icon_theme.load_icon(self._icon.props.icon_name, + style.STANDARD_ICON_SIZE, 0) + context.set_icon_pixbuf(pixbuf, hot_x=pixbuf.props.width / 2, + hot_y=pixbuf.props.height / 2) + + def _notify_active_cb(self, widget, pspec): + if self.props.active: + self._put_in_clipboard() + else: + self.owns_clipboard = False + + def _get_targets(self): + targets = [] + for format_type in self._cb_object.get_formats().keys(): + targets.append((format_type, 0, 0)) + return targets diff --git a/src/jarabe/frame/clipboardmenu.py b/src/jarabe/frame/clipboardmenu.py new file mode 100644 index 0000000..4c077d9 --- /dev/null +++ b/src/jarabe/frame/clipboardmenu.py @@ -0,0 +1,256 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import tempfile +import urlparse +import os +import logging +import gconf +import glib + +import gtk + +from sugar.graphics.palette import Palette +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor +from sugar.datastore import datastore +from sugar import mime +from sugar import env +from sugar.activity.i18n import pgettext + +from jarabe.frame import clipboard +from jarabe.journal import misc +from jarabe.model import bundleregistry + + +class ClipboardMenu(Palette): + + def __init__(self, cb_object): + Palette.__init__(self, text_maxlen=100) + + self._cb_object = cb_object + + self.set_group_id('frame') + + cb_service = clipboard.get_instance() + cb_service.connect('object-state-changed', + self._object_state_changed_cb) + + self._progress_bar = None + + self._remove_item = MenuItem(pgettext('Clipboard', 'Remove'), + 'list-remove') + self._remove_item.connect('activate', self._remove_item_activate_cb) + self.menu.append(self._remove_item) + self._remove_item.show() + + self._open_item = MenuItem(_('Open'), 'zoom-activity') + self._open_item.connect('activate', self._open_item_activate_cb) + self.menu.append(self._open_item) + self._open_item.show() + + self._journal_item = MenuItem(_('Keep')) + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + icon = Icon(icon_name='document-save', icon_size=gtk.ICON_SIZE_MENU, + xo_color=color) + self._journal_item.set_image(icon) + + self._journal_item.connect('activate', self._journal_item_activate_cb) + self.menu.append(self._journal_item) + self._journal_item.show() + + self._update() + + def _update_open_submenu(self): + activities = self._get_activities() + logging.debug('_update_open_submenu: %r', activities) + child = self._open_item.get_child() + if activities is None or len(activities) <= 1: + child.set_text(_('Open')) + if self._open_item.get_submenu() is not None: + self._open_item.remove_submenu() + return + + child.set_text(_('Open with')) + submenu = self._open_item.get_submenu() + if submenu is None: + submenu = gtk.Menu() + self._open_item.set_submenu(submenu) + submenu.show() + else: + for item in submenu.get_children(): + submenu.remove(item) + + for service_name in activities: + registry = bundleregistry.get_registry() + activity_info = registry.get_bundle(service_name) + + if not activity_info: + logging.warning('Activity %s is unknown.', service_name) + + item = gtk.MenuItem(activity_info.get_name()) + item.connect('activate', self._open_submenu_item_activate_cb, + service_name) + submenu.append(item) + item.show() + + def _update_items_visibility(self): + activities = self._get_activities() + installable = self._cb_object.is_bundle() + percent = self._cb_object.get_percent() + + if percent == 100 and (activities or installable): + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = True + self._journal_item.props.sensitive = True + elif percent == 100 and (not activities and not installable): + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = False + self._journal_item.props.sensitive = True + else: + self._remove_item.props.sensitive = True + self._open_item.props.sensitive = False + self._journal_item.props.sensitive = False + + self._update_progress_bar() + + def _get_activities(self): + mime_type = self._cb_object.get_mime_type() + if not mime_type: + return '' + + registry = bundleregistry.get_registry() + activities = registry.get_activities_for_type(mime_type) + if activities: + return [info.get_bundle_id() for info in activities] + else: + return '' + + def _update_progress_bar(self): + percent = self._cb_object.get_percent() + if percent == 100.0: + if self._progress_bar: + self._progress_bar = None + self.set_content(None) + else: + if self._progress_bar is None: + self._progress_bar = gtk.ProgressBar() + self._progress_bar.show() + self.set_content(self._progress_bar) + + self._progress_bar.props.fraction = percent / 100.0 + self._progress_bar.props.text = '%.2f %%' % percent + + def _object_state_changed_cb(self, cb_service, cb_object): + if cb_object != self._cb_object: + return + self._update() + + def _update(self): + name = self._cb_object.get_name() + self.props.primary_text = glib.markup_escape_text(name) + preview = self._cb_object.get_preview() + if preview: + self.props.secondary_text = glib.markup_escape_text(preview) + self._update_progress_bar() + self._update_items_visibility() + self._update_open_submenu() + + def _open_item_activate_cb(self, menu_item): + logging.debug('_open_item_activate_cb') + percent = self._cb_object.get_percent() + if percent < 100 or menu_item.get_submenu() is not None: + return + jobject = self._copy_to_journal() + misc.resume(jobject.metadata, self._get_activities()[0]) + jobject.destroy() + + def _open_submenu_item_activate_cb(self, menu_item, service_name): + logging.debug('_open_submenu_item_activate_cb') + percent = self._cb_object.get_percent() + if percent < 100: + return + jobject = self._copy_to_journal() + misc.resume(jobject.metadata, service_name) + jobject.destroy() + + def _remove_item_activate_cb(self, menu_item): + cb_service = clipboard.get_instance() + cb_service.delete_object(self._cb_object.get_id()) + + def _journal_item_activate_cb(self, menu_item): + logging.debug('_journal_item_activate_cb') + jobject = self._copy_to_journal() + jobject.destroy() + + def _write_to_temp_file(self, data): + tmp_dir = os.path.join(env.get_profile_path(), 'data') + f, file_path = tempfile.mkstemp(dir=tmp_dir) + try: + os.write(f, data) + finally: + os.close(f) + return file_path + + def _copy_to_journal(self): + formats = self._cb_object.get_formats().keys() + most_significant_mime_type = mime.choose_most_significant(formats) + format_ = self._cb_object.get_formats()[most_significant_mime_type] + + transfer_ownership = False + if most_significant_mime_type == 'text/uri-list': + uris = mime.split_uri_list(format_.get_data()) + if len(uris) == 1 and uris[0].startswith('file://'): + parsed_url = urlparse.urlparse(uris[0]) + file_path = parsed_url.path # pylint: disable=E1101 + transfer_ownership = False + mime_type = mime.get_for_file(file_path) + else: + file_path = self._write_to_temp_file(format_.get_data()) + transfer_ownership = True + mime_type = 'text/uri-list' + else: + if format_.is_on_disk(): + parsed_url = urlparse.urlparse(format_.get_data()) + file_path = parsed_url.path # pylint: disable=E1101 + transfer_ownership = False + mime_type = mime.get_for_file(file_path) + else: + file_path = self._write_to_temp_file(format_.get_data()) + transfer_ownership = True + sniffed_mime_type = mime.get_for_file(file_path) + if sniffed_mime_type == 'application/octet-stream': + mime_type = most_significant_mime_type + else: + mime_type = sniffed_mime_type + + jobject = datastore.create() + jobject.metadata['title'] = self._cb_object.get_name() + jobject.metadata['keep'] = '0' + jobject.metadata['buddies'] = '' + jobject.metadata['preview'] = '' + client = gconf.client_get_default() + color = client.get_string('/desktop/sugar/user/color') + jobject.metadata['icon-color'] = color + jobject.metadata['mime_type'] = mime_type + jobject.file_path = file_path + + datastore.write(jobject, transfer_ownership=transfer_ownership) + + return jobject diff --git a/src/jarabe/frame/clipboardobject.py b/src/jarabe/frame/clipboardobject.py new file mode 100644 index 0000000..407af2f --- /dev/null +++ b/src/jarabe/frame/clipboardobject.py @@ -0,0 +1,147 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import urlparse +import gio +import gtk + +from gettext import gettext as _ +from sugar import mime +from sugar.bundle.activitybundle import ActivityBundle + + +class ClipboardObject(object): + + def __init__(self, object_path, name): + self._id = object_path + self._name = name + self._percent = 0 + self._formats = {} + + def destroy(self): + for format_ in self._formats.itervalues(): + format_.destroy() + + def get_id(self): + return self._id + + def get_name(self): + name = self._name + if not name: + mime_type = mime.get_mime_description(self.get_mime_type()) + + if not mime_type: + mime_type = 'Data' + name = _('%s clipping') % mime_type + + return name + + def get_icon(self): + mime_type = self.get_mime_type() + + generic_types = mime.get_all_generic_types() + for generic_type in generic_types: + if mime_type in generic_type.mime_types: + return generic_type.icon + + icons = gio.content_type_get_icon(mime_type) + icon_name = None + if icons is not None: + icon_theme = gtk.icon_theme_get_default() + for icon_name in icons.props.names: + icon_info = icon_theme.lookup_icon(icon_name, + gtk.ICON_SIZE_LARGE_TOOLBAR, 0) + if icon_info is not None: + icon_info.free() + return icon_name + + return 'application-octet-stream' + + def get_preview(self): + for mime_type in ['text/plain']: + if mime_type in self._formats: + return self._formats[mime_type].get_data() + return '' + + def is_bundle(self): + # A bundle will have only one format. + if not self._formats: + return False + else: + return self._formats.keys()[0] in [ActivityBundle.MIME_TYPE, + ActivityBundle.DEPRECATED_MIME_TYPE] + + def get_percent(self): + return self._percent + + def set_percent(self, percent): + self._percent = percent + + def add_format(self, format_): + self._formats[format_.get_type()] = format_ + + def get_formats(self): + return self._formats + + def get_mime_type(self): + if not self._formats: + return '' + + format_ = mime.choose_most_significant(self._formats.keys()) + if format_ == 'text/uri-list': + data = self._formats['text/uri-list'].get_data() + uri = urlparse.urlparse(mime.split_uri_list(data)[0], 'file') + scheme = uri.scheme # pylint: disable=E1101 + if scheme == 'file': + path = uri.path # pylint: disable=E1101 + if os.path.exists(path): + format_ = mime.get_for_file(path) + else: + format_ = mime.get_from_file_name(path) + logging.debug('Chose %r!', format_) + + return format_ + + +class Format(object): + + def __init__(self, mime_type, data, on_disk): + self.owns_disk_data = False + + self._type = mime_type + self._data = data + self._on_disk = on_disk + + def destroy(self): + if self._on_disk: + uri = urlparse.urlparse(self._data) + path = uri.path # pylint: disable=E1101 + if os.path.isfile(path): + os.remove(path) + + def get_type(self): + return self._type + + def get_data(self): + return self._data + + def set_data(self, data): + self._data = data + + def is_on_disk(self): + return self._on_disk diff --git a/src/jarabe/frame/clipboardpanelwindow.py b/src/jarabe/frame/clipboardpanelwindow.py new file mode 100644 index 0000000..6811c0d --- /dev/null +++ b/src/jarabe/frame/clipboardpanelwindow.py @@ -0,0 +1,140 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from urlparse import urlparse +import hashlib + +import gtk + +from jarabe.frame.framewindow import FrameWindow +from jarabe.frame.clipboardtray import ClipboardTray + +from jarabe.frame import clipboard + + +class ClipboardPanelWindow(FrameWindow): + def __init__(self, frame, orientation): + FrameWindow.__init__(self, orientation) + + self._frame = frame + + # Listening for new clipboard objects + # NOTE: we need to keep a reference to gtk.Clipboard in order to keep + # listening to it. + self._clipboard = gtk.Clipboard() + self._clipboard.connect('owner-change', self._owner_change_cb) + + self._clipboard_tray = ClipboardTray() + self._clipboard_tray.show() + self.append(self._clipboard_tray) + + # Receiving dnd drops + self.drag_dest_set(0, [], 0) + self.connect('drag_motion', self._clipboard_tray.drag_motion_cb) + self.connect('drag_leave', self._clipboard_tray.drag_leave_cb) + self.connect('drag_drop', self._clipboard_tray.drag_drop_cb) + self.connect('drag_data_received', + self._clipboard_tray.drag_data_received_cb) + + def _owner_change_cb(self, x_clipboard, event): + logging.debug('owner_change_cb') + + if self._clipboard_tray.owns_clipboard(): + return + + cb_service = clipboard.get_instance() + + targets = x_clipboard.wait_for_targets() + cb_selections = [] + if targets is None: + return + + target_is_uri = False + for target in targets: + if target not in ('TIMESTAMP', 'TARGETS', + 'MULTIPLE', 'SAVE_TARGETS'): + logging.debug('Asking for target %s.', target) + if target == 'text/uri-list': + target_is_uri = True + + selection = x_clipboard.wait_for_contents(target) + if not selection: + logging.warning('no data for selection target %s.', target) + continue + cb_selections.append(selection) + + if target_is_uri: + uri = selection.data + filename = uri[len('file://'):].strip() + md5 = self._md5_for_file(filename) + data_hash = hash(md5) + else: + data_hash = hash(selection.data) + + if len(cb_selections) > 0: + key = cb_service.add_object(name="", data_hash=data_hash) + if key is None: + return + cb_service.set_object_percent(key, percent=0) + for selection in cb_selections: + self._add_selection(key, selection) + cb_service.set_object_percent(key, percent=100) + + def _md5_for_file(self, file_name): + '''Calculate md5 for file data + + Calculating block wise to prevent issues with big files in memory + ''' + block_size = 8192 + md5 = hashlib.md5() + f = open(file_name, 'r') + while True: + data = f.read(block_size) + if not data: + break + md5.update(data) + f.close() + return md5.digest() + + def _add_selection(self, key, selection): + if not selection.data: + logging.warning('no data for selection target %s.', selection.type) + return + + logging.debug('adding type ' + selection.type + '.') + + cb_service = clipboard.get_instance() + if selection.type == 'text/uri-list': + uris = selection.get_uris() + + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list' \ + ' still not supported.') + uri = uris[0] + scheme, netloc_, path_, parameters_, query_, fragment_ = \ + urlparse(uri) + on_disk = (scheme == 'file') + + cb_service.add_object_format(key, + selection.type, + uri, + on_disk) + else: + cb_service.add_object_format(key, + selection.type, + selection.data, + on_disk=False) diff --git a/src/jarabe/frame/clipboardtray.py b/src/jarabe/frame/clipboardtray.py new file mode 100644 index 0000000..37d5e1a --- /dev/null +++ b/src/jarabe/frame/clipboardtray.py @@ -0,0 +1,223 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import tempfile + +import gtk + +from sugar import util +from sugar.graphics import tray +from sugar.graphics import style + +from jarabe.frame import clipboard +from jarabe.frame.clipboardicon import ClipboardIcon + + +class _ContextMap(object): + """Maps a drag context to the clipboard object involved in the dragging.""" + def __init__(self): + self._context_map = {} + + def add_context(self, context, object_id, data_types): + """Establishes the mapping. data_types will serve us for reference- + counting this mapping. + """ + self._context_map[context] = [object_id, data_types] + + def get_object_id(self, context): + """Retrieves the object_id associated with context. + Will release the association when this function was called as many + times as the number of data_types that this clipboard object contains. + """ + [object_id, data_types_left] = self._context_map[context] + + data_types_left = data_types_left - 1 + if data_types_left == 0: + del self._context_map[context] + else: + self._context_map[context] = [object_id, data_types_left] + + return object_id + + def has_context(self, context): + return context in self._context_map + + +class ClipboardTray(tray.VTray): + + MAX_ITEMS = gtk.gdk.screen_height() / style.GRID_CELL_SIZE - 2 + + def __init__(self): + tray.VTray.__init__(self, align=tray.ALIGN_TO_END) + self._icons = {} + self._context_map = _ContextMap() + + cb_service = clipboard.get_instance() + cb_service.connect('object-added', self._object_added_cb) + cb_service.connect('object-deleted', self._object_deleted_cb) + + def owns_clipboard(self): + for icon in self._icons.values(): + if icon.owns_clipboard: + return True + return False + + def _add_selection(self, object_id, selection): + if not selection.data: + return + + logging.debug('ClipboardTray: adding type %r', selection.type) + + cb_service = clipboard.get_instance() + if selection.type == 'text/uri-list': + uris = selection.data.split('\n') + if len(uris) > 1: + raise NotImplementedError('Multiple uris in text/uri-list' \ + ' still not supported.') + + cb_service.add_object_format(object_id, + selection.type, + uris[0], + on_disk=True) + else: + cb_service.add_object_format(object_id, + selection.type, + selection.data, + on_disk=False) + + def _object_added_cb(self, cb_service, cb_object): + if self._icons: + group = self._icons.values()[0] + else: + group = None + + icon = ClipboardIcon(cb_object, group) + self.add_item(icon) + icon.show() + self._icons[cb_object.get_id()] = icon + + objects_to_delete = self.get_children()[:-self.MAX_ITEMS] + for icon in objects_to_delete: + logging.debug('ClipboardTray: deleting surplus object') + cb_service = clipboard.get_instance() + cb_service.delete_object(icon.get_object_id()) + + logging.debug('ClipboardTray: %r was added', cb_object.get_id()) + + def _object_deleted_cb(self, cb_service, object_id): + icon = self._icons[object_id] + self.remove_item(icon) + del self._icons[object_id] + # select the last available icon + if self._icons: + last_icon = self.get_children()[-1] + last_icon.props.active = True + + logging.debug('ClipboardTray: %r was deleted', object_id) + + def drag_motion_cb(self, widget, context, x, y, time): + logging.debug('ClipboardTray._drag_motion_cb') + + if self._internal_drag(context): + context.drag_status(gtk.gdk.ACTION_MOVE, time) + else: + context.drag_status(gtk.gdk.ACTION_COPY, time) + self.props.drag_active = True + + return True + + def drag_leave_cb(self, widget, context, time): + self.props.drag_active = False + + def drag_drop_cb(self, widget, context, x, y, time): + logging.debug('ClipboardTray._drag_drop_cb') + + if self._internal_drag(context): + # TODO: We should move the object within the clipboard here + if not self._context_map.has_context(context): + context.drop_finish(False, gtk.get_current_event_time()) + return False + + cb_service = clipboard.get_instance() + object_id = cb_service.add_object(name="") + + self._context_map.add_context(context, object_id, len(context.targets)) + + if 'XdndDirectSave0' in context.targets: + window = context.source_window + prop_type, format_, filename = \ + window.property_get('XdndDirectSave0', 'text/plain') + + # FIXME query the clipboard service for a filename? + base_dir = tempfile.gettempdir() + dest_filename = util.unique_id() + + name_, dot, extension = filename.rpartition('.') + dest_filename += dot + extension + + dest_uri = 'file://' + os.path.join(base_dir, dest_filename) + + window.property_change('XdndDirectSave0', prop_type, format_, + gtk.gdk.PROP_MODE_REPLACE, dest_uri) + + widget.drag_get_data(context, 'XdndDirectSave0', time) + else: + for target in context.targets: + if str(target) not in ('TIMESTAMP', 'TARGETS', 'MULTIPLE'): + widget.drag_get_data(context, target, time) + + cb_service.set_object_percent(object_id, percent=100) + + return True + + def drag_data_received_cb(self, widget, context, x, y, selection, + targetType, time): + logging.debug('ClipboardTray: got data for target %r', + selection.target) + + object_id = self._context_map.get_object_id(context) + try: + if selection is None: + logging.warn('ClipboardTray: empty selection for target %s', + selection.target) + elif selection.target == 'XdndDirectSave0': + if selection.data == 'S': + window = context.source_window + + prop_type, format_, dest = window.property_get( + 'XdndDirectSave0', 'text/plain') + + clipboardservice = clipboard.get_instance() + clipboardservice.add_object_format(object_id, + 'XdndDirectSave0', + dest, on_disk=True) + else: + self._add_selection(object_id, selection) + + finally: + # If it's the last target to be processed, finish + # the dnd transaction + if not self._context_map.has_context(context): + context.drop_finish(True, gtk.get_current_event_time()) + + def _internal_drag(self, context): + view_ancestor = context.get_source_widget().get_ancestor(gtk.Viewport) + if view_ancestor is self._viewport: + return True + else: + return False diff --git a/src/jarabe/frame/devicestray.py b/src/jarabe/frame/devicestray.py new file mode 100644 index 0000000..c5db639 --- /dev/null +++ b/src/jarabe/frame/devicestray.py @@ -0,0 +1,53 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +from sugar.graphics import tray + +from jarabe import config + + +class DevicesTray(tray.HTray): + def __init__(self): + tray.HTray.__init__(self, align=tray.ALIGN_TO_END) + + for f in os.listdir(os.path.join(config.ext_path, 'deviceicon')): + if f.endswith('.py') and not f.startswith('__'): + module_name = f[:-3] + try: + mod = __import__('deviceicon.' + module_name, globals(), + locals(), [module_name]) + mod.setup(self) + except Exception: + logging.exception('Exception while loading extension:') + + def add_device(self, view): + index = 0 + relative_index = getattr(view, 'FRAME_POSITION_RELATIVE', -1) + for item in self.get_children(): + current_relative_index = getattr(item, 'FRAME_POSITION_RELATIVE', + 0) + if current_relative_index >= relative_index: + index += 1 + else: + break + self.add_item(view, index=index) + view.show() + + def remove_device(self, view): + self.remove_item(view) diff --git a/src/jarabe/frame/eventarea.py b/src/jarabe/frame/eventarea.py new file mode 100644 index 0000000..1b5bf86 --- /dev/null +++ b/src/jarabe/frame/eventarea.py @@ -0,0 +1,153 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import gobject +import wnck +import gconf + + +_MAX_DELAY = 1000 + + +class EventArea(gobject.GObject): + __gsignals__ = { + 'enter': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'leave': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._windows = [] + self._hover = False + self._sids = {} + client = gconf.client_get_default() + self._edge_delay = client.get_int('/desktop/sugar/frame/edge_delay') + self._corner_delay = client.get_int('/desktop/sugar/frame' + '/corner_delay') + + right = gtk.gdk.screen_width() - 1 + bottom = gtk.gdk.screen_height() - 1 + width = gtk.gdk.screen_width() - 2 + height = gtk.gdk.screen_height() - 2 + + if self._edge_delay != _MAX_DELAY: + invisible = self._create_invisible(1, 0, width, 1, + self._edge_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(1, bottom, width, 1, + self._edge_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(0, 1, 1, height, + self._edge_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, 1, 1, height, + self._edge_delay) + self._windows.append(invisible) + + if self._corner_delay != _MAX_DELAY: + invisible = self._create_invisible(0, 0, 1, 1, + self._corner_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, 0, 1, 1, + self._corner_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(0, bottom, 1, 1, + self._corner_delay) + self._windows.append(invisible) + + invisible = self._create_invisible(right, bottom, 1, 1, + self._corner_delay) + self._windows.append(invisible) + + screen = wnck.screen_get_default() + screen.connect('window-stacking-changed', + self._window_stacking_changed_cb) + + def _create_invisible(self, x, y, width, height, delay): + invisible = gtk.Invisible() + if delay >= 0: + invisible.connect('enter-notify-event', self._enter_notify_cb, + delay) + invisible.connect('leave-notify-event', self._leave_notify_cb) + + invisible.drag_dest_set(0, [], 0) + invisible.connect('drag_motion', self._drag_motion_cb) + invisible.connect('drag_leave', self._drag_leave_cb) + + invisible.realize() + # pylint: disable=E1101 + invisible.window.set_events(gtk.gdk.POINTER_MOTION_MASK | + gtk.gdk.ENTER_NOTIFY_MASK | + gtk.gdk.LEAVE_NOTIFY_MASK) + invisible.window.move_resize(x, y, width, height) + + return invisible + + def _notify_enter(self): + if not self._hover: + self._hover = True + self.emit('enter') + + def _notify_leave(self): + if self._hover: + self._hover = False + self.emit('leave') + + def _enter_notify_cb(self, widget, event, delay): + if widget in self._sids: + gobject.source_remove(self._sids[widget]) + self._sids[widget] = gobject.timeout_add(delay, + self.__delay_cb, + widget) + + def __delay_cb(self, widget): + del self._sids[widget] + self._notify_enter() + return False + + def _leave_notify_cb(self, widget, event): + if widget in self._sids: + gobject.source_remove(self._sids[widget]) + del self._sids[widget] + self._notify_leave() + + def _drag_motion_cb(self, widget, drag_context, x, y, timestamp): + drag_context.drag_status(0, timestamp) + self._notify_enter() + return True + + def _drag_leave_cb(self, widget, drag_context, timestamp): + self._notify_leave() + return True + + def show(self): + for window in self._windows: + window.show() + + def hide(self): + for window in self._windows: + window.hide() + + def _window_stacking_changed_cb(self, screen): + for window in self._windows: + window.window.raise_() diff --git a/src/jarabe/frame/frame.py b/src/jarabe/frame/frame.py new file mode 100644 index 0000000..7407e18 --- /dev/null +++ b/src/jarabe/frame/frame.py @@ -0,0 +1,348 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gtk +import gobject + +from sugar.graphics import animator +from sugar.graphics import style +from sugar.graphics import palettegroup +from sugar import profile + +from jarabe.frame.eventarea import EventArea +from jarabe.frame.activitiestray import ActivitiesTray +from jarabe.frame.zoomtoolbar import ZoomToolbar +from jarabe.frame.friendstray import FriendsTray +from jarabe.frame.devicestray import DevicesTray +from jarabe.frame.framewindow import FrameWindow +from jarabe.frame.clipboardpanelwindow import ClipboardPanelWindow +from jarabe.frame.notification import NotificationIcon, NotificationWindow +from jarabe.model import notifications + + +TOP_RIGHT = 0 +TOP_LEFT = 1 +BOTTOM_RIGHT = 2 +BOTTOM_LEFT = 3 + +_FRAME_HIDING_DELAY = 500 +_NOTIFICATION_DURATION = 5000 + + +class _Animation(animator.Animation): + def __init__(self, frame, end): + start = frame.current_position + animator.Animation.__init__(self, start, end) + self._frame = frame + + def next_frame(self, current): + self._frame.move(current) + + +class _MouseListener(object): + def __init__(self, frame): + self._frame = frame + self._hide_sid = 0 + + def mouse_enter(self): + self._show_frame() + + def mouse_leave(self): + if self._frame.mode == Frame.MODE_MOUSE: + self._hide_frame() + + def _show_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._frame.show(Frame.MODE_MOUSE) + + def _hide_frame_timeout_cb(self): + self._frame.hide() + return False + + def _hide_frame(self): + if self._hide_sid != 0: + gobject.source_remove(self._hide_sid) + self._hide_sid = gobject.timeout_add( + _FRAME_HIDING_DELAY, self._hide_frame_timeout_cb) + + +class _KeyListener(object): + def __init__(self, frame): + self._frame = frame + + def key_press(self): + if self._frame.visible: + if self._frame.mode == Frame.MODE_KEYBOARD: + self._frame.hide() + else: + self._frame.show(Frame.MODE_KEYBOARD) + + +class Frame(object): + MODE_MOUSE = 0 + MODE_KEYBOARD = 1 + MODE_NON_INTERACTIVE = 2 + + def __init__(self): + logging.debug('STARTUP: Loading the frame') + self.mode = None + + self._palette_group = palettegroup.get_group('frame') + self._palette_group.connect('popdown', self._palette_group_popdown_cb) + + self._left_panel = None + self._right_panel = None + self._top_panel = None + self._bottom_panel = None + + self.current_position = 0.0 + self._animator = None + + self._event_area = EventArea() + self._event_area.connect('enter', self._enter_corner_cb) + self._event_area.show() + + self._top_panel = self._create_top_panel() + self._bottom_panel = self._create_bottom_panel() + self._left_panel = self._create_left_panel() + self._right_panel = self._create_right_panel() + + screen = gtk.gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + self._key_listener = _KeyListener(self) + self._mouse_listener = _MouseListener(self) + + self._notif_by_icon = {} + + notification_service = notifications.get_service() + notification_service.notification_received.connect( + self.__notification_received_cb) + notification_service.notification_cancelled.connect( + self.__notification_cancelled_cb) + + def is_visible(self): + return self.current_position != 0.0 + + visible = property(is_visible, None) + + def hide(self): + if self._animator: + self._animator.stop() + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 0.0)) + self._animator.start() + + self.mode = None + + def show(self, mode): + if self.visible: + return + if self._animator: + self._animator.stop() + + self.mode = mode + + self._animator = animator.Animator(0.5) + self._animator.add(_Animation(self, 1.0)) + self._animator.start() + + def move(self, pos): + self.current_position = pos + self._update_position() + + def _is_hover(self): + return (self._top_panel.hover or \ + self._bottom_panel.hover or \ + self._left_panel.hover or \ + self._right_panel.hover) + + def _create_top_panel(self): + panel = self._create_panel(gtk.POS_TOP) + + zoom_toolbar = ZoomToolbar() + panel.append(zoom_toolbar, expand=False) + zoom_toolbar.show() + + activities_tray = ActivitiesTray() + panel.append(activities_tray) + activities_tray.show() + + return panel + + def _create_bottom_panel(self): + panel = self._create_panel(gtk.POS_BOTTOM) + + devices_tray = DevicesTray() + panel.append(devices_tray) + devices_tray.show() + + return panel + + def _create_right_panel(self): + panel = self._create_panel(gtk.POS_RIGHT) + + tray = FriendsTray() + panel.append(tray) + tray.show() + + return panel + + def _create_left_panel(self): + panel = ClipboardPanelWindow(self, gtk.POS_LEFT) + + self._connect_to_panel(panel) + panel.connect('drag-motion', self._drag_motion_cb) + panel.connect('drag-leave', self._drag_leave_cb) + + return panel + + def _create_panel(self, orientation): + panel = FrameWindow(orientation) + self._connect_to_panel(panel) + + return panel + + def _move_panel(self, panel, pos, x1, y1, x2, y2): + x = (x2 - x1) * pos + x1 + y = (y2 - y1) * pos + y1 + + panel.move(int(x), int(y)) + + # FIXME we should hide and show as necessary to free memory + if not panel.props.visible: + panel.show() + + def _connect_to_panel(self, panel): + panel.connect('enter-notify-event', self._enter_notify_cb) + panel.connect('leave-notify-event', self._leave_notify_cb) + + def _update_position(self): + screen_h = gtk.gdk.screen_height() + screen_w = gtk.gdk.screen_width() + + self._move_panel(self._top_panel, self.current_position, + 0, - self._top_panel.size, 0, 0) + + self._move_panel(self._bottom_panel, self.current_position, + 0, screen_h, 0, screen_h - self._bottom_panel.size) + + self._move_panel(self._left_panel, self.current_position, + - self._left_panel.size, 0, 0, 0) + + self._move_panel(self._right_panel, self.current_position, + screen_w, 0, screen_w - self._right_panel.size, 0) + + def _size_changed_cb(self, screen): + self._update_position() + + def _enter_notify_cb(self, window, event): + if event.detail != gtk.gdk.NOTIFY_INFERIOR: + self._mouse_listener.mouse_enter() + + def _leave_notify_cb(self, window, event): + if event.detail == gtk.gdk.NOTIFY_INFERIOR: + return + + if not self._is_hover() and not self._palette_group.is_up(): + self._mouse_listener.mouse_leave() + + def _palette_group_popdown_cb(self, group): + if not self._is_hover(): + self._mouse_listener.mouse_leave() + + def _drag_motion_cb(self, window, context, x, y, time): + self._mouse_listener.mouse_enter() + + def _drag_leave_cb(self, window, drag_context, timestamp): + self._mouse_listener.mouse_leave() + + def _enter_corner_cb(self, event_area): + self._mouse_listener.mouse_enter() + + def notify_key_press(self): + self._key_listener.key_press() + + def add_notification(self, icon, corner=gtk.CORNER_TOP_LEFT, + duration=_NOTIFICATION_DURATION): + + if not isinstance(icon, NotificationIcon): + raise TypeError('icon must be a NotificationIcon.') + + window = NotificationWindow() + + screen = gtk.gdk.screen_get_default() + if corner == gtk.CORNER_TOP_LEFT: + window.move(0, 0) + elif corner == gtk.CORNER_TOP_RIGHT: + window.move(screen.get_width() - style.GRID_CELL_SIZE, 0) + elif corner == gtk.CORNER_BOTTOM_LEFT: + window.move(0, screen.get_height() - style.GRID_CELL_SIZE) + elif corner == gtk.CORNER_BOTTOM_RIGHT: + window.move(screen.get_width() - style.GRID_CELL_SIZE, + screen.get_height() - style.GRID_CELL_SIZE) + else: + raise ValueError('Inalid corner: %r' % corner) + + window.add(icon) + icon.show() + window.show() + + self._notif_by_icon[icon] = window + + gobject.timeout_add(duration, + lambda: self.remove_notification(icon)) + + def remove_notification(self, icon): + if icon not in self._notif_by_icon: + logging.debug('icon %r not in list of notifications.', icon) + return + + window = self._notif_by_icon[icon] + window.destroy() + del self._notif_by_icon[icon] + + def __notification_received_cb(self, **kwargs): + logging.debug('__notification_received_cb') + icon = NotificationIcon() + + hints = kwargs['hints'] + + icon_file_name = hints.get('x-sugar-icon-file-name', '') + if icon_file_name: + icon.props.icon_filename = icon_file_name + else: + icon.props.icon_name = 'application-octet-stream' + + icon_colors = hints.get('x-sugar-icon-colors', '') + if not icon_colors: + icon_colors = profile.get_color() + icon.props.xo_color = icon_colors + + duration = kwargs.get('expire_timeout', -1) + if duration == -1: + duration = _NOTIFICATION_DURATION + + self.add_notification(icon, gtk.CORNER_TOP_RIGHT, duration) + + def __notification_cancelled_cb(self, **kwargs): + # Do nothing for now. Our notification UI is so simple, there's no + # point yet. + pass diff --git a/src/jarabe/frame/frameinvoker.py b/src/jarabe/frame/frameinvoker.py new file mode 100644 index 0000000..a4abfa8 --- /dev/null +++ b/src/jarabe/frame/frameinvoker.py @@ -0,0 +1,38 @@ +# Copyright (C) 2007, Eduardo Silva +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk + +from sugar.graphics import style +from sugar.graphics.palette import WidgetInvoker + + +def _get_screen_area(): + frame_thickness = style.GRID_CELL_SIZE + + x = y = frame_thickness + width = gtk.gdk.screen_width() - frame_thickness + height = gtk.gdk.screen_height() - frame_thickness + + return gtk.gdk.Rectangle(x, y, width, height) + + +class FrameWidgetInvoker(WidgetInvoker): + def __init__(self, widget): + WidgetInvoker.__init__(self, widget, widget.child) + + self._position_hint = self.ANCHORED + self._screen_area = _get_screen_area() diff --git a/src/jarabe/frame/framewindow.py b/src/jarabe/frame/framewindow.py new file mode 100644 index 0000000..394ba00 --- /dev/null +++ b/src/jarabe/frame/framewindow.py @@ -0,0 +1,153 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +from gtk import gdk +import gobject + +from sugar.graphics import style + + +class FrameContainer(gtk.Bin): + """A container class for frame panel rendering. Hosts a child 'box' where + frame elements can be added. Excludes grid-sized squares at each end + of the frame panel, and a space alongside the inside of the screen where + a border is drawn.""" + + __gtype_name__ = 'SugarFrameContainer' + + def __init__(self, position): + gtk.Bin.__init__(self) + self._position = position + + if self.is_vertical(): + box = gtk.VBox() + else: + box = gtk.HBox() + self.add(box) + box.show() + + def is_vertical(self): + return self._position in (gtk.POS_LEFT, gtk.POS_RIGHT) + + def do_expose_event(self, event): + # Draw the inner border as a rectangle + cr = self.get_parent_window().cairo_create() + r, g, b, a = style.COLOR_BUTTON_GREY.get_rgba() + cr.set_source_rgba (r, g, b, a) + + if self.is_vertical(): + x = style.GRID_CELL_SIZE if self._position == gtk.POS_LEFT else 0 + y = style.GRID_CELL_SIZE + width = style.LINE_WIDTH + height = self.allocation.height - (style.GRID_CELL_SIZE * 2) + else: + x = style.GRID_CELL_SIZE + y = style.GRID_CELL_SIZE if self._position == gtk.POS_TOP else 0 + height = style.LINE_WIDTH + width = self.allocation.width - (style.GRID_CELL_SIZE * 2) + + cr.rectangle(x, y, width, height) + cr.fill() + + gtk.Bin.do_expose_event(self, event) + return False + + def do_size_request(self, req): + if self.is_vertical(): + req.height = gdk.screen_height() + req.width = style.GRID_CELL_SIZE + style.LINE_WIDTH + else: + req.width = gdk.screen_width() + req.height = style.GRID_CELL_SIZE + style.LINE_WIDTH + + self.get_child().size_request() + + def do_size_allocate(self, allocation): + self.allocation = allocation + + # exclude grid squares at two ends of the frame + # allocate remaining space to child box, minus the space needed for + # drawing the border + allocation = gdk.Rectangle() + if self.is_vertical(): + allocation.x = 0 if self._position == gtk.POS_LEFT \ + else style.LINE_WIDTH + allocation.y = style.GRID_CELL_SIZE + allocation.width = self.allocation.width - style.LINE_WIDTH + allocation.height = self.allocation.height \ + - (style.GRID_CELL_SIZE * 2) + else: + allocation.x = style.GRID_CELL_SIZE + allocation.y = 0 if self._position == gtk.POS_TOP \ + else style.LINE_WIDTH + allocation.width = self.allocation.width \ + - (style.GRID_CELL_SIZE * 2) + allocation.height = self.allocation.height - style.LINE_WIDTH + + self.get_child().size_allocate(allocation) + + +class FrameWindow(gtk.Window): + __gtype_name__ = 'SugarFrameWindow' + + def __init__(self, position): + gtk.Window.__init__(self) + self.hover = False + self.size = style.GRID_CELL_SIZE + style.LINE_WIDTH + + accel_group = gtk.AccelGroup() + self.set_data('sugar-accel-group', accel_group) + self.add_accel_group(accel_group) + + self._position = position + + self.set_decorated(False) + self.connect('realize', self._realize_cb) + self.connect('enter-notify-event', self._enter_notify_cb) + self.connect('leave-notify-event', self._leave_notify_cb) + + self._container = FrameContainer(position) + self.add(self._container) + self._container.show() + self._update_size() + + screen = gdk.screen_get_default() + screen.connect('size-changed', self._size_changed_cb) + + def append(self, child, expand=True, fill=True): + self._container.get_child().pack_start(child, expand=expand, fill=fill) + + def _update_size(self): + if self._position == gtk.POS_TOP or self._position == gtk.POS_BOTTOM: + self.resize(gdk.screen_width(), self.size) + else: + self.resize(self.size, gdk.screen_height()) + + def _realize_cb(self, widget): + self.window.set_type_hint(gdk.WINDOW_TYPE_HINT_DOCK) + self.window.set_accept_focus(False) + + def _enter_notify_cb(self, window, event): + if event.detail != gdk.NOTIFY_INFERIOR: + self.hover = True + + def _leave_notify_cb(self, window, event): + if event.detail != gdk.NOTIFY_INFERIOR: + self.hover = False + + def _size_changed_cb(self, screen): + self._update_size() diff --git a/src/jarabe/frame/friendstray.py b/src/jarabe/frame/friendstray.py new file mode 100644 index 0000000..26a279b --- /dev/null +++ b/src/jarabe/frame/friendstray.py @@ -0,0 +1,129 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +from sugar.graphics.tray import VTray, TrayIcon + +from jarabe.view.buddymenu import BuddyMenu +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.model import shell +from jarabe.model.buddy import get_owner_instance +from jarabe.model import neighborhood + + +class FriendIcon(TrayIcon): + def __init__(self, buddy): + TrayIcon.__init__(self, icon_name='computer-xo', + xo_color=buddy.get_color()) + + self._buddy = buddy + self.set_palette_invoker(FrameWidgetInvoker(self)) + self.palette_invoker.cache_palette = False + + def create_palette(self): + palette = BuddyMenu(self._buddy) + palette.props.icon_visible = False + palette.set_group_id('frame') + return palette + + +class FriendsTray(VTray): + def __init__(self): + VTray.__init__(self) + + self._shared_activity = None + self._buddies = {} + + shell.get_model().connect('active-activity-changed', + self.__active_activity_changed_cb) + + neighborhood.get_model().connect('activity-added', + self.__neighborhood_activity_added_cb) + + def add_buddy(self, buddy): + if buddy.props.key in self._buddies: + return + + icon = FriendIcon(buddy) + self.add_item(icon) + icon.show() + + self._buddies[buddy.props.key] = icon + + def remove_buddy(self, buddy): + if buddy.props.key not in self._buddies: + return + + self.remove_item(self._buddies[buddy.props.key]) + del self._buddies[buddy.props.key] + + def clear(self): + for item in self.get_children(): + self.remove_item(item) + item.destroy() + self._buddies = {} + + def __neighborhood_activity_added_cb(self, neighborhood_model, + shared_activity): + logging.debug('FriendsTray.__neighborhood_activity_added_cb') + active_activity = shell.get_model().get_active_activity() + if active_activity.get_activity_id() != shared_activity.activity_id: + return + + self.clear() + + # always display ourselves + self.add_buddy(get_owner_instance()) + + self._set_current_activity(shared_activity.activity_id) + + def __active_activity_changed_cb(self, home_model, home_activity): + logging.debug('FriendsTray.__active_activity_changed_cb') + self.clear() + + # always display ourselves + self.add_buddy(get_owner_instance()) + + if home_activity is None: + return + + activity_id = home_activity.get_activity_id() + if activity_id is None: + return + + self._set_current_activity(activity_id) + + def _set_current_activity(self, activity_id): + logging.debug('FriendsTray._set_current_activity') + neighborhood_model = neighborhood.get_model() + self._shared_activity = neighborhood_model.get_activity(activity_id) + if self._shared_activity is None: + return + + for buddy in self._shared_activity.get_buddies(): + self.add_buddy(buddy) + + self._shared_activity.connect('buddy-added', self.__buddy_added_cb) + self._shared_activity.connect('buddy-removed', self.__buddy_removed_cb) + + def __buddy_added_cb(self, activity, buddy): + logging.debug('FriendsTray.__buddy_added_cb') + self.add_buddy(buddy) + + def __buddy_removed_cb(self, activity, buddy): + logging.debug('FriendsTray.__buddy_removed_cb') + self.remove_buddy(buddy) diff --git a/src/jarabe/frame/notification.py b/src/jarabe/frame/notification.py new file mode 100644 index 0000000..3471e2c --- /dev/null +++ b/src/jarabe/frame/notification.py @@ -0,0 +1,102 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk + +from sugar.graphics import style +from sugar.graphics.xocolor import XoColor + +from jarabe.view.pulsingicon import PulsingIcon + + +class NotificationIcon(gtk.EventBox): + __gtype_name__ = 'SugarNotificationIcon' + + __gproperties__ = { + 'xo-color': (object, None, None, gobject.PARAM_READWRITE), + 'icon-name': (str, None, None, None, gobject.PARAM_READWRITE), + 'icon-filename': (str, None, None, None, gobject.PARAM_READWRITE), + } + + _PULSE_TIMEOUT = 3 + + def __init__(self, **kwargs): + self._icon = PulsingIcon(pixel_size=style.STANDARD_ICON_SIZE) + gobject.GObject.__init__(self, **kwargs) + self.props.visible_window = False + + self._icon.props.pulse_color = \ + XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self._icon.props.pulsing = True + self.add(self._icon) + self._icon.show() + + gobject.timeout_add_seconds(self._PULSE_TIMEOUT, + self.__stop_pulsing_cb) + + self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + + def __stop_pulsing_cb(self): + self._icon.props.pulsing = False + return False + + def do_set_property(self, pspec, value): + if pspec.name == 'xo-color': + if self._icon.props.base_color != value: + self._icon.props.base_color = value + elif pspec.name == 'icon-name': + if self._icon.props.icon_name != value: + self._icon.props.icon_name = value + elif pspec.name == 'icon-filename': + if self._icon.props.file != value: + self._icon.props.file = value + + def do_get_property(self, pspec): + if pspec.name == 'xo-color': + return self._icon.props.base_color + elif pspec.name == 'icon-name': + return self._icon.props.icon_name + elif pspec.name == 'icon-filename': + return self._icon.props.file + + def _set_palette(self, palette): + self._icon.palette = palette + + def _get_palette(self): + return self._icon.palette + + palette = property(_get_palette, _set_palette) + + +class NotificationWindow(gtk.Window): + __gtype_name__ = 'SugarNotificationWindow' + + def __init__(self, **kwargs): + + gtk.Window.__init__(self, **kwargs) + + self.set_decorated(False) + self.set_resizable(False) + self.connect('realize', self._realize_cb) + + def _realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(False) + + color = gtk.gdk.color_parse(style.COLOR_TOOLBAR_GREY.get_html()) + self.modify_bg(gtk.STATE_NORMAL, color) diff --git a/src/jarabe/frame/zoomtoolbar.py b/src/jarabe/frame/zoomtoolbar.py new file mode 100644 index 0000000..c28fe1c --- /dev/null +++ b/src/jarabe/frame/zoomtoolbar.py @@ -0,0 +1,94 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2009 Simon Schampijer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging + +import glib +import gtk + +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.graphics.radiotoolbutton import RadioToolButton + +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.model import shell + + +class ZoomToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + + # we shouldn't be mirrored in RTL locales + self.set_direction(gtk.TEXT_DIR_LTR) + + # ask not to be collapsed if possible + self.set_size_request(4 * style.GRID_CELL_SIZE, -1) + + self._mesh_button = self._add_button('zoom-neighborhood', + _('Neighborhood'), _('F1'), shell.ShellModel.ZOOM_MESH) + self._groups_button = self._add_button('zoom-groups', + _('Group'), _('F2'), shell.ShellModel.ZOOM_GROUP) + self._home_button = self._add_button('zoom-home', + _('Home'), _('F3'), shell.ShellModel.ZOOM_HOME) + self._activity_button = self._add_button('zoom-activity', + _('Activity'), _('F4'), shell.ShellModel.ZOOM_ACTIVITY) + + shell_model = shell.get_model() + self._set_zoom_level(shell_model.zoom_level) + shell_model.zoom_level_changed.connect(self.__zoom_level_changed_cb) + + def _add_button(self, icon_name, label, accelerator, zoom_level): + if self.get_children(): + group = self.get_children()[0] + else: + group = None + + button = RadioToolButton(named_icon=icon_name, group=group, + accelerator=accelerator) + button.connect('clicked', self.__level_clicked_cb, zoom_level) + self.add(button) + button.show() + + palette = Palette(glib.markup_escape_text(label)) + palette.props.invoker = FrameWidgetInvoker(button) + palette.set_group_id('frame') + button.set_palette(palette) + + return button + + def __level_clicked_cb(self, button, level): + if not button.get_active(): + return + + shell.get_model().set_zoom_level(level) + + def __zoom_level_changed_cb(self, **kwargs): + self._set_zoom_level(kwargs['new_level']) + + def _set_zoom_level(self, new_level): + logging.debug('new zoom level: %r', new_level) + if new_level == shell.ShellModel.ZOOM_MESH: + self._mesh_button.props.active = True + elif new_level == shell.ShellModel.ZOOM_GROUP: + self._groups_button.props.active = True + elif new_level == shell.ShellModel.ZOOM_HOME: + self._home_button.props.active = True + elif new_level == shell.ShellModel.ZOOM_ACTIVITY: + self._activity_button.props.active = True + else: + raise ValueError('Invalid zoom level: %r' % (new_level)) -- cgit v0.9.1