diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/changelog 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/changelog
--- 2.91.3+git20120514.b9fec3e1-1/debian/changelog	2012-06-25 18:53:21.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/changelog	2015-04-07 07:35:59.000000000 +0000
@@ -1,3 +1,28 @@
+hamster-applet (2.91.3+git20120514.b9fec3e1-1ubuntu2) vivid; urgency=medium
+
+  [ Sebastian Bator ]
+  * debian/patches/07-dont-break-with-only-one-year-of-stats.patch:
+    Fix the show statistics functionality (LP: #1207400)
+
+ -- Timo Jyrinki <timo-jyrinki@ubuntu.com>  Tue, 07 Apr 2015 10:35:58 +0300
+
+hamster-applet (2.91.3+git20120514.b9fec3e1-1ubuntu1) raring-proposed; urgency=low
+
+  * Merge from Debian. Remaining changes:
+    - 01-dont-fail-on-applet-import.patch
+    - 02-dont-traceback-on-quit.patch (refreshed)
+    - 04-lp926481.patch
+    - 05-lp979707.patch
+    - debian/control: Recommends hamster-indicator
+    - debian/rules: don't remove applet.py and applet.ui as they are needed by
+      indicator-hamster (continue to not install the other files)
+  * Dropped:
+    - 03-check-self-app-window.patch: no longer needed
+  * debian/patches/06-gpg-dbus-update.patch: Updates in DBus interface of GTG.
+    Patch thanks to Izidor Matusov
+
+ -- Jamie Strandboge <jamie@ubuntu.com>  Fri, 22 Mar 2013 15:35:38 -0500
+
 hamster-applet (2.91.3+git20120514.b9fec3e1-1) unstable; urgency=low
 
   * New upstream release.
@@ -31,6 +56,58 @@ hamster-applet (2.91.3+git20120204.b1157
 
  -- Michael Biebl <biebl@debian.org>  Mon, 06 Feb 2012 15:52:29 +0100
 
+hamster-applet (2.91.3+git20110714.9aefd7-2ubuntu5) raring; urgency=low
+
+  * debian/patches/05-lp979707.patch: fix a crash in Overview (LP: #979707)
+
+ -- Gediminas Paulauskas <menesis@pov.lt>  Mon, 07 Jan 2013 15:52:53 +0200
+
+hamster-applet (2.91.3+git20110714.9aefd7-2ubuntu4) quantal; urgency=low
+
+  * Recommends hamster-indicator (LP: #1049078)
+
+ -- Jamie Strandboge <jamie@ubuntu.com>  Tue, 25 Sep 2012 08:49:16 -0500
+
+hamster-applet (2.91.3+git20110714.9aefd7-2ubuntu3) precise; urgency=low
+
+  * debian/patches/04-lp926481.patch: if we somehow got a click with no
+    selected start_time, then reset the selection but don't update the widget
+    (LP: #926481)
+
+ -- Jamie Strandboge <jamie@ubuntu.com>  Mon, 06 Feb 2012 07:56:53 -0600
+
+hamster-applet (2.91.3+git20110714.9aefd7-2ubuntu2) precise; urgency=low
+
+  * debian/patches/03-check-self-app-window.patch: verify that self.app.window
+    is not None (LP: #918487)
+
+ -- Jamie Strandboge <jamie@ubuntu.com>  Thu, 26 Jan 2012 08:53:18 -0600
+
+hamster-applet (2.91.3+git20110714.9aefd7-2ubuntu1) precise; urgency=low
+
+  * Resynchronize with Debian. Since the Debian and Ubuntu packages diverged
+    for a long time, this changelog entry represents the status of the Ubuntu
+    delta as compared to the hamster package shipped in 11.10. Remaining
+    changes:
+    - 01-dont-fail-on-applet-import.patch: if import of gnomeapplet fails,
+    default to upper panel orientation. This is needed for indicator-hamster
+    - 02-dont-traceback-on-quit.patch: don't traceback if saving to gconf
+      fails, but instead indicate to the caller that there was an error
+    - debian/control: Remove Vcs-Bzr link (Ubuntu developers should use
+      lp:ubuntu/hamster-applet instead)
+    - Drop the following, no longer needed:
+      + debian/patches/02_lp705482.patch (included upstream)
+      + debian/patches/03-dont-install-applet.patch (Debian packaging
+        addresses it)
+      + debian/patches/06-lp799809.patch (included upstream)
+      + debian/control: drop changes to Homepage and Description
+      + debian/rules: drop changes for quilt since we are now source format v3
+        and Debian packaging is adjusted for waf
+  * debian/rules: don't remove applet.py and applet.ui as they are needed by
+    indicator-hamster (continue to not install the other files)
+
+ -- Jamie Strandboge <jamie@ubuntu.com>  Mon, 07 Nov 2011 11:41:53 -0600
+
 hamster-applet (2.91.3+git20110714.9aefd7-2) unstable; urgency=low
 
   * Upload to unstable.
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/control 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/control
--- 2.91.3+git20120514.b9fec3e1-1/debian/control	2012-06-25 18:53:40.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/control	2015-04-07 07:40:14.000000000 +0000
@@ -2,12 +2,12 @@
 # 
 # Modifications should be made to debian/control.in instead.
 # This file is regenerated automatically in the clean target.
-
 Source: hamster-applet
 Section: gnome
 Priority: optional
-Maintainer: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>
-Uploaders: Josselin Mouette <joss@debian.org>, Michael Biebl <biebl@debian.org>
+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
+XSBC-Original-Maintainer: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>
+Uploaders: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>, Josselin Mouette <joss@debian.org>, Michael Biebl <biebl@debian.org>
 Build-Depends: cdbs (>= 0.4.90~),
                debhelper (>= 8),
                gnome-pkg-tools,
@@ -36,7 +36,8 @@ Depends: ${misc:Depends},
          python-gnome2,
          python-xdg
 Recommends: python-notify,
-            gnome-icon-theme
+            gnome-icon-theme,
+            hamster-indicator
 Suggests: python-evolution
 Description: time tracking applet for GNOME
  Project Hamster helps you to keep track of how much time you spend on various
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/control.in 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/control.in
--- 2.91.3+git20120514.b9fec3e1-1/debian/control.in	2011-12-31 13:55:45.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/control.in	2013-03-22 21:17:09.000000000 +0000
@@ -1,7 +1,8 @@
 Source: hamster-applet
 Section: gnome
 Priority: optional
-Maintainer: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>
+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
+XSBC-Original-Maintainer: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>
 Uploaders: @GNOME_TEAM@
 Build-Depends: cdbs (>= 0.4.90~),
                debhelper (>= 8),
@@ -31,7 +32,8 @@ Depends: ${misc:Depends},
          python-gnome2,
          python-xdg
 Recommends: python-notify,
-            gnome-icon-theme
+            gnome-icon-theme,
+            hamster-indicator
 Suggests: python-evolution
 Description: time tracking applet for GNOME
  Project Hamster helps you to keep track of how much time you spend on various
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/patches/01-dont-fail-on-applet-import.patch 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/01-dont-fail-on-applet-import.patch
--- 2.91.3+git20120514.b9fec3e1-1/debian/patches/01-dont-fail-on-applet-import.patch	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/01-dont-fail-on-applet-import.patch	2013-03-22 21:00:15.000000000 +0000
@@ -0,0 +1,64 @@
+Author: Jamie Strandboge <jamie@canonical.com>
+Description: if import of gnomeapplet fails, default to upper panel
+ orientation. Inspired by work by Alberto Milone in preparation for
+ indicator work. We continue to ship this patch even though we don't ship
+ the gnome-panel hamster applet because indicator-hamster currently imports
+ applet.py for other things.
+Forwarded: not-needed (upstream is porting to gnome3)
+
+Index: hamster-applet-2.91.3+git20110714.9aefd7/src/hamster/applet.py
+===================================================================
+--- hamster-applet-2.91.3+git20110714.9aefd7.orig/src/hamster/applet.py	2011-08-19 13:32:18.000000000 -0500
++++ hamster-applet-2.91.3+git20110714.9aefd7/src/hamster/applet.py	2011-11-07 11:35:07.000000000 -0600
+@@ -26,7 +26,12 @@
+ pygtk.require("2.0")
+ import gtk, pango
+ 
+-import gnomeapplet
++try:
++    import gnomeapplet
++except ImportError:
++    logging.warning("Could not import gnomeapplet. Defaulting to upper panel")
++    gnomeapplet = None
++
+ import gobject
+ import dbus, dbus.service, dbus.mainloop.glib
+ import locale
+@@ -110,9 +115,10 @@
+ 
+         popup_dir = self.get_parent().get_orient()
+ 
+-        orient_vertical = popup_dir in [gnomeapplet.ORIENT_LEFT] or \
+-                          popup_dir in [gnomeapplet.ORIENT_RIGHT]
+-
++        orient_vertical = False
++        if gnomeapplet != None:
++            orient_vertical = popup_dir in [gnomeapplet.ORIENT_LEFT] or \
++                              popup_dir in [gnomeapplet.ORIENT_RIGHT]
+ 
+         context = self.label.get_pango_context()
+         metrics = context.get_metrics(self.label.style.font_desc,
+@@ -148,9 +154,9 @@
+ 
+         orient_vertical = True
+         new_size = allocation.width
+-        if self.popup_dir in [gnomeapplet.ORIENT_LEFT]:
++        if gnomeapplet != None and self.popup_dir in [gnomeapplet.ORIENT_LEFT]:
+             new_angle = 270
+-        elif self.popup_dir in [gnomeapplet.ORIENT_RIGHT]:
++        elif gnomeapplet != None and self.popup_dir in [gnomeapplet.ORIENT_RIGHT]:
+             new_angle = 90
+         else:
+             new_angle = 0
+@@ -453,7 +459,10 @@
+ 
+         self.popup_dir = self.applet.get_orient()
+ 
+-        if self.popup_dir in (gnomeapplet.ORIENT_DOWN, gnomeapplet.ORIENT_UP):
++        if gnomeapplet == None:
++            # Default to upper panel (ORIENT_DOWN)
++            y = y + label.height
++        elif self.popup_dir in (gnomeapplet.ORIENT_DOWN, gnomeapplet.ORIENT_UP):
+             if self.popup_dir == gnomeapplet.ORIENT_DOWN:
+                 y = y + label.height
+             else:
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/patches/02-dont-traceback-on-quit.patch 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/02-dont-traceback-on-quit.patch
--- 2.91.3+git20120514.b9fec3e1-1/debian/patches/02-dont-traceback-on-quit.patch	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/02-dont-traceback-on-quit.patch	2013-03-22 21:01:20.000000000 +0000
@@ -0,0 +1,41 @@
+Author: Jamie Strandboge <jamie@canonical.com>
+Description: don't traceback if saving to gconf fails, but instead indicate
+ to the caller that there was an error
+Forwarded: no
+Bug-Ubuntu: https://bugs.launchpad.net/bugs/816460
+
+Index: hamster-applet-2.91.3+git20120514.b9fec3e1/src/hamster/configuration.py
+===================================================================
+--- hamster-applet-2.91.3+git20120514.b9fec3e1.orig/src/hamster/configuration.py	2013-03-22 16:01:17.000000000 -0500
++++ hamster-applet-2.91.3+git20120514.b9fec3e1/src/hamster/configuration.py	2013-03-22 16:01:17.000000000 -0500
+@@ -287,16 +287,20 @@
+         #for gconf refer to the full key path
+         key = self._fix_key(key)
+ 
+-        if vtype is bool:
+-            self._client.set_bool(key, value)
+-        elif vtype is str:
+-            self._client.set_string(key, value)
+-        elif vtype is int:
+-            self._client.set_int(key, value)
+-        elif vtype in (list, tuple):
+-            #Save every value as a string
+-            strvalues = [str(i) for i in value]
+-            self._client.set_list(key, gconf.VALUE_STRING, strvalues)
++        try:
++            if vtype is bool:
++                self._client.set_bool(key, value)
++            elif vtype is str:
++                self._client.set_string(key, value)
++            elif vtype is int:
++                self._client.set_int(key, value)
++            elif vtype in (list, tuple):
++                #Save every value as a string
++                strvalues = [str(i) for i in value]
++                self._client.set_list(key, gconf.VALUE_STRING, strvalues)
++        except:
++            log.warn("Could not save '%s' to gconf database" % str(key))
++            return False
+ 
+         return True
+ 
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/patches/04-lp926481.patch 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/04-lp926481.patch
--- 2.91.3+git20120514.b9fec3e1-1/debian/patches/04-lp926481.patch	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/04-lp926481.patch	2013-03-22 21:08:38.000000000 +0000
@@ -0,0 +1,24 @@
+Author: Jamie Strandboge <jamie@canonical.com>
+Description: if we somehow got a click with no selected start_time, then
+ reset the selection but don't update the widget
+Bug: https://bugzilla.gnome.org/show_bug.cgi?id=669478
+Bug-Ubuntu: https://launchpad.net/bugs/926481
+
+Index: hamster-applet-2.91.3+git20110714.9aefd7/src/hamster/widgets/dayline.py
+===================================================================
+--- hamster-applet-2.91.3+git20110714.9aefd7.orig/src/hamster/widgets/dayline.py	2012-02-06 09:29:48.000000000 -0600
++++ hamster-applet-2.91.3+git20110714.9aefd7/src/hamster/widgets/dayline.py	2012-02-06 09:39:01.000000000 -0600
+@@ -182,6 +182,13 @@
+     def on_click(self, scene, event, target):
+         self.drag_start = None
+ 
++        # If self.selection.start_time is somehow None, just reset the selection.
++        # This can sometimes happen when dragging left to right in small
++        # increments. https://bugzilla.gnome.org/show_bug.cgi?id=669478
++        if self.selection == None or self.selection.start_time == None:
++            self.new_selection()
++            return
++
+         start_time = self.selection.start_time
+         if start_time > dt.datetime.now():
+             start_time = dt.datetime.now()
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/patches/05-lp979707.patch 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/05-lp979707.patch
--- 2.91.3+git20120514.b9fec3e1-1/debian/patches/05-lp979707.patch	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/05-lp979707.patch	2013-03-22 21:09:39.000000000 +0000
@@ -0,0 +1,18 @@
+Author: Gediminas Paulauskas <menesis@pov.lt>
+Description: fix a crash in Overview
+ If you right-click on an activity and select "Add new"
+Bug-Ubuntu: https://launchpad.net/bugs/979707
+Forwarded-Upstream: no
+Index: hamster-applet-2.91.3+git20120514.b9fec3e1/src/hamster/overview.py
+===================================================================
+--- hamster-applet-2.91.3+git20120514.b9fec3e1.orig/src/hamster/overview.py	2013-03-22 16:09:35.000000000 -0500
++++ hamster-applet-2.91.3+git20120514.b9fec3e1/src/hamster/overview.py	2013-03-22 16:09:35.000000000 -0500
+@@ -381,7 +381,7 @@
+         elif isinstance(fact, dt.date):
+             selected_date = fact
+         else:
+-            selected_date = fact["date"]
++            selected_date = fact.date
+ 
+         dialogs.edit.show(self, fact_date = selected_date)
+ 
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/patches/06-gtg-dbus-update.patch 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/06-gtg-dbus-update.patch
--- 2.91.3+git20120514.b9fec3e1-1/debian/patches/06-gtg-dbus-update.patch	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/06-gtg-dbus-update.patch	2013-03-22 21:10:09.000000000 +0000
@@ -0,0 +1,42 @@
+From a216f10fb3ce737ddd7802561e8e95d1bb69a8ea Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Izidor=20Matu=C5=A1ov?= <izidor.matusov@gmail.com>
+Date: Sat, 16 Mar 2013 08:36:51 +0100
+Subject: [PATCH] Updates in DBus interface of GTG
+
+---
+ src/hamster/external.py | 10 +++++-----
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+diff --git a/src/hamster/external.py b/src/hamster/external.py
+index 05624c3..f1a5d2b 100644
+--- a/src/hamster/external.py
++++ b/src/hamster/external.py
+@@ -58,7 +58,7 @@ def get_activities(self, query = None):
+ 
+             tasks = []
+             try:
+-                tasks = conn.get_tasks()
++                tasks = conn.GetTasks()
+             except dbus.exceptions.DBusException:  #TODO too lame to figure out how to connect to the disconnect signal
+                 self.__gtg_connection = None
+                 return self.get_activities(query) # reconnect
+@@ -77,12 +77,12 @@ def get_activities(self, query = None):
+ 
+     def __get_gtg_connection(self):
+         bus = dbus.SessionBus()
+-        if self.__gtg_connection and bus.name_has_owner("org.GTG"):
++        if self.__gtg_connection and bus.name_has_owner("org.gnome.GTG"):
+             return self.__gtg_connection
+ 
+-        if bus.name_has_owner("org.GTG"):
+-            self.__gtg_connection = dbus.Interface(bus.get_object('org.GTG', '/org/GTG'),
+-                                                   dbus_interface='org.GTG')
++        if bus.name_has_owner("org.gnome.GTG"):
++            self.__gtg_connection = dbus.Interface(bus.get_object('org.gnome.GTG', '/org/gnome/GTG'),
++                                                   dbus_interface='org.gnome.GTG')
+             return self.__gtg_connection
+         else:
+             return None
+-- 
+1.8.1.5
+
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/patches/07-dont-break-with-only-one-year-of-stats.patch 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/07-dont-break-with-only-one-year-of-stats.patch
--- 2.91.3+git20120514.b9fec3e1-1/debian/patches/07-dont-break-with-only-one-year-of-stats.patch	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/07-dont-break-with-only-one-year-of-stats.patch	2015-03-25 10:50:14.000000000 +0000
@@ -0,0 +1,35 @@
+From 8ba12fff998b3a8bb73f1655ef07a75e610eee43 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Toms=20Bau=C4=A3is?= <toms.baugis@gmail.com>
+Date: Mon, 24 Sep 2012 22:22:24 +0200
+Subject: [PATCH] don't break when we have just one year of stats. fixes #30
+
+---
+ src/hamster/stats.py | 5 ++++-
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+diff --git a/src/hamster/stats.py b/src/hamster/stats.py
+index 1d1c880..cf7bce2 100644
+--- a/src/hamster/stats.py
++++ b/src/hamster/stats.py
+@@ -129,7 +129,7 @@ class Stats(gtk.Object):
+     def init_stats(self):
+         self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 2), dt.date.today())
+ 
+-        if not self.stat_facts or self.stat_facts[-1].start_time.year == self.stat_facts[0].start_time.year:
++        if not self.stat_facts:
+             self.get_widget("explore_controls").hide()
+         else:
+             by_year = stuff.totals(self.stat_facts,
+@@ -156,6 +156,9 @@ class Stats(gtk.Object):
+                 for year in years:
+                     year_box.pack_start(YearButton(str(year), year, self.on_year_changed))
+ 
++            if self.stat_facts[-1].start_time.year == self.stat_facts[0].start_time.year:
++                self.get_widget("explore_controls").hide_all()
++            else:
+                 year_box.show_all()
+ 
+ 
+-- 
+2.1.0
+
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/patches/series 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/series
--- 2.91.3+git20120514.b9fec3e1-1/debian/patches/series	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/patches/series	2015-04-07 07:34:00.000000000 +0000
@@ -0,0 +1,6 @@
+01-dont-fail-on-applet-import.patch
+02-dont-traceback-on-quit.patch
+04-lp926481.patch
+05-lp979707.patch
+06-gtg-dbus-update.patch
+07-dont-break-with-only-one-year-of-stats.patch
diff -pruN 2.91.3+git20120514.b9fec3e1-1/debian/rules 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/rules
--- 2.91.3+git20120514.b9fec3e1-1/debian/rules	2012-02-12 08:42:51.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/debian/rules	2013-03-22 21:20:41.000000000 +0000
@@ -10,8 +10,7 @@ binary-install/hamster-applet::
 	cd debian/hamster-applet && \
 		rm -rf usr/lib/bonobo && \
 		rm -f usr/lib/hamster-applet/hamster-applet && \
-		rm -f usr/share/applications/hamster-applet.desktop && \
-		find -name applet.py -o -name applet.ui -delete
+		rm -f usr/share/applications/hamster-applet.desktop
 	dh_python2 -phamster-applet
 	dh_gconf -phamster-applet
 
diff -pruN 2.91.3+git20120514.b9fec3e1-1/.pc/01-dont-fail-on-applet-import.patch/src/hamster/applet.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/01-dont-fail-on-applet-import.patch/src/hamster/applet.py
--- 2.91.3+git20120514.b9fec3e1-1/.pc/01-dont-fail-on-applet-import.patch/src/hamster/applet.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/01-dont-fail-on-applet-import.patch/src/hamster/applet.py	2012-06-25 18:47:07.000000000 +0000
@@ -0,0 +1,713 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2007-2010 Toms Bauģis <toms.baugis at gmail.com>
+# Copyright (C) 2007-2009 Patryk Zawadzki <patrys at pld-linux.org>
+# Copyright (C) 2008 Pēteris Caune <cuu508 at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import datetime as dt
+
+import pygtk
+pygtk.require("2.0")
+import gtk, pango
+
+import gnomeapplet
+import gobject
+import dbus, dbus.service, dbus.mainloop.glib
+import locale
+
+from configuration import conf, runtime, dialogs, load_ui_file
+
+import widgets, idle
+from lib import stuff, trophies
+
+try:
+    import wnck
+except:
+    logging.warning("Could not import wnck - workspace tracking will be disabled")
+    wnck = None
+
+try:
+    import pynotify
+    pynotify.init('Hamster Applet')
+except:
+    logging.warning("Could not import pynotify - notifications will be disabled")
+    pynotify = None
+
+class PanelButton(gtk.ToggleButton):
+    def __init__(self):
+        gtk.ToggleButton.__init__(self)
+        self.set_relief(gtk.RELIEF_NONE)
+        self.set_border_width(0)
+
+        self.label = gtk.Label()
+        self.label.set_justify(gtk.JUSTIFY_CENTER)
+
+        self.label.connect('style-set', self.on_label_style_set)
+        self.connect('size_allocate', self.on_size_allocate)
+        self.connect('button_press_event', self.on_button_press)
+
+        self.add(self.label)
+
+        self.activity, self.duration = None, None
+        self.prev_size = 0
+
+
+        # remove padding, so we fit on small panels (adapted from clock applet)
+        gtk.rc_parse_string ("""style "hamster-applet-button-style" {
+                GtkWidget::focus-line-width=0
+                GtkWidget::focus-padding=0
+            }
+
+            widget "*.hamster-applet-button" style "hamster-applet-button-style"
+        """);
+        gtk.Widget.set_name (self, "hamster-applet-button");
+
+
+    def set_active(self, is_active):
+        self.set_property('active', is_active)
+
+    def set_text(self, activity, duration):
+        label = stuff.escape_pango(activity)
+
+        if len(activity) > 25:  #ellipsize at some random length
+            label = "%s%s" % (stuff.escape_pango(activity[:25]), "&#8230;")
+
+        self.activity = label
+        self.duration = duration
+        self.reformat_label()
+
+    def reformat_label(self):
+        label = self.activity
+        if self.duration:
+            if self.use_two_line_format():
+                label = "%s\n%s" % (self.activity, self.duration)
+            else:
+                label = "%s %s" % (self.activity, self.duration)
+
+        label = '<span gravity="south">%s</span>' % label
+        self.label.set_markup("") #clear - seems to fix the warning
+        self.label.set_markup(label)
+
+    def use_two_line_format(self):
+        if not self.get_parent():
+            return False
+
+        popup_dir = self.get_parent().get_orient()
+
+        orient_vertical = popup_dir in [gnomeapplet.ORIENT_LEFT] or \
+                          popup_dir in [gnomeapplet.ORIENT_RIGHT]
+
+
+        context = self.label.get_pango_context()
+        metrics = context.get_metrics(self.label.style.font_desc,
+                                      pango.Context.get_language(context))
+        ascent = pango.FontMetrics.get_ascent(metrics)
+        descent = pango.FontMetrics.get_descent(metrics)
+
+        if orient_vertical == False:
+            thickness = self.style.ythickness;
+        else:
+            thickness = self.style.xthickness;
+
+        focus_width = self.style_get_property("focus-line-width")
+        focus_pad = self.style_get_property("focus-padding")
+
+        required_size = 2 * ((pango.PIXELS(ascent + descent) ) + 2 * (focus_width + focus_pad + thickness))
+
+        if orient_vertical:
+            available_size = self.get_allocation().width
+        else:
+            available_size = self.get_allocation().height
+
+        return required_size <= available_size
+
+    def on_label_style_set(self, widget, something):
+        self.reformat_label()
+
+    def on_size_allocate(self, widget, allocation):
+        if not self.get_parent():
+            return
+
+        self.popup_dir = self.get_parent().get_orient()
+
+        orient_vertical = True
+        new_size = allocation.width
+        if self.popup_dir in [gnomeapplet.ORIENT_LEFT]:
+            new_angle = 270
+        elif self.popup_dir in [gnomeapplet.ORIENT_RIGHT]:
+            new_angle = 90
+        else:
+            new_angle = 0
+            orient_vertical = False
+            new_size = allocation.height
+
+        if new_angle != self.label.get_angle():
+            self.label.set_angle(new_angle)
+
+        if new_size != self.prev_size:
+            self.reformat_label()
+
+        self.prev_size = new_size
+
+    def on_button_press(self, widget, event):
+        # this allows dragging applet around panel and friends
+        if event.button != 1:
+            widget.stop_emission('button_press_event')
+        return False
+
+
+class HamsterApplet(object):
+    def __init__(self, applet):
+        self.applet = applet
+
+        self.button = PanelButton()
+        self.button.connect('toggled', self.on_toggle)
+        self.applet.add(self.button)
+
+        self.applet.setup_menu_from_file (runtime.data_dir,
+                                          "Hamster_Applet.xml",
+                                          None,
+                                          [("about", self.on_about),
+                                          ("overview", self.show_overview),
+                                          ("preferences", self.show_preferences),
+                                          ("help", self.on_help_clicked),
+                                          ])
+
+        # load window of activity switcher and todays view
+        self._gui = load_ui_file("applet.ui")
+        self.window = self._gui.get_object('hamster-window')
+        # on close don't destroy the popup, just hide it instead
+        self.window.connect("delete_event", lambda *args: self.__show_toggle(False))
+        self.window.connect("window-state-event", self.on_window_state_changed)
+
+        self.new_name = widgets.ActivityEntry()
+        self.new_name.connect("value-entered", self.on_switch_activity_clicked)
+
+        self.new_name.set_property("secondary-icon-name", "help-contents")
+        self.new_name.connect("icon-press", self.on_more_info_button_clicked)
+
+        widgets.add_hint(self.new_name, _("Activity"))
+        self.get_widget("new_name_box").add(self.new_name)
+        self.new_name.connect("changed", self.on_activity_text_changed)
+
+        self.new_tags = widgets.TagsEntry()
+        self.new_tags.connect("tags_selected", self.on_switch_activity_clicked)
+        widgets.add_hint(self.new_tags, _("Tags"))
+        self.get_widget("new_tags_box").add(self.new_tags)
+
+        self.tag_box = widgets.TagBox(interactive = False)
+        self.get_widget("tag_box").add(self.tag_box)
+
+        self.treeview = widgets.FactTree()
+        self.treeview.connect("key-press-event", self.on_todays_keys)
+        self.treeview.connect("edit-clicked", self._open_edit_activity)
+        self.treeview.connect("row-activated", self.on_today_row_activated)
+
+        self.get_widget("today_box").add(self.treeview)
+
+        # DBus Setup
+        try:
+            dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+            # Set up connection to the screensaver
+            self.dbusIdleListener = idle.DbusIdleListener()
+            self.dbusIdleListener.connect('idle-changed', self.on_idle_changed)
+
+        except dbus.DBusException, e:
+            logging.error("Can't init dbus: %s" % e)
+
+        # configuration
+        self.timeout_enabled = conf.get("enable_timeout")
+        self.notify_on_idle = conf.get("notify_on_idle")
+        self.notify_interval = conf.get("notify_interval")
+        self.workspace_tracking = conf.get("workspace_tracking")
+
+        conf.connect('conf-changed', self.on_conf_changed)
+
+        # Load today's data, activities and set label
+        self.last_activity = None
+        self.todays_facts = None
+
+
+        runtime.storage.connect('activities-changed', self.after_activity_update)
+        runtime.storage.connect('facts-changed', self.after_fact_update)
+        runtime.storage.connect('toggle-called', self.on_toggle_called)
+
+        self.screen = None
+        if self.workspace_tracking:
+            self.init_workspace_tracking()
+
+        self.notification = None
+        if pynotify:
+            self.notification = pynotify.Notification("Oh hi",
+                                                      "Greetings from hamster!")
+            self.notification.set_urgency(pynotify.URGENCY_LOW) # lower than grass
+
+        self._gui.connect_signals(self)
+        self.prev_size = None
+
+        self.load_day()
+        gobject.timeout_add_seconds(60, self.refresh_hamster) # refresh hamster every 60 seconds to update duration
+        self.refresh_hamster()
+
+
+
+
+    def init_workspace_tracking(self):
+        if not wnck: # can't track if we don't have the trackable
+            return
+
+        self.screen = wnck.screen_get_default()
+        self.screen.workspace_handler = self.screen.connect("active-workspace-changed", self.on_workspace_changed)
+        self.workspace_activities = {}
+
+    """UI functions"""
+    def refresh_hamster(self):
+        """refresh hamster every x secs - load today, check last activity etc."""
+        try:
+            #if we the day view is visible - update day's durations
+            if self.button.get_active():
+                self.load_day()
+
+            self.update_label()
+            self.check_user()
+            trophies.check_ongoing(self.todays_facts)
+        except Exception, e:
+            logging.error("Error while refreshing: %s" % e)
+        finally:  # we want to go on no matter what, so in case of any error we find out about it sooner
+            return True
+
+
+    def update_label(self):
+        if self.last_activity and self.last_activity.end_time is None:
+            delta = dt.datetime.now() - self.last_activity.start_time
+            duration = delta.seconds /  60
+            label = "%s %s" % (self.last_activity.activity,
+                               stuff.format_duration(duration, False))
+            self.button.set_text(self.last_activity.activity,
+                                 stuff.format_duration(duration, False))
+        else:
+            label = "%s" % _(u"No activity")
+            self.button.set_text(label, None)
+
+
+    def check_user(self):
+        if not self.notification:
+            return
+
+        if self.notify_interval <= 0 or self.notify_interval >= 121:
+            return
+
+        now = dt.datetime.now()
+        message = None
+        if self.last_activity:
+            delta = now - self.last_activity.start_time
+            duration = delta.seconds /  60
+
+            if duration and duration % self.notify_interval == 0:
+                message = self.last_activity.activity
+
+        elif self.notify_on_idle:
+            #if we have no last activity, let's just calculate duration from 00:00
+            if (now.minute + now.hour *60) % self.notify_interval == 0:
+                message = _(u"No activity")
+
+
+        if message:
+            self.notification.update(_("Time Tracker"), message, "hamster-applet")
+            self.notification.show()
+
+
+    def load_day(self):
+        """sets up today's tree and fills it with records
+           returns information about last activity"""
+
+        facts = self.todays_facts = runtime.storage.get_todays_facts()
+
+        if facts and facts[-1].end_time == None:
+            self.last_activity = facts[-1]
+        else:
+            self.last_activity = None
+
+
+        if self.button.get_active():
+            self.treeview.detach_model()
+
+
+            if len(facts) > 15:
+                self._gui.get_object("today_box").set_size_request(-1, 360)
+                self._gui.get_object("today_box").set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
+            else:
+                self._gui.get_object("today_box").set_size_request(-1, -1)
+                self._gui.get_object("today_box").set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_NEVER)
+
+            by_category = {}
+            for fact in facts:
+                duration = 24 * 60 * fact.delta.days + fact.delta.seconds / 60
+                by_category[fact.category] = \
+                              by_category.setdefault(fact.category, 0) + duration
+                self.treeview.add_fact(fact)
+
+            self.treeview.attach_model()
+
+            if not facts:
+                self._gui.get_object("today_box").hide()
+                self._gui.get_object("fact_totals").set_text(_("No records today"))
+            else:
+                self._gui.get_object("today_box").show()
+
+                total_strings = []
+                for category in by_category:
+                    # listing of today's categories and time spent in them
+                    duration = locale.format("%.1f", (by_category[category] / 60.0))
+                    total_strings.append(_("%(category)s: %(duration)s") % \
+                            ({'category': category,
+                              #duration in main drop-down per category in hours
+                              'duration': _("%sh") % duration
+                              }))
+
+                total_string = ", ".join(total_strings)
+                self._gui.get_object("fact_totals").set_text(total_string)
+
+            self.set_last_activity()
+
+    def set_last_activity(self):
+        activity = self.last_activity
+        #sets all the labels and everything as necessary
+        self.get_widget("stop_tracking").set_sensitive(activity != None)
+
+
+        if activity:
+            self.get_widget("switch_activity").show()
+            self.get_widget("start_tracking").hide()
+
+            delta = dt.datetime.now() - activity.start_time
+            duration = delta.seconds /  60
+
+            if activity.category != _("Unsorted"):
+                self.get_widget("last_activity_name").set_text("%s - %s" % (activity.activity, activity.category))
+            else:
+                self.get_widget("last_activity_name").set_text(activity.activity)
+
+
+            self.get_widget("last_activity_duration").set_text(stuff.format_duration(duration) or _("Just started"))
+            self.get_widget("last_activity_description").set_text(activity.description or "")
+            self.get_widget("activity_info_box").show()
+
+            self.tag_box.draw(activity.tags)
+        else:
+            self.get_widget("switch_activity").hide()
+            self.get_widget("start_tracking").show()
+
+            self.get_widget("last_activity_name").set_text(_("No activity"))
+
+            self.get_widget("activity_info_box").hide()
+            self.tag_box.draw([])
+
+
+    def delete_selected(self):
+        fact = self.treeview.get_selected_fact()
+        runtime.storage.remove_fact(fact.id)
+
+    def __show_toggle(self, is_active):
+        """main window display and positioning"""
+        self.button.set_active(is_active)
+
+        if is_active == False:
+            self.window.hide()
+            return True
+
+        self.position_popup()
+
+
+        # doing unstick / stick here, because sometimes while switching
+        # between workplaces window still manages to disappear
+        self.window.unstick()
+        self.window.stick() #show on all desktops
+
+        self.new_name.set_text("");
+        self.new_tags.set_text("");
+        gobject.idle_add(self._delayed_display)
+
+
+    def position_popup(self):
+        label = self.button.get_allocation()
+        window = self.window.get_allocation()
+
+        x, y = self.button.get_parent_window().get_origin()
+
+        self.popup_dir = self.applet.get_orient()
+
+        if self.popup_dir in (gnomeapplet.ORIENT_DOWN, gnomeapplet.ORIENT_UP):
+            if self.popup_dir == gnomeapplet.ORIENT_DOWN:
+                y = y + label.height
+            else:
+                y = y - window.height
+
+            screen_w = self.button.get_screen().get_width()
+            if x + window.width > screen_w:
+                x = screen_w - window.width
+
+        elif self.popup_dir in (gnomeapplet.ORIENT_RIGHT, gnomeapplet.ORIENT_LEFT):
+            if self.popup_dir == gnomeapplet.ORIENT_RIGHT:
+                x = x + label.width
+            else:
+                x = x - window.width
+
+            screen_h = self.button.get_screen().get_height()
+            if y + window.height > screen_h:
+                y = screen_h - window.height
+
+        self.window.move(x, y)
+
+    def _delayed_display(self):
+        """show window only when gtk has become idle. otherwise we get
+        mixed results. TODO - this looks like a hack though"""
+        self.window.show()
+        self.window.present()
+        self.new_name.grab_focus()
+
+        self.load_day() # reload day each time before showing to avoid outdated last activity
+        self.update_label() #update also label, otherwise we can get 1 minute difference in durations (due to timers refreshed once a minute)
+
+
+    """events"""
+    def on_window_state_changed(self, window, event):
+        """untoggle the button when window gets minimized"""
+        if (event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED \
+            and event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED):
+            self.button.set_active(False)
+
+
+    def on_toggle(self, widget):
+        self.__show_toggle(self.button.get_active())
+
+
+    def on_todays_keys(self, tree, event):
+        if (event.keyval == gtk.keysyms.Delete):
+            self.delete_selected()
+            return True
+
+        return False
+
+    def _open_edit_activity(self, row, fact):
+        """opens activity editor for selected row"""
+        dialogs.edit.show(self.applet, fact_id = fact.id)
+
+    def on_today_row_activated(self, tree, path, column):
+        fact = tree.get_selected_fact()
+        fact = stuff.Fact(fact.activity,
+                    tags = ", ".join(fact.tags),
+                    category = fact.category,
+                    description = fact.description)
+
+        if fact.activity:
+            runtime.storage.add_fact(fact)
+            self.__show_toggle(False)
+
+
+    def on_windows_keys(self, tree, event_key):
+        if (event_key.keyval == gtk.keysyms.Escape
+          or (event_key.keyval == gtk.keysyms.w
+              and event_key.state & gtk.gdk.CONTROL_MASK)):
+            if self.new_name.popup.get_property("visible") == False \
+               and self.new_tags.popup.get_property("visible") == False:
+                self.__show_toggle(False)
+                return True
+        return False
+
+    """button events"""
+    def on_overview(self, menu_item):
+        dialogs.overview.show(self.applet)
+        self.__show_toggle(False)
+
+    def show_overview(self, menu_item, verb):
+        return self.on_overview(menu_item)
+
+    def on_custom_fact(self, menu_item):
+        dialogs.edit.show(self.applet)
+
+    def on_about (self, component, verb):
+        dialogs.about.show(self.window)
+
+    def show_preferences(self, menu_item, verb):
+        dialogs.prefs.show(self.applet)
+        self.__show_toggle(False)
+
+
+    """signals"""
+    def after_activity_update(self, widget):
+        self.new_name.refresh_activities()
+        self.load_day()
+        self.update_label()
+
+    def after_fact_update(self, event):
+        self.load_day()
+        self.update_label()
+
+    def on_idle_changed(self, event, state):
+        # state values: 0 = active, 1 = idle
+
+        # refresh when we are out of idle
+        # for example, instantly after coming back from suspend
+        if state == 0:
+            self.refresh_hamster()
+        elif self.timeout_enabled and self.last_activity and \
+             self.last_activity.end_time is None:
+
+            runtime.storage.stop_tracking(self.dbusIdleListener.getIdleFrom())
+
+    def on_workspace_changed(self, screen, previous_workspace):
+        if not previous_workspace:
+            # wnck has a slight hiccup on init and after that calls
+            # workspace changed event with blank previous state that should be
+            # ignored
+            return
+
+        if not self.workspace_tracking:
+            return # default to not doing anything
+
+        current_workspace = screen.get_active_workspace()
+
+        # rely on workspace numbers as names change
+        prev = previous_workspace.get_number()
+        new = current_workspace.get_number()
+
+        # on switch, update our mapping between spaces and activities
+        self.workspace_activities[prev] = self.last_activity
+
+        activity = None
+        if "name" in self.workspace_tracking:
+            # first try to look up activity by desktop name
+            mapping = conf.get("workspace_mapping")
+
+            fact = None
+            if new < len(mapping):
+                fact = stuff.Fact(mapping[new])
+
+                if fact.activity:
+                    category_id = None
+                    if fact.category:
+                        category_id = runtime.storage.get_category_id(fact.category)
+
+                    activity = runtime.storage.get_activity_by_name(fact.activity,
+                                                                    category_id,
+                                                                    resurrect = False)
+                    if activity:
+                        # we need dict below
+                        activity = dict(name = activity.activity,
+                                        category = activity.category,
+                                        description = fact.description,
+                                        tags = fact.tags)
+
+
+        if not activity and "memory" in self.workspace_tracking:
+            # now see if maybe we have any memory of the new workspace
+            # (as in - user was here and tracking Y)
+            # if the new workspace is in our dict, switch to the specified activity
+            if new in self.workspace_activities and self.workspace_activities[new]:
+                activity = self.workspace_activities[new]
+
+        if not activity:
+            return
+
+        # check if maybe there is no need to switch, as field match:
+        if self.last_activity and \
+           self.last_activity.activity.lower() == activity.activity.lower() and \
+           (self.last_activity.category or "").lower() == (activity.category or "").lower() and \
+           ", ".join(self.last_activity.tags).lower() == ", ".join(activity.tags).lower():
+            return
+
+        # ok, switch
+        fact = stuff.Fact(activity.activity,
+                          tags = ", ".join(activity.tags),
+                          category = activity.category,
+                          description = activity.description);
+        runtime.storage.add_fact(fact)
+
+        if self.notification:
+            self.notification.update(_("Changed activity"),
+                                     _("Switched to '%s'") % activity.activity,
+                                     "hamster-applet")
+            self.notification.show()
+
+    def on_toggle_called(self, client):
+        self.__show_toggle(not self.button.get_active())
+
+    def on_conf_changed(self, event, key, value):
+        if key == "enable_timeout":
+            self.timeout_enabled = value
+        elif key == "notify_on_idle":
+            self.notify_on_idle = value
+        elif key == "notify_interval":
+            self.notify_interval = value
+        elif key == "day_start_minutes":
+            self.load_day()
+            self.update_label()
+        elif key == "workspace_tracking":
+            self.workspace_tracking = value
+            if self.workspace_tracking and not self.screen:
+                self.init_workspace_tracking()
+            elif not self.workspace_tracking:
+                if self.screen:
+                    self.screen.disconnect(self.screen.workspace_handler)
+                    self.screen = None
+
+    def on_activity_text_changed(self, widget):
+        self.get_widget("switch_activity").set_sensitive(widget.get_text() != "")
+
+    def on_switch_activity_clicked(self, widget):
+        activity, temporary = self.new_name.get_value()
+
+        fact = stuff.Fact(activity,
+                          tags = self.new_tags.get_text().decode("utf8", "replace"))
+        if not fact.activity:
+            return
+
+        runtime.storage.add_fact(fact, temporary)
+        self.new_name.set_text("")
+        self.new_tags.set_text("")
+        self.__show_toggle(False)
+
+    def on_stop_tracking_clicked(self, widget):
+        runtime.storage.stop_tracking()
+        self.last_activity = None
+        self.__show_toggle(False)
+
+    def on_window_size_request(self, window, event):
+        box = self.window.get_allocation()
+        if self.prev_size and self.prev_size != (box.width, box.height):
+            self.treeview.fix_row_heights()
+            self.position_popup()
+        self.prev_size = (box.width, box.height)
+
+    def show(self):
+        self.window.show_all()
+
+    def get_widget(self, name):
+        return self._gui.get_object(name)
+
+    def on_more_info_button_clicked(self, *args):
+        gtk.show_uri(gtk.gdk.Screen(), "ghelp:hamster-applet#input", 0L)
+        return False
+
+    def on_help_clicked(self, *args):
+        gtk.show_uri(gtk.gdk.Screen(), "ghelp:hamster-applet", 0L)
+
+        trophies.unlock("basic_instructions")
+        return False
diff -pruN 2.91.3+git20120514.b9fec3e1-1/.pc/02-dont-traceback-on-quit.patch/src/hamster/configuration.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/02-dont-traceback-on-quit.patch/src/hamster/configuration.py
--- 2.91.3+git20120514.b9fec3e1-1/.pc/02-dont-traceback-on-quit.patch/src/hamster/configuration.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/02-dont-traceback-on-quit.patch/src/hamster/configuration.py	2012-06-25 18:47:07.000000000 +0000
@@ -0,0 +1,304 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2008 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+gconf part of this code copied from Gimmie (c) Alex Gravely via Conduit (c) John Stowers, 2006
+License: GPLv2
+"""
+
+import gconf
+import os
+from client import Storage
+from xdg.BaseDirectory import xdg_data_home
+import logging
+import datetime as dt
+import gobject, gtk
+
+import logging
+log = logging.getLogger("configuration")
+
+class Singleton(object):
+    def __new__(cls, *args, **kwargs):
+        if '__instance' not in vars(cls):
+            cls.__instance = object.__new__(cls, *args, **kwargs)
+        return cls.__instance
+
+class RuntimeStore(Singleton):
+    """
+    Handles one-shot configuration that is not stored between sessions
+    """
+    database_path = ""
+    database_file = None
+    last_etag = None
+    data_dir = ""
+    home_data_dir = ""
+    storage = None
+    conf = None
+
+
+    def __init__(self):
+        try:
+            import defs
+            self.data_dir = os.path.join(defs.DATA_DIR, "hamster-applet")
+            self.version = defs.VERSION
+        except:
+            # if defs is not there, we are running from sources
+            module_dir = os.path.dirname(os.path.realpath(__file__))
+            self.data_dir = os.path.join(module_dir, '..', '..', 'data')
+            self.version = "uninstalled"
+
+        self.data_dir = os.path.realpath(self.data_dir)
+
+
+        self.storage = Storage()
+
+
+        self.home_data_dir = os.path.realpath(os.path.join(xdg_data_home, "hamster-applet"))
+
+    @property
+    def art_dir(self):
+        return os.path.join(self.data_dir, "art")
+
+
+runtime = RuntimeStore()
+
+
+class OneWindow(object):
+    def __init__(self, get_dialog_class):
+        self.dialogs = {}
+        self.get_dialog_class = get_dialog_class
+        self.dialog_close_handlers = {}
+
+    def on_close_window(self, dialog):
+        for key, assoc_dialog in list(self.dialogs.iteritems()):
+            if dialog == assoc_dialog:
+                del self.dialogs[key]
+
+        handler = self.dialog_close_handlers.pop(dialog)
+        dialog.disconnect(handler)
+
+
+    def show(self, parent = None, **kwargs):
+        params = str(sorted(kwargs.items())) #this is not too safe but will work for most cases
+
+        if params in self.dialogs:
+            window = self.dialogs[params].window
+            self.dialogs[params].show()
+            window.present()
+        else:
+            if parent:
+                dialog = self.get_dialog_class()(parent, **kwargs)
+
+                if isinstance(parent, gtk.Widget):
+                    dialog.window.set_transient_for(parent.get_toplevel())
+
+                self.dialog_close_handlers[dialog] = dialog.connect("on-close", self.on_close_window)
+            else:
+                dialog = self.get_dialog_class()(**kwargs)
+
+                # no parent means we close on window close
+                dialog.window.connect("destroy",
+                                      lambda window, params: gtk.main_quit(),
+                                      params)
+
+            self.dialogs[params] = dialog
+
+class Dialogs(Singleton):
+    """makes sure that we have single instance open for windows where it makes
+       sense"""
+    def __init__(self):
+        def get_edit_class():
+            from edit_activity import CustomFactController
+            return CustomFactController
+        self.edit = OneWindow(get_edit_class)
+
+        def get_overview_class():
+            from overview import Overview
+            return Overview
+        self.overview = OneWindow(get_overview_class)
+
+        def get_stats_class():
+            from stats import Stats
+            return Stats
+        self.stats = OneWindow(get_stats_class)
+
+        def get_about_class():
+            from about import About
+            return About
+        self.about = OneWindow(get_about_class)
+
+        def get_prefs_class():
+            from preferences import PreferencesEditor
+            return PreferencesEditor
+        self.prefs = OneWindow(get_prefs_class)
+
+dialogs = Dialogs()
+
+
+def load_ui_file(name):
+    ui = gtk.Builder()
+    ui.add_from_file(os.path.join(runtime.data_dir, name))
+    return ui
+
+class GConfStore(gobject.GObject, Singleton):
+    """
+    Settings implementation which stores settings in GConf
+    Snatched from the conduit project (http://live.gnome.org/Conduit)
+    """
+    GCONF_DIR = "/apps/hamster-applet/"
+    VALID_KEY_TYPES = (bool, str, int, list, tuple)
+    DEFAULTS = {
+        'enable_timeout'              :   False,       # Should hamster stop tracking on idle
+        'stop_on_shutdown'            :   False,       # Should hamster stop tracking on shutdown
+        'notify_on_idle'              :   False,       # Remind also if no activity is set
+        'notify_interval'             :   27,          # Remind of current activity every X minutes
+        'day_start_minutes'           :   5 * 60 + 30, # At what time does the day start (5:30AM)
+        'overview_window_box'         :   [],          # X, Y, W, H
+        'overview_window_maximized'   :   False,       # Is overview window maximized
+        'workspace_tracking'          :   [],          # Should hamster switch activities on workspace change 0,1,2
+        'workspace_mapping'           :   [],          # Mapping between workspace numbers and activities
+        'standalone_window_box'       :   [],          # X, Y, W, H
+        'standalone_window_maximized' :   False,       # Is overview window maximized
+        'activities_source'           :   "",          # Source of TODO items ("", "evo", "gtg")
+        'last_report_folder'          :   "~",         # Path to directory where the last report was saved
+    }
+
+    __gsignals__ = {
+        "conf-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT))
+    }
+    def __init__(self):
+        gobject.GObject.__init__(self)
+        self._client = gconf.client_get_default()
+        self._client.add_dir(self.GCONF_DIR[:-1], gconf.CLIENT_PRELOAD_RECURSIVE)
+        self._notifications = []
+
+    def _fix_key(self, key):
+        """
+        Appends the GCONF_PREFIX to the key if needed
+
+        @param key: The key to check
+        @type key: C{string}
+        @returns: The fixed key
+        @rtype: C{string}
+        """
+        if not key.startswith(self.GCONF_DIR):
+            return self.GCONF_DIR + key
+        else:
+            return key
+
+    def _key_changed(self, client, cnxn_id, entry, data=None):
+        """
+        Callback when a gconf key changes
+        """
+        key = self._fix_key(entry.key)[len(self.GCONF_DIR):]
+        value = self._get_value(entry.value, self.DEFAULTS[key])
+
+        self.emit('conf-changed', key, value)
+
+
+    def _get_value(self, value, default):
+        """calls appropriate gconf function by the default value"""
+        vtype = type(default)
+
+        if vtype is bool:
+            return value.get_bool()
+        elif vtype is str:
+            return value.get_string()
+        elif vtype is int:
+            return value.get_int()
+        elif vtype in (list, tuple):
+            l = []
+            for i in value.get_list():
+                l.append(i.get_string())
+            return l
+
+        return None
+
+    def get(self, key, default=None):
+        """
+        Returns the value of the key or the default value if the key is
+        not yet in gconf
+        """
+
+        #function arguments override defaults
+        if default is None:
+            default = self.DEFAULTS.get(key, None)
+        vtype = type(default)
+
+        #we now have a valid key and type
+        if default is None:
+            log.warn("Unknown key: %s, must specify default value" % key)
+            return None
+
+        if vtype not in self.VALID_KEY_TYPES:
+            log.warn("Invalid key type: %s" % vtype)
+            return None
+
+        #for gconf refer to the full key path
+        key = self._fix_key(key)
+
+        if key not in self._notifications:
+            self._client.notify_add(key, self._key_changed)
+            self._notifications.append(key)
+
+        value = self._client.get(key)
+        if value is None:
+            self.set(key, default)
+            return default
+
+        value = self._get_value(value, default)
+        if value is not None:
+            return value
+
+        log.warn("Unknown gconf key: %s" % key)
+        return None
+
+    def set(self, key, value):
+        """
+        Sets the key value in gconf and connects adds a signal
+        which is fired if the key changes
+        """
+        log.debug("Settings %s -> %s" % (key, value))
+        if key in self.DEFAULTS:
+            vtype = type(self.DEFAULTS[key])
+        else:
+            vtype = type(value)
+
+        if vtype not in self.VALID_KEY_TYPES:
+            log.warn("Invalid key type: %s" % vtype)
+            return False
+
+        #for gconf refer to the full key path
+        key = self._fix_key(key)
+
+        if vtype is bool:
+            self._client.set_bool(key, value)
+        elif vtype is str:
+            self._client.set_string(key, value)
+        elif vtype is int:
+            self._client.set_int(key, value)
+        elif vtype in (list, tuple):
+            #Save every value as a string
+            strvalues = [str(i) for i in value]
+            self._client.set_list(key, gconf.VALUE_STRING, strvalues)
+
+        return True
+
+
+conf = GConfStore()
diff -pruN 2.91.3+git20120514.b9fec3e1-1/.pc/04-lp926481.patch/src/hamster/widgets/dayline.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/04-lp926481.patch/src/hamster/widgets/dayline.py
--- 2.91.3+git20120514.b9fec3e1-1/.pc/04-lp926481.patch/src/hamster/widgets/dayline.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/04-lp926481.patch/src/hamster/widgets/dayline.py	2012-06-25 18:47:07.000000000 +0000
@@ -0,0 +1,375 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2007-2010 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+import gtk
+import gobject
+
+import time
+import datetime as dt
+
+from ..lib import stuff, graphics, pytweener
+from ..configuration import conf
+
+
+class Selection(graphics.Sprite):
+    def __init__(self, start_time = None, end_time = None):
+        graphics.Sprite.__init__(self, z_order = 100)
+        self.start_time, self.end_time  = None, None
+        self.width, self.height = None, None
+        self.fill = None # will be set to proper theme color on render
+        self.fixed = False
+
+        self.start_label = graphics.Label("", 8, "#333", visible = False)
+        self.end_label = graphics.Label("", 8, "#333", visible = False)
+        self.duration_label = graphics.Label("", 8, "#FFF", visible = False)
+
+        self.add_child(self.start_label, self.end_label, self.duration_label)
+        self.connect("on-render", self.on_render)
+
+
+    def on_render(self, sprite):
+        if not self.fill: # not ready yet
+            return
+
+        self.graphics.rectangle(0, 0, self.width, self.height)
+        self.graphics.fill(self.fill, 0.3)
+
+        self.graphics.rectangle(0.5, 0.5, self.width, self.height)
+        self.graphics.stroke(self.fill)
+
+
+        # adjust labels
+        self.start_label.visible = self.fixed == False and self.start_time is not None
+        if self.start_label.visible:
+            self.start_label.text = self.start_time.strftime("%H:%M")
+            if self.x - self.start_label.width - 5 > 0:
+                self.start_label.x = -self.start_label.width - 5
+            else:
+                self.start_label.x = 5
+
+            self.start_label.y = self.height
+
+        self.end_label.visible = self.fixed == False and self.end_time is not None
+        if self.end_label.visible:
+            self.end_label.text = self.end_time.strftime("%H:%M")
+            self.end_label.x = self.width + 5
+            self.end_label.y = self.height
+
+
+
+            duration = self.end_time - self.start_time
+            duration = int(duration.seconds / 60)
+            self.duration_label.text =  "%02d:%02d" % (duration / 60, duration % 60)
+
+            self.duration_label.visible = self.duration_label.width < self.width
+            if self.duration_label.visible:
+                self.duration_label.y = (self.height - self.duration_label.height) / 2
+                self.duration_label.x = (self.width - self.duration_label.width) / 2
+        else:
+            self.duration_label.visible = False
+
+
+
+class DayLine(graphics.Scene):
+    __gsignals__ = {
+        "on-time-chosen": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+    }
+
+    def __init__(self, start_time = None):
+        graphics.Scene.__init__(self)
+
+
+
+        day_start = conf.get("day_start_minutes")
+        self.day_start = dt.time(day_start / 60, day_start % 60)
+
+        self.view_time = start_time or dt.datetime.combine(dt.date.today(), self.day_start)
+
+        self.scope_hours = 24
+
+
+        self.fact_bars = []
+        self.categories = []
+
+        self.connect("on-enter-frame", self.on_enter_frame)
+        self.connect("on-mouse-move", self.on_mouse_move)
+        self.connect("on-mouse-down", self.on_mouse_down)
+        self.connect("on-mouse-up", self.on_mouse_up)
+        self.connect("on-click", self.on_click)
+
+
+        self.plot_area = graphics.Sprite()
+
+        self.selection = Selection()
+        self.chosen_selection = Selection()
+
+        self.plot_area.add_child(self.selection, self.chosen_selection)
+
+        self.drag_start = None
+        self.current_x = None
+        self.snap_points = []
+
+        self.add_child(self.plot_area)
+
+
+    def plot(self, date, facts, select_start, select_end = None):
+        for bar in self.fact_bars:
+            self.plot_area.sprites.remove(bar)
+
+        self.fact_bars = []
+        for fact in facts:
+            fact_bar = graphics.Rectangle(0, 0, fill = "#aaa", stroke="#aaa") # dimensions will depend on screen situation
+            fact_bar.fact = fact
+
+            if fact.category in self.categories:
+                fact_bar.category = self.categories.index(fact.category)
+            else:
+                fact_bar.category = len(self.categories)
+                self.categories.append(fact.category)
+
+            self.plot_area.add_child(fact_bar)
+            self.fact_bars.append(fact_bar)
+
+        self.view_time = dt.datetime.combine((select_start - dt.timedelta(hours=self.day_start.hour, minutes=self.day_start.minute)).date(), self.day_start)
+
+        if select_start and select_start > dt.datetime.now():
+            select_start = dt.datetime.now()
+        self.chosen_selection.start_time = select_start
+
+        if select_end and select_end > dt.datetime.now():
+            select_end = dt.datetime.now()
+        self.chosen_selection.end_time = select_end
+
+        self.chosen_selection.width = None
+        self.chosen_selection.fixed = True
+        self.chosen_selection.visible = True
+
+        self.redraw()
+
+
+    def on_mouse_down(self, scene, event):
+        self.drag_start = self.current_x
+        self.chosen_selection.visible = False
+
+    def on_mouse_up(self, scene, event):
+        if self.drag_start:
+            self.drag_start = None
+
+            start_time = self.selection.start_time
+            if start_time > dt.datetime.now():
+                start_time = dt.datetime.now()
+
+            end_time = self.selection.end_time
+            self.new_selection()
+            self.emit("on-time-chosen", start_time, end_time)
+
+    def on_click(self, scene, event, target):
+        self.drag_start = None
+
+        start_time = self.selection.start_time
+        if start_time > dt.datetime.now():
+            start_time = dt.datetime.now()
+
+
+        end_time = None
+        if self.fact_bars:
+            times = [bar.fact.start_time for bar in self.fact_bars if bar.fact.start_time - start_time > dt.timedelta(minutes=5)]
+            times.extend([bar.fact.start_time + bar.fact.delta for bar in self.fact_bars if bar.fact.start_time + bar.fact.delta - start_time  > dt.timedelta(minutes=5)])
+            if times:
+                end_time = min(times)
+
+        self.new_selection()
+
+        self.emit("on-time-chosen", start_time, end_time)
+
+
+    def new_selection(self):
+        self.plot_area.sprites.remove(self.selection)
+        self.selection = Selection()
+        self.plot_area.add_child(self.selection)
+        self.redraw()
+
+    def on_mouse_move(self, scene, event):
+        if self.current_x:
+            active_bar = None
+            # find if we are maybe on a bar
+            for bar in self.fact_bars:
+                if bar.x < self.current_x < bar.x + bar.width:
+                    active_bar = bar
+                    break
+
+            if active_bar:
+                self.set_tooltip_text("%s - %s" % (active_bar.fact.activity, active_bar.fact.category))
+            else:
+                self.set_tooltip_text("")
+
+        self.redraw()
+
+
+    def on_enter_frame(self, scene, context):
+        g = graphics.Graphics(context)
+
+        self.plot_area.y = 15.5
+        self.plot_area.height = self.height - 30
+
+
+
+
+        vertical = min(self.plot_area.height / 5 , 7)
+        minute_pixel = (self.scope_hours * 60.0 - 15) / self.width
+
+        snap_points = []
+
+        g.set_line_style(width=1)
+
+
+
+
+        bottom = self.plot_area.y + self.plot_area.height
+
+        for bar in self.fact_bars:
+            bar.y = vertical * bar.category + 5
+            bar.height = vertical
+
+            bar_start_time = bar.fact.start_time - self.view_time
+            minutes = bar_start_time.seconds / 60 + bar_start_time.days * self.scope_hours  * 60
+
+            bar.x = round(minutes / minute_pixel) + 0.5
+            bar.width = round((bar.fact.delta).seconds / 60 / minute_pixel)
+
+            if not snap_points or bar.x - snap_points[-1][0] > 1:
+                snap_points.append((bar.x, bar.fact.start_time))
+
+            if not snap_points or bar.x + bar.width - snap_points[-1][0] > 1:
+                snap_points.append((bar.x + bar.width, bar.fact.start_time + bar.fact.delta))
+
+        self.snap_points = snap_points
+
+
+        if self.chosen_selection.start_time and self.chosen_selection.width is None:
+            # we have time but no pixels
+            minutes = round((self.chosen_selection.start_time - self.view_time).seconds / 60 / minute_pixel) + 0.5
+            self.chosen_selection.x = minutes
+            if self.chosen_selection.end_time:
+                self.chosen_selection.width = round((self.chosen_selection.end_time - self.chosen_selection.start_time).seconds / 60 / minute_pixel)
+            else:
+                self.chosen_selection.width = 0
+            self.chosen_selection.height = self.chosen_selection.parent.height
+
+            # use the oportunity to set proper colors too
+            self.chosen_selection.fill = self.get_style().bg[gtk.STATE_SELECTED].to_string()
+            self.chosen_selection.duration_label.color = self.get_style().fg[gtk.STATE_SELECTED].to_string()
+
+
+        self.selection.visible = self._mouse_in # TODO - think harder about the mouse_out event
+
+        self.selection.width = 0
+        self.selection.height = self.selection.parent.height
+        if self.mouse_x:
+            start_x = max(min(self.mouse_x, self.width-1), 0) #mouse, but within screen regions
+
+            # check for snap points
+            start_x = start_x + 0.5
+            minutes = int(round(start_x * minute_pixel / 15)) * 15
+            start_time = self.view_time + dt.timedelta(hours = minutes / 60, minutes = minutes % 60)
+
+            if snap_points:
+                delta, closest_snap, time = min((abs(start_x - i), i, time) for i, time in snap_points)
+
+
+                if abs(closest_snap - start_x) < 5 and (not self.drag_start or self.drag_start != closest_snap):
+                    start_x = closest_snap
+                    minutes = (time.hour - self.day_start.hour) * 60 + time.minute - self.day_start.minute
+                    start_time = time
+
+
+            self.current_x = minutes / minute_pixel
+
+
+            end_time, end_x = None, None
+            if self.drag_start:
+                minutes = int(self.drag_start * minute_pixel)
+                end_time =  self.view_time + dt.timedelta(hours = minutes / 60, minutes = minutes % 60)
+                end_x = round(self.drag_start) + 0.5
+
+            if end_time and end_time < start_time:
+                start_time, end_time = end_time, start_time
+                start_x, end_x = end_x, start_x
+
+
+            self.selection.start_time = start_time
+            self.selection.end_time = end_time
+
+            self.selection.x = start_x
+            if end_time:
+                self.selection.width = end_x - start_x
+
+            self.selection.y = 0
+
+            self.selection.fill = self.get_style().bg[gtk.STATE_SELECTED].to_string()
+            self.selection.duration_label.color = self.get_style().fg[gtk.STATE_SELECTED].to_string()
+
+
+
+        #time scale
+        g.set_color("#000")
+
+        background = self.get_style().bg[gtk.STATE_NORMAL].to_string()
+        text = self.get_style().text[gtk.STATE_NORMAL].to_string()
+
+        tick_color = g.colors.contrast(background, 80)
+
+        layout = g.create_layout(size = 8)
+        for i in range(self.scope_hours * 60):
+            label_time = (self.view_time + dt.timedelta(minutes=i))
+
+            g.set_color(tick_color)
+            if label_time.minute == 0:
+                g.move_to(round(i / minute_pixel) + 0.5, bottom - 15)
+                g.line_to(round(i / minute_pixel) + 0.5, bottom)
+                g.stroke()
+            elif label_time.minute % 15 == 0:
+                g.move_to(round(i / minute_pixel) + 0.5, bottom - 5)
+                g.line_to(round(i / minute_pixel) + 0.5, bottom)
+                g.stroke()
+
+
+
+            if label_time.minute == 0 and label_time.hour % 4 == 0:
+                if label_time.hour == 0:
+                    g.move_to(round(i / minute_pixel) + 0.5, self.plot_area.y)
+                    g.line_to(round(i / minute_pixel) + 0.5, bottom)
+                    label_minutes = label_time.strftime("%b %d")
+                else:
+                    label_minutes = label_time.strftime("%H<small><sup>%M</sup></small>")
+
+                g.set_color(text)
+                layout.set_markup(label_minutes)
+                label_w, label_h = layout.get_pixel_size()
+
+                g.move_to(round(i / minute_pixel) + 2, 0)
+                context.show_layout(layout)
+
+        #current time
+        if self.view_time < dt.datetime.now() < self.view_time + dt.timedelta(hours = self.scope_hours):
+            minutes = round((dt.datetime.now() - self.view_time).seconds / 60 / minute_pixel) + 0.5
+            g.move_to(minutes, self.plot_area.y)
+            g.line_to(minutes, bottom)
+            g.stroke("#f00", 0.4)
+            snap_points.append(minutes - 0.5)
diff -pruN 2.91.3+git20120514.b9fec3e1-1/.pc/05-lp979707.patch/src/hamster/overview.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/05-lp979707.patch/src/hamster/overview.py
--- 2.91.3+git20120514.b9fec3e1-1/.pc/05-lp979707.patch/src/hamster/overview.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/05-lp979707.patch/src/hamster/overview.py	2012-06-25 18:47:07.000000000 +0000
@@ -0,0 +1,436 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import pygtk
+pygtk.require('2.0')
+
+import os
+import datetime as dt
+import calendar
+import webbrowser
+
+import gtk, gobject
+import pango
+
+import widgets, reports
+from configuration import runtime, conf, dialogs, load_ui_file
+from lib import stuff, trophies
+from lib.i18n import C_
+
+from overview_activities import OverviewBox
+from overview_totals import TotalsBox
+
+
+class Overview(gtk.Object):
+    __gsignals__ = {
+        "on-close": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+
+    def __init__(self, parent = None):
+        gtk.Object.__init__(self)
+
+        self.parent = parent# determine if app should shut down on close
+        self._gui = load_ui_file("overview.ui")
+        self.report_chooser = None
+        self.window = self.get_widget("tabs_window")
+        self.window.connect("delete_event", self.on_delete_window)
+
+        self.range_pick = widgets.RangePick()
+        self.get_widget("range_pick_box").add(self.range_pick)
+        self.range_pick.connect("range-selected", self.on_range_selected)
+
+        self.overview = OverviewBox()
+        self.get_widget("overview_tab").add(self.overview)
+        self.fact_tree = self.overview.fact_tree # TODO - this is upside down, should maybe get the overview tab over here
+        self.fact_tree.connect("cursor-changed", self.on_fact_selection_changed)
+
+        self.fact_tree.connect("button-press-event", self.on_fact_tree_button_press)
+
+        self.reports = TotalsBox()
+        self.get_widget("reports_tab").add(self.reports)
+
+        self.timechart = widgets.TimeChart()
+        self.timechart.connect("zoom-out-clicked", self.on_timechart_zoom_out_clicked)
+        self.timechart.connect("range-picked", self.on_timechart_new_range)
+        self.get_widget("by_day_box").add(self.timechart)
+
+        self._gui.connect_signals(self)
+
+        self.external_listeners = [
+            (runtime.storage, runtime.storage.connect('activities-changed',self.after_activity_update)),
+            (runtime.storage, runtime.storage.connect('facts-changed',self.after_activity_update)),
+            (conf, conf.connect('conf-changed', self.on_conf_change))
+        ]
+        self.show()
+
+
+    def show(self):
+        self.position_window()
+        self.window.show_all()
+
+        self.facts = None
+
+        self.day_start = conf.get("day_start_minutes")
+        self.day_start = dt.time(self.day_start / 60, self.day_start % 60)
+
+        self.view_date = (dt.datetime.today() - dt.timedelta(hours = self.day_start.hour,
+                                                        minutes = self.day_start.minute)).date()
+
+        #set to monday
+        self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1)
+
+        # look if we need to start on sunday or monday
+        self.start_date = self.start_date + dt.timedelta(stuff.locale_first_weekday())
+
+        # see if we have not gotten carried away too much in all these calculations
+        if (self.view_date - self.start_date) == dt.timedelta(7):
+            self.start_date += dt.timedelta(7)
+
+        self.end_date = self.start_date + dt.timedelta(6)
+
+        self.current_range = "week"
+
+        self.timechart.day_start = self.day_start
+
+
+        self.search()
+
+    def position_window(self):
+        if conf.get("overview_window_maximized"):
+            self.window.maximize()
+        else:
+            window_box = conf.get("overview_window_box")
+            if window_box:
+                x, y, w, h = (int(i) for i in window_box)
+                self.window.move(x, y)
+                self.window.resize(w, h)
+            else:
+                self.window.set_position(gtk.WIN_POS_CENTER)
+
+
+    def on_fact_tree_button_press(self, treeview, event):
+        if event.button == 3:
+            x = int(event.x)
+            y = int(event.y)
+            time = event.time
+            pthinfo = treeview.get_path_at_pos(x, y)
+            if pthinfo is not None:
+                path, col, cellx, celly = pthinfo
+                treeview.grab_focus()
+                treeview.set_cursor( path, col, 0)
+                self.get_widget("fact_tree_popup").popup( None, None, None, event.button, time)
+            return True
+
+    def on_timechart_new_range(self, chart, start_date, end_date):
+        self.start_date = start_date
+        self.end_date = end_date
+        self.apply_range_select()
+
+    def on_timechart_zoom_out_clicked(self, chart):
+        if (self.end_date - self.start_date < dt.timedelta(days=6)):
+            self.on_week_activate(None)
+        elif (self.end_date - self.start_date < dt.timedelta(days=27)):
+            self.on_month_activate(None)
+        else:
+            self.current_range = "manual"
+            self.start_date = self.view_date.replace(day=1, month=1)
+            self.end_date = self.start_date.replace(year = self.start_date.year + 1) - dt.timedelta(days=1)
+            self.apply_range_select()
+
+
+    def search(self):
+        if self.start_date > self.end_date: # make sure the end is always after beginning
+            self.start_date, self.end_date = self.end_date, self.start_date
+
+        search_terms = self.get_widget("search").get_text().decode("utf8", "replace")
+        self.facts = runtime.storage.get_facts(self.start_date, self.end_date, search_terms)
+
+        self.get_widget("export").set_sensitive(len(self.facts) > 0)
+
+        self.set_title()
+
+        self.range_pick.set_range(self.start_date, self.end_date, self.view_date)
+
+        durations = [(fact.start_time, fact.delta) for fact in self.facts]
+        self.timechart.draw(durations, self.start_date, self.end_date)
+
+        if self.get_widget("window_tabs").get_current_page() == 0:
+            self.overview.search(self.start_date, self.end_date, self.facts)
+            self.reports.search(self.start_date, self.end_date, self.facts)
+        else:
+            self.reports.search(self.start_date, self.end_date, self.facts)
+            self.overview.search(self.start_date, self.end_date, self.facts)
+
+    def set_title(self):
+        self.title = stuff.format_range(self.start_date, self.end_date)
+        self.window.set_title(self.title.decode("utf-8"))
+
+
+    def on_conf_change(self, event, key, value):
+        if key == "day_start_minutes":
+            self.day_start = dt.time(value / 60, value % 60)
+            self.timechart.day_start = self.day_start
+            self.search()
+
+    def on_fact_selection_changed(self, tree):
+        """ enables and disables action buttons depending on selected item """
+        fact = tree.get_selected_fact()
+        real_fact = fact is not None and isinstance(fact, stuff.Fact)
+
+        self.get_widget('remove').set_sensitive(real_fact)
+        self.get_widget('edit').set_sensitive(real_fact)
+
+        return True
+
+    def after_activity_update(self, widget):
+        self.search()
+
+
+    def on_search_icon_press(self, widget, position, data):
+        if position == gtk.ENTRY_ICON_SECONDARY:
+            widget.set_text('')
+
+        self.search()
+
+    def on_search_activate(self, widget):
+        self.search()
+
+    def on_search_changed(self, widget):
+        has_text = len(widget.get_text()) > 0
+        widget.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, has_text)
+
+    def on_export_activate(self, widget):
+        def on_report_chosen(widget, format, path):
+            self.report_chooser = None
+            reports.simple(self.facts, self.start_date, self.end_date, format, path)
+
+            if format == ("html"):
+                webbrowser.open_new("file://%s" % path)
+            else:
+                try:
+                    gtk.show_uri(gtk.gdk.Screen(), "file://%s" % os.path.split(path)[0], 0L)
+                except:
+                    pass # bug 626656 - no use in capturing this one i think
+
+        def on_report_chooser_closed(widget):
+            self.report_chooser = None
+
+        if not self.report_chooser:
+            self.report_chooser = widgets.ReportChooserDialog()
+            self.report_chooser.connect("report-chosen", on_report_chosen)
+            self.report_chooser.connect("report-chooser-closed",
+                                        on_report_chooser_closed)
+            self.report_chooser.show(self.start_date, self.end_date)
+        else:
+            self.report_chooser.present()
+
+
+    def apply_range_select(self):
+        if self.view_date < self.start_date:
+            self.view_date = self.start_date
+
+        if self.view_date > self.end_date:
+            self.view_date = self.end_date
+
+        self.search()
+
+
+    def on_range_selected(self, widget, range, start, end):
+        self.current_range = range
+        self.start_date = start
+        self.end_date = end
+        self.apply_range_select()
+
+    def on_day_activate(self, button):
+        self.current_range = "day"
+        self.start_date = self.view_date
+        self.end_date = self.start_date + dt.timedelta(0)
+        self.apply_range_select()
+
+    def on_week_activate(self, button):
+        self.current_range = "week"
+        self.start_date, self.end_date = stuff.week(self.view_date)
+        self.apply_range_select()
+
+    def on_month_activate(self, button):
+        self.current_range = "month"
+        self.start_date, self.end_date = stuff.month(self.view_date)
+        self.apply_range_select()
+
+    def on_manual_range_apply_clicked(self, button):
+        self.current_range = "manual"
+        cal_date = self.get_widget("start_calendar").get_date()
+        self.start_date = dt.date(cal_date[0], cal_date[1] + 1, cal_date[2])
+
+        cal_date = self.get_widget("end_calendar").get_date()
+        self.end_date = dt.date(cal_date[0], cal_date[1] + 1, cal_date[2])
+
+        self.apply_range_select()
+
+
+    def on_tabs_window_configure_event(self, window, event):
+        # this is required so that the rows would grow on resize
+        self.fact_tree.fix_row_heights()
+
+    def on_tabs_window_state_changed(self, window, event):
+        # not enough space - maximized the overview window
+        maximized = window.get_window().get_state() & gtk.gdk.WINDOW_STATE_MAXIMIZED
+        if maximized:
+            trophies.unlock("not_enough_space")
+
+
+
+
+    def on_prev_activate(self, action):
+        if self.current_range == "day":
+            self.start_date -= dt.timedelta(1)
+            self.end_date -= dt.timedelta(1)
+        elif self.current_range == "week":
+            self.start_date -= dt.timedelta(7)
+            self.end_date -= dt.timedelta(7)
+        elif self.current_range == "month":
+            self.end_date = self.start_date - dt.timedelta(1)
+            first_weekday, days_in_month = calendar.monthrange(self.end_date.year, self.end_date.month)
+            self.start_date = self.end_date - dt.timedelta(days_in_month - 1)
+        else:
+            # manual range - just jump to the next window
+            days =  (self.end_date - self.start_date) + dt.timedelta(days = 1)
+            self.start_date = self.start_date - days
+            self.end_date = self.end_date - days
+
+        self.view_date = self.start_date
+        self.search()
+
+    def on_next_activate(self, action):
+        if self.current_range == "day":
+            self.start_date += dt.timedelta(1)
+            self.end_date += dt.timedelta(1)
+        elif self.current_range == "week":
+            self.start_date += dt.timedelta(7)
+            self.end_date += dt.timedelta(7)
+        elif self.current_range == "month":
+            self.start_date = self.end_date + dt.timedelta(1)
+            first_weekday, days_in_month = calendar.monthrange(self.start_date.year, self.start_date.month)
+            self.end_date = self.start_date + dt.timedelta(days_in_month - 1)
+        else:
+            # manual range - just jump to the next window
+            days =  (self.end_date - self.start_date) + dt.timedelta(days = 1)
+            self.start_date = self.start_date + days
+            self.end_date = self.end_date + days
+
+        self.view_date = self.start_date
+        self.search()
+
+
+    def on_home_activate(self, action):
+        self.view_date = (dt.datetime.today() - dt.timedelta(hours = self.day_start.hour,
+                                                        minutes = self.day_start.minute)).date()
+        if self.current_range == "day":
+            self.start_date = self.view_date
+            self.end_date = self.start_date + dt.timedelta(0)
+
+        elif self.current_range == "week":
+            self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1)
+            self.start_date = self.start_date + dt.timedelta(stuff.locale_first_weekday())
+            self.end_date = self.start_date + dt.timedelta(6)
+        elif self.current_range == "month":
+            self.start_date = self.view_date - dt.timedelta(self.view_date.day - 1) #set to beginning of month
+            first_weekday, days_in_month = calendar.monthrange(self.view_date.year, self.view_date.month)
+            self.end_date = self.start_date + dt.timedelta(days_in_month - 1)
+        else:
+            days =  (self.end_date - self.start_date)
+            self.start_date = self.view_date
+            self.end_date = self.view_date + days
+
+        self.search()
+
+    def get_widget(self, name):
+        """ skip one variable (huh) """
+        return self._gui.get_object(name)
+
+    def on_window_tabs_switch_page(self, notebook, page, pagenum):
+        if pagenum == 0:
+            self.on_fact_selection_changed(self.fact_tree)
+        elif pagenum == 1:
+            self.get_widget('remove').set_sensitive(False)
+            self.get_widget('edit').set_sensitive(False)
+            self.reports.do_charts()
+
+
+    def on_add_activate(self, action):
+        fact = self.fact_tree.get_selected_fact()
+        if not fact:
+            selected_date = self.start_date
+        elif isinstance(fact, dt.date):
+            selected_date = fact
+        else:
+            selected_date = fact["date"]
+
+        dialogs.edit.show(self, fact_date = selected_date)
+
+    def on_remove_activate(self, button):
+        self.overview.delete_selected()
+
+
+    def on_edit_activate(self, button):
+        fact = self.fact_tree.get_selected_fact()
+        if not fact or isinstance(fact, dt.date):
+            return
+        dialogs.edit.show(self, fact_id = fact.id)
+
+    def on_tabs_window_deleted(self, widget, event):
+        self.close_window()
+
+    def on_window_key_pressed(self, tree, event_key):
+      if (event_key.keyval == gtk.keysyms.Escape
+          or (event_key.keyval == gtk.keysyms.w
+              and event_key.state & gtk.gdk.CONTROL_MASK)):
+        self.close_window()
+
+    def on_close_activate(self, action):
+        self.close_window()
+
+    def close_window(self):
+        # properly saving window state and position
+        maximized = self.window.get_window().get_state() & gtk.gdk.WINDOW_STATE_MAXIMIZED
+        conf.set("overview_window_maximized", maximized)
+
+        # make sure to remember dimensions only when in normal state
+        if maximized == False and not self.window.get_window().get_state() & gtk.gdk.WINDOW_STATE_ICONIFIED:
+            x, y = self.window.get_position()
+            w, h = self.window.get_size()
+            conf.set("overview_window_box", [x, y, w, h])
+
+        if not self.parent:
+            gtk.main_quit()
+        else:
+            for obj, handler in self.external_listeners:
+                obj.disconnect(handler)
+            self._gui = None
+            self.window.destroy()
+            self.window = None
+
+            self.emit("on-close")
+
+
+
+    def on_delete_window(self, window, event):
+        self.close_window()
+        return True
diff -pruN 2.91.3+git20120514.b9fec3e1-1/.pc/06-gtg-dbus-update.patch/src/hamster/external.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/06-gtg-dbus-update.patch/src/hamster/external.py
--- 2.91.3+git20120514.b9fec3e1-1/.pc/06-gtg-dbus-update.patch/src/hamster/external.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/06-gtg-dbus-update.patch/src/hamster/external.py	2012-06-25 18:47:07.000000000 +0000
@@ -0,0 +1,111 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2007 Patryk Zawadzki <patrys at pld-linux.org>
+# Copyright (C) 2008, 2010 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+from configuration import conf
+import gobject
+import dbus, dbus.mainloop.glib
+
+try:
+    import evolution
+    from evolution import ecal
+except:
+    evolution = None
+
+class ActivitiesSource(gobject.GObject):
+    def __init__(self):
+        gobject.GObject.__init__(self)
+        self.source = conf.get("activities_source")
+        self.__gtg_connection = None
+
+        if self.source == "evo" and not evolution:
+            self.source == "" # on failure pretend that there is no evolution
+        elif self.source == "gtg":
+            gobject.GObject.__init__(self)
+            dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
+    def get_activities(self, query = None):
+        if not self.source:
+            return []
+
+        if self.source == "evo":
+            return [activity for activity in get_eds_tasks()
+                         if query is None or activity['name'].startswith(query)]
+
+        elif self.source == "gtg":
+            conn = self.__get_gtg_connection()
+            if not conn:
+                return []
+
+            activities = []
+
+            tasks = []
+            try:
+                tasks = conn.get_tasks()
+            except dbus.exceptions.DBusException:  #TODO too lame to figure out how to connect to the disconnect signal
+                self.__gtg_connection = None
+                return self.get_activities(query) # reconnect
+
+
+            for task in tasks:
+                if query is None or task['title'].lower().startswith(query):
+                    name = task['title']
+                    if len(task['tags']):
+                        name = "%s, %s" % (name, " ".join([tag.replace("@", "#") for tag in task['tags']]))
+
+                    activities.append({"name": name,
+                                       "category": ""})
+
+            return activities
+
+    def __get_gtg_connection(self):
+        bus = dbus.SessionBus()
+        if self.__gtg_connection and bus.name_has_owner("org.GTG"):
+            return self.__gtg_connection
+
+        if bus.name_has_owner("org.GTG"):
+            self.__gtg_connection = dbus.Interface(bus.get_object('org.GTG', '/org/GTG'),
+                                                   dbus_interface='org.GTG')
+            return self.__gtg_connection
+        else:
+            return None
+
+
+
+def get_eds_tasks():
+    try:
+        sources = ecal.list_task_sources()
+        tasks = []
+        if not sources:
+            # BUG - http://bugzilla.gnome.org/show_bug.cgi?id=546825
+            sources = [('default', 'default')]
+
+        for source in sources:
+            category = source[0]
+
+            data = ecal.open_calendar_source(source[1], ecal.CAL_SOURCE_TYPE_TODO)
+            if data:
+                for task in data.get_all_objects():
+                    if task.get_status() in [ecal.ICAL_STATUS_NONE, ecal.ICAL_STATUS_INPROCESS]:
+                        tasks.append({'name': task.get_summary(), 'category' : category})
+        return tasks
+    except Exception, e:
+        logging.warn(e)
+        return []
diff -pruN 2.91.3+git20120514.b9fec3e1-1/.pc/07-dont-break-with-only-one-year-of-stats.patch/src/hamster/stats.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/07-dont-break-with-only-one-year-of-stats.patch/src/hamster/stats.py
--- 2.91.3+git20120514.b9fec3e1-1/.pc/07-dont-break-with-only-one-year-of-stats.patch/src/hamster/stats.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/07-dont-break-with-only-one-year-of-stats.patch/src/hamster/stats.py	2012-06-25 18:47:07.000000000 +0000
@@ -0,0 +1,463 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+import pygtk
+pygtk.require('2.0')
+
+import os
+import time
+import datetime as dt
+import calendar
+from itertools import groupby
+from gettext import ngettext
+import locale
+import math
+
+import gtk, gobject
+import pango
+
+import widgets
+from lib import stuff, charting, graphics
+from configuration import runtime, conf, load_ui_file
+
+from lib.i18n import C_
+
+class Stats(gtk.Object):
+    __gsignals__ = {
+        "on-close": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+
+    def __init__(self, parent = None):
+        gtk.Object.__init__(self)
+        self._gui = load_ui_file("stats.ui")
+        self.report_chooser = None
+        self.window = self.get_widget("stats_window")
+
+        self.parent = parent# determine if app should shut down on close
+
+        self.timechart = widgets.TimeChart()
+        self.timechart.interactive = False
+
+        self.get_widget("explore_everything").add(self.timechart)
+        self.get_widget("explore_everything").show_all()
+
+        self.window.set_position(gtk.WIN_POS_CENTER)
+        self.chart_category_totals = charting.Chart(value_format = "%.1f",
+                                                       max_bar_width = 20,
+                                                       legend_width = 70,
+                                                       interactive = False)
+        self.get_widget("explore_category_totals").add(self.chart_category_totals)
+
+
+        self.chart_weekday_totals = charting.Chart(value_format = "%.1f",
+                                                      max_bar_width = 20,
+                                                      legend_width = 70,
+                                                      interactive = False)
+        self.get_widget("explore_weekday_totals").add(self.chart_weekday_totals)
+
+        self.chart_weekday_starts_ends = charting.HorizontalDayChart(max_bar_width = 20,
+                                                                     legend_width = 70)
+        self.get_widget("explore_weekday_starts_ends").add(self.chart_weekday_starts_ends)
+
+        self.chart_category_starts_ends = charting.HorizontalDayChart(max_bar_width = 20,
+                                                                      legend_width = 70)
+        self.get_widget("explore_category_starts_ends").add(self.chart_category_starts_ends)
+
+
+        #ah, just want summary look just like all the other text on the page
+        class CairoText(graphics.Scene):
+            def __init__(self):
+                graphics.Scene.__init__(self)
+                self.text = ""
+                self.label = graphics.Label(self.text, 10)
+                self.label.wrap = pango.WRAP_WORD
+                self.add_child(self.label)
+                self.connect("on-enter-frame", self.on_enter_frame)
+
+            def set_text(self, text):
+                self.label.text = text
+                self.redraw()
+
+            def on_enter_frame(self, scene, context):
+                # now for the text - we want reduced contrast for relaxed visuals
+                fg_color = self.get_style().fg[gtk.STATE_NORMAL].to_string()
+                self.label.color = self.colors.contrast(fg_color,  80)
+
+                self.label.width = self.width
+
+
+        self.explore_summary = CairoText()
+        self.get_widget("explore_summary").add(self.explore_summary)
+        self.get_widget("explore_summary").show_all()
+
+        self.external_listeners = [
+            (runtime.storage, runtime.storage.connect('activities-changed',self.after_fact_update)),
+            (runtime.storage, runtime.storage.connect('facts-changed',self.after_fact_update))
+        ]
+
+        self._gui.connect_signals(self)
+        self.show()
+
+    def show(self):
+        self.window.show_all()
+        self.stat_facts = None
+        day_start = conf.get("day_start_minutes")
+        day_start = dt.time(day_start / 60, day_start % 60)
+        self.timechart.day_start = day_start
+        self.init_stats()
+        self.get_widget("year_box").get_children()[0].set_active(True)
+        self.stats()
+
+
+
+    def init_stats(self):
+        self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 2), dt.date.today())
+
+        if not self.stat_facts or self.stat_facts[-1].start_time.year == self.stat_facts[0].start_time.year:
+            self.get_widget("explore_controls").hide()
+        else:
+            by_year = stuff.totals(self.stat_facts,
+                                   lambda fact: fact.start_time.year,
+                                   lambda fact: 1)
+
+            year_box = self.get_widget("year_box")
+            if len(year_box.get_children()) == 0:
+                class YearButton(gtk.ToggleButton):
+                    def __init__(self, label, year, on_clicked):
+                        gtk.ToggleButton.__init__(self, label)
+                        self.year = year
+                        self.connect("clicked", on_clicked)
+
+                all_button = YearButton(C_("years", "All").encode("utf-8"),
+                                        None,
+                                        self.on_year_changed)
+                year_box.pack_start(all_button)
+                self.bubbling = True # TODO figure out how to properly work with togglebuttons as radiobuttons
+                all_button.set_active(True)
+                self.bubbling = False # TODO figure out how to properly work with togglebuttons as radiobuttons
+
+                years = sorted(by_year.keys())
+                for year in years:
+                    year_box.pack_start(YearButton(str(year), year, self.on_year_changed))
+
+                year_box.show_all()
+
+
+    def stats(self, year = None):
+        facts = self.stat_facts
+        if year:
+            facts = filter(lambda fact: fact.start_time.year == year,
+                           facts)
+
+        if not facts or (facts[-1].start_time - facts[0].start_time) < dt.timedelta(days=6):
+            self.get_widget("statistics_box").hide()
+            #self.get_widget("explore_controls").hide()
+            label = self.get_widget("not_enough_records_label")
+
+            if not facts:
+                label.set_text(_("""There is no data to generate statistics yet.
+A week of usage would be nice!"""))
+            else:
+                label.set_text(_(u"Collecting data — check back after a week has passed!"))
+
+            label.show()
+            return
+        else:
+            self.get_widget("statistics_box").show()
+            self.get_widget("explore_controls").show()
+            self.get_widget("not_enough_records_label").hide()
+
+        # All dates in the scope
+        durations = [(fact.start_time, fact.delta) for fact in facts]
+        self.timechart.draw(durations, facts[0].date, facts[-1].date)
+
+
+        # Totals by category
+        categories = stuff.totals(facts,
+                                  lambda fact: fact.category,
+                                  lambda fact: fact.delta.seconds / 60 / 60.0)
+        category_keys = sorted(categories.keys())
+        categories = [categories[key] for key in category_keys]
+        self.chart_category_totals.plot(category_keys, categories)
+
+        # Totals by weekday
+        weekdays = stuff.totals(facts,
+                                lambda fact: (fact.start_time.weekday(),
+                                              fact.start_time.strftime("%a")),
+                                lambda fact: fact.delta.seconds / 60 / 60.0)
+
+        weekday_keys = sorted(weekdays.keys(), key = lambda x: x[0]) #sort
+        weekdays = [weekdays[key] for key in weekday_keys] #get values in the order
+        weekday_keys = [key[1] for key in weekday_keys] #now remove the weekday and keep just the abbreviated one
+        self.chart_weekday_totals.plot(weekday_keys, weekdays)
+
+
+        split_minutes = 5 * 60 + 30 #the mystical hamster midnight
+
+        # starts and ends by weekday
+        by_weekday = {}
+        for date, date_facts in groupby(facts, lambda fact: fact.start_time.date()):
+            date_facts = list(date_facts)
+            weekday = (date_facts[0].start_time.weekday(),
+                       date_facts[0].start_time.strftime("%a"))
+            by_weekday.setdefault(weekday, [])
+
+            start_times, end_times = [], []
+            for fact in date_facts:
+                start_time = fact.start_time.time()
+                start_time = start_time.hour * 60 + start_time.minute
+                if fact.end_time:
+                    end_time = fact.end_time.time()
+                    end_time = end_time.hour * 60 + end_time.minute
+
+                    if start_time < split_minutes:
+                        start_time += 24 * 60
+                    if end_time < start_time:
+                        end_time += 24 * 60
+
+                    start_times.append(start_time)
+                    end_times.append(end_time)
+            if start_times and end_times:
+                by_weekday[weekday].append((min(start_times), max(end_times)))
+
+
+        for day in by_weekday:
+            n = len(by_weekday[day])
+            # calculate mean and variance for starts and ends
+            means = (sum([fact[0] for fact in by_weekday[day]]) / n,
+                     sum([fact[1] for fact in by_weekday[day]]) / n)
+            variances = (sum([(fact[0] - means[0]) ** 2 for fact in by_weekday[day]]) / n,
+                         sum([(fact[1] - means[1]) ** 2 for fact in by_weekday[day]]) / n)
+
+            # In the normal distribution, the range from
+            # (mean - standard deviation) to infinit, or from
+            # -infinit to (mean + standard deviation),  has an accumulated
+            # probability of 84.1%. Meaning we are using the place where if we
+            # picked a random start(or end), 84.1% of the times it will be
+            # inside the range.
+            by_weekday[day] = (int(means[0] - math.sqrt(variances[0])),
+                               int(means[1] + math.sqrt(variances[1])))
+
+        min_weekday = min([by_weekday[day][0] for day in by_weekday])
+        max_weekday = max([by_weekday[day][1] for day in by_weekday])
+
+
+        weekday_keys = sorted(by_weekday.keys(), key = lambda x: x[0])
+        weekdays = [by_weekday[key] for key in weekday_keys]
+        weekday_keys = [key[1] for key in weekday_keys] # get rid of the weekday number as int
+
+
+        # starts and ends by category
+        by_category = {}
+        for date, date_facts in groupby(facts, lambda fact: fact.start_time.date()):
+            date_facts = sorted(list(date_facts), key = lambda x: x.category)
+
+            for category, category_facts in groupby(date_facts, lambda x: x.category):
+                category_facts = list(category_facts)
+                by_category.setdefault(category, [])
+
+                start_times, end_times = [], []
+                for fact in category_facts:
+                    start_time = fact.start_time
+                    start_time = start_time.hour * 60 + start_time.minute
+                    if fact.end_time:
+                        end_time = fact.end_time.time()
+                        end_time = end_time.hour * 60 + end_time.minute
+
+                        if start_time < split_minutes:
+                            start_time += 24 * 60
+                        if end_time < start_time:
+                            end_time += 24 * 60
+
+                        start_times.append(start_time)
+                        end_times.append(end_time)
+
+                if start_times and end_times:
+                    by_category[category].append((min(start_times), max(end_times)))
+
+        for cat in by_category:
+            # For explanation see the comments in the starts and ends by day
+            n = len(by_category[cat])
+            means = (sum([fact[0] for fact in by_category[cat]]) / n,
+                     sum([fact[1] for fact in by_category[cat]]) / n)
+            variances = (sum([(fact[0] - means[0]) ** 2 for fact in by_category[cat]]) / n,
+                         sum([(fact[1] - means[1]) ** 2 for fact in by_category[cat]]) / n)
+
+            by_category[cat] = (int(means[0] - math.sqrt(variances[0])),
+                                int(means[1] + math.sqrt(variances[1])))
+
+        min_category = min([by_category[day][0] for day in by_category])
+        max_category = max([by_category[day][1] for day in by_category])
+
+        category_keys = sorted(by_category.keys(), key = lambda x: x[0])
+        categories = [by_category[key] for key in category_keys]
+
+
+        #get starting and ending hours for graph and turn them into exact hours that divide by 3
+        min_hour = min([min_weekday, min_category]) / 60 * 60
+        max_hour = max([max_weekday, max_category]) / 60 * 60
+
+        self.chart_weekday_starts_ends.plot_day(weekday_keys, weekdays, min_hour, max_hour)
+        self.chart_category_starts_ends.plot_day(category_keys, categories, min_hour, max_hour)
+
+
+        #now the factoids!
+        summary = ""
+
+        # first record
+        if not year:
+            # date format for the first record if the year has not been selected
+            # Using python datetime formatting syntax. See:
+            # http://docs.python.org/library/time.html#time.strftime
+            first_date = facts[0].start_time.strftime(C_("first record", "%b %d, %Y"))
+        else:
+            # date of first record when year has been selected
+            # Using python datetime formatting syntax. See:
+            # http://docs.python.org/library/time.html#time.strftime
+            first_date = facts[0].start_time.strftime(C_("first record", "%b %d"))
+
+        summary += _("First activity was recorded on %s.") % \
+                                                     ("<b>%s</b>" % first_date)
+
+        # total time tracked
+        total_delta = dt.timedelta(days=0)
+        for fact in facts:
+            total_delta += fact.delta
+
+        if total_delta.days > 1:
+            human_years_str = ngettext("%(num)s year",
+                                       "%(num)s years",
+                                       total_delta.days / 365) % {
+                              'num': "<b>%s</b>" % locale.format("%.2f", (total_delta.days / 365.0))}
+            working_years_str = ngettext("%(num)s year",
+                                         "%(num)s years",
+                                         total_delta.days * 3 / 365) % {
+                         'num': "<b>%s</b>" % locale.format("%.2f",  (total_delta.days * 3 / 365.0)) }
+            #FIXME: difficult string to properly pluralize
+            summary += " " + _("""Time tracked so far is %(human_days)s human days \
+(%(human_years)s) or %(working_days)s working days (%(working_years)s).""") % {
+              "human_days": ("<b>%d</b>" % total_delta.days),
+              "human_years": human_years_str,
+              "working_days": ("<b>%d</b>" % (total_delta.days * 3)), # 8 should be pretty much an average working day
+              "working_years": working_years_str }
+
+
+        # longest fact
+        max_fact = None
+        for fact in facts:
+            if not max_fact or fact.delta > max_fact.delta:
+                max_fact = fact
+
+        longest_date = max_fact.start_time.strftime(
+            # How the date of the longest activity should be displayed in statistics
+            # Using python datetime formatting syntax. See:
+            # http://docs.python.org/library/time.html#time.strftime
+            C_("date of the longest activity", "%b %d, %Y"))
+
+        num_hours = max_fact.delta.seconds / 60 / 60.0 + max_fact.delta.days * 24
+        hours = "<b>%s</b>" % locale.format("%.1f", num_hours)
+
+        summary += "\n" + ngettext("Longest continuous work happened on \
+%(date)s and was %(hours)s hour.",
+                                  "Longest continuous work happened on \
+%(date)s and was %(hours)s hours.",
+                                  int(num_hours)) % {"date": longest_date,
+                                                     "hours": hours}
+
+        # total records (in selected scope)
+        summary += " " + ngettext("There is %s record.",
+                                  "There are %s records.",
+                                  len(facts)) % ("<b>%d</b>" % len(facts))
+
+
+        early_start, early_end = dt.time(5,0), dt.time(9,0)
+        late_start, late_end = dt.time(20,0), dt.time(5,0)
+
+
+        fact_count = len(facts)
+        def percent(condition):
+            matches = [fact for fact in facts if condition(fact)]
+            return round(len(matches) / float(fact_count) * 100)
+
+
+        early_percent = percent(lambda fact: early_start < fact.start_time.time() < early_end)
+        late_percent = percent(lambda fact: fact.start_time.time() > late_start or fact.start_time.time() < late_end)
+        short_percent = percent(lambda fact: fact.delta <= dt.timedelta(seconds = 60 * 15))
+
+        if fact_count < 100:
+            summary += "\n\n" + _("Hamster would like to observe you some more!")
+        elif early_percent >= 20:
+            summary += "\n\n" + _("With %s percent of all activities starting before \
+9am, you seem to be an early bird.") % ("<b>%d</b>" % early_percent)
+        elif late_percent >= 20:
+            summary += "\n\n" + _("With %s percent of all activities starting after \
+11pm, you seem to be a night owl.") % ("<b>%d</b>" % late_percent)
+        elif short_percent >= 20:
+            summary += "\n\n" + _("With %s percent of all activities being shorter \
+than 15 minutes, you seem to be a busy bee.") % ("<b>%d</b>" % short_percent)
+
+        self.explore_summary.set_text(summary)
+
+
+
+    def on_year_changed(self, button):
+        if self.bubbling: return
+
+        for child in button.parent.get_children():
+            if child != button and child.get_active():
+                self.bubbling = True
+                child.set_active(False)
+                self.bubbling = False
+
+        self.stats(button.year)
+
+
+    def after_fact_update(self, event):
+        self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
+        self.stats()
+
+    def get_widget(self, name):
+        """ skip one variable (huh) """
+        return self._gui.get_object(name)
+
+    def on_window_key_pressed(self, tree, event_key):
+      if (event_key.keyval == gtk.keysyms.Escape
+          or (event_key.keyval == gtk.keysyms.w
+              and event_key.state & gtk.gdk.CONTROL_MASK)):
+        self.close_window()
+
+    def on_stats_window_deleted(self, widget, event):
+        self.close_window()
+        return True
+
+    def close_window(self):
+        if not self.parent:
+            gtk.main_quit()
+        else:
+            for obj, handler in self.external_listeners:
+                obj.disconnect(handler)
+            self.window.destroy()
+            self.window = None
+            self._gui = None
+            self.emit("on-close")
+
+
+if __name__ == "__main__":
+    stats_viewer = Stats()
+    gtk.main()
diff -pruN 2.91.3+git20120514.b9fec3e1-1/.pc/applied-patches 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/applied-patches
--- 2.91.3+git20120514.b9fec3e1-1/.pc/applied-patches	1970-01-01 00:00:00.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/.pc/applied-patches	2015-04-07 08:36:42.201438010 +0000
@@ -0,0 +1,6 @@
+01-dont-fail-on-applet-import.patch
+02-dont-traceback-on-quit.patch
+04-lp926481.patch
+05-lp979707.patch
+06-gtg-dbus-update.patch
+07-dont-break-with-only-one-year-of-stats.patch
diff -pruN 2.91.3+git20120514.b9fec3e1-1/src/hamster/applet.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/applet.py
--- 2.91.3+git20120514.b9fec3e1-1/src/hamster/applet.py	2012-06-25 18:47:07.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/applet.py	2015-04-07 08:36:42.000000000 +0000
@@ -26,7 +26,12 @@ import pygtk
 pygtk.require("2.0")
 import gtk, pango
 
-import gnomeapplet
+try:
+    import gnomeapplet
+except ImportError:
+    logging.warning("Could not import gnomeapplet. Defaulting to upper panel")
+    gnomeapplet = None
+
 import gobject
 import dbus, dbus.service, dbus.mainloop.glib
 import locale
@@ -110,9 +115,10 @@ class PanelButton(gtk.ToggleButton):
 
         popup_dir = self.get_parent().get_orient()
 
-        orient_vertical = popup_dir in [gnomeapplet.ORIENT_LEFT] or \
-                          popup_dir in [gnomeapplet.ORIENT_RIGHT]
-
+        orient_vertical = False
+        if gnomeapplet != None:
+            orient_vertical = popup_dir in [gnomeapplet.ORIENT_LEFT] or \
+                              popup_dir in [gnomeapplet.ORIENT_RIGHT]
 
         context = self.label.get_pango_context()
         metrics = context.get_metrics(self.label.style.font_desc,
@@ -148,9 +154,9 @@ class PanelButton(gtk.ToggleButton):
 
         orient_vertical = True
         new_size = allocation.width
-        if self.popup_dir in [gnomeapplet.ORIENT_LEFT]:
+        if gnomeapplet != None and self.popup_dir in [gnomeapplet.ORIENT_LEFT]:
             new_angle = 270
-        elif self.popup_dir in [gnomeapplet.ORIENT_RIGHT]:
+        elif gnomeapplet != None and self.popup_dir in [gnomeapplet.ORIENT_RIGHT]:
             new_angle = 90
         else:
             new_angle = 0
@@ -453,7 +459,10 @@ class HamsterApplet(object):
 
         self.popup_dir = self.applet.get_orient()
 
-        if self.popup_dir in (gnomeapplet.ORIENT_DOWN, gnomeapplet.ORIENT_UP):
+        if gnomeapplet == None:
+            # Default to upper panel (ORIENT_DOWN)
+            y = y + label.height
+        elif self.popup_dir in (gnomeapplet.ORIENT_DOWN, gnomeapplet.ORIENT_UP):
             if self.popup_dir == gnomeapplet.ORIENT_DOWN:
                 y = y + label.height
             else:
diff -pruN 2.91.3+git20120514.b9fec3e1-1/src/hamster/configuration.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/configuration.py
--- 2.91.3+git20120514.b9fec3e1-1/src/hamster/configuration.py	2012-06-25 18:47:07.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/configuration.py	2015-04-07 08:36:42.000000000 +0000
@@ -287,16 +287,20 @@ class GConfStore(gobject.GObject, Single
         #for gconf refer to the full key path
         key = self._fix_key(key)
 
-        if vtype is bool:
-            self._client.set_bool(key, value)
-        elif vtype is str:
-            self._client.set_string(key, value)
-        elif vtype is int:
-            self._client.set_int(key, value)
-        elif vtype in (list, tuple):
-            #Save every value as a string
-            strvalues = [str(i) for i in value]
-            self._client.set_list(key, gconf.VALUE_STRING, strvalues)
+        try:
+            if vtype is bool:
+                self._client.set_bool(key, value)
+            elif vtype is str:
+                self._client.set_string(key, value)
+            elif vtype is int:
+                self._client.set_int(key, value)
+            elif vtype in (list, tuple):
+                #Save every value as a string
+                strvalues = [str(i) for i in value]
+                self._client.set_list(key, gconf.VALUE_STRING, strvalues)
+        except:
+            log.warn("Could not save '%s' to gconf database" % str(key))
+            return False
 
         return True
 
diff -pruN 2.91.3+git20120514.b9fec3e1-1/src/hamster/external.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/external.py
--- 2.91.3+git20120514.b9fec3e1-1/src/hamster/external.py	2012-06-25 18:47:07.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/external.py	2015-04-07 08:36:42.000000000 +0000
@@ -58,7 +58,7 @@ class ActivitiesSource(gobject.GObject):
 
             tasks = []
             try:
-                tasks = conn.get_tasks()
+                tasks = conn.GetTasks()
             except dbus.exceptions.DBusException:  #TODO too lame to figure out how to connect to the disconnect signal
                 self.__gtg_connection = None
                 return self.get_activities(query) # reconnect
@@ -77,12 +77,12 @@ class ActivitiesSource(gobject.GObject):
 
     def __get_gtg_connection(self):
         bus = dbus.SessionBus()
-        if self.__gtg_connection and bus.name_has_owner("org.GTG"):
+        if self.__gtg_connection and bus.name_has_owner("org.gnome.GTG"):
             return self.__gtg_connection
 
-        if bus.name_has_owner("org.GTG"):
-            self.__gtg_connection = dbus.Interface(bus.get_object('org.GTG', '/org/GTG'),
-                                                   dbus_interface='org.GTG')
+        if bus.name_has_owner("org.gnome.GTG"):
+            self.__gtg_connection = dbus.Interface(bus.get_object('org.gnome.GTG', '/org/gnome/GTG'),
+                                                   dbus_interface='org.gnome.GTG')
             return self.__gtg_connection
         else:
             return None
diff -pruN 2.91.3+git20120514.b9fec3e1-1/src/hamster/overview.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/overview.py
--- 2.91.3+git20120514.b9fec3e1-1/src/hamster/overview.py	2012-06-25 18:47:07.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/overview.py	2015-04-07 08:36:42.000000000 +0000
@@ -381,7 +381,7 @@ class Overview(gtk.Object):
         elif isinstance(fact, dt.date):
             selected_date = fact
         else:
-            selected_date = fact["date"]
+            selected_date = fact.date
 
         dialogs.edit.show(self, fact_date = selected_date)
 
diff -pruN 2.91.3+git20120514.b9fec3e1-1/src/hamster/stats.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/stats.py
--- 2.91.3+git20120514.b9fec3e1-1/src/hamster/stats.py	2012-06-25 18:47:07.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/stats.py	2015-04-07 08:36:42.000000000 +0000
@@ -129,7 +129,7 @@ class Stats(gtk.Object):
     def init_stats(self):
         self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 2), dt.date.today())
 
-        if not self.stat_facts or self.stat_facts[-1].start_time.year == self.stat_facts[0].start_time.year:
+        if not self.stat_facts:
             self.get_widget("explore_controls").hide()
         else:
             by_year = stuff.totals(self.stat_facts,
@@ -156,6 +156,9 @@ class Stats(gtk.Object):
                 for year in years:
                     year_box.pack_start(YearButton(str(year), year, self.on_year_changed))
 
+            if self.stat_facts[-1].start_time.year == self.stat_facts[0].start_time.year:
+                self.get_widget("explore_controls").hide_all()
+            else:
                 year_box.show_all()
 
 
diff -pruN 2.91.3+git20120514.b9fec3e1-1/src/hamster/widgets/dayline.py 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/widgets/dayline.py
--- 2.91.3+git20120514.b9fec3e1-1/src/hamster/widgets/dayline.py	2012-06-25 18:47:07.000000000 +0000
+++ 2.91.3+git20120514.b9fec3e1-1ubuntu2/src/hamster/widgets/dayline.py	2015-04-07 08:36:42.000000000 +0000
@@ -182,6 +182,13 @@ class DayLine(graphics.Scene):
     def on_click(self, scene, event, target):
         self.drag_start = None
 
+        # If self.selection.start_time is somehow None, just reset the selection.
+        # This can sometimes happen when dragging left to right in small
+        # increments. https://bugzilla.gnome.org/show_bug.cgi?id=669478
+        if self.selection == None or self.selection.start_time == None:
+            self.new_selection()
+            return
+
         start_time = self.selection.start_time
         if start_time > dt.datetime.now():
             start_time = dt.datetime.now()
