[hamster-applet] harmless but noisy shift - had to refactor class names, otherwise getting lost all the time



commit 34a833ebf3ad9a20ec86441e0e499d45c2c0ad9d
Author: Toms Bauģis <toms baugis gmail com>
Date:   Fri Jan 15 01:35:21 2010 +0000

    harmless but noisy shift - had to refactor class names, otherwise getting lost all the time

 hamster/configuration.py                           |    8 +-
 hamster/overview.py                                |  372 ++++++++++++
 .../{stats_overview.py => overview_activities.py}  |    0
 hamster/{stats_reports.py => overview_totals.py}   |    2 +-
 hamster/stats.py                                   |  601 +++++++++++---------
 hamster/stats_stats.py                             |  425 --------------
 6 files changed, 704 insertions(+), 704 deletions(-)
---
diff --git a/hamster/configuration.py b/hamster/configuration.py
index 9c1a974..5658284 100644
--- a/hamster/configuration.py
+++ b/hamster/configuration.py
@@ -157,13 +157,13 @@ class Dialogs(Singleton):
         self.edit = OneWindow(get_edit_class)
 
         def get_overview_class():
-            from stats import StatsViewer
-            return StatsViewer
+            from overview import Overview
+            return Overview
         self.overview = OneWindow(get_overview_class)
 
         def get_stats_class():
-            from stats_stats import StatsViewer2
-            return StatsViewer2
+            from stats import Stats
+            return Stats
         self.stats = OneWindow(get_stats_class)
 
         def get_about_class():
diff --git a/hamster/overview.py b/hamster/overview.py
new file mode 100644
index 0000000..246fc7c
--- /dev/null
+++ b/hamster/overview.py
@@ -0,0 +1,372 @@
+# - 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 stuff
+from hamster.i18n import C_
+from configuration import runtime, conf, dialogs
+import widgets, reports
+
+from overview_activities import OverviewBox
+from overview_totals import TotalsBox
+
+
+class Overview(object):
+    def __init__(self, parent = None):
+        self.parent = parent# determine if app should shut down on close
+        self._gui = stuff.load_ui_file("stats.ui")
+        self.report_chooser = None
+
+        self.facts = None
+
+        self.window = self.get_widget("tabs_window")
+
+        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.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.reports = TotalsBox()
+        self.get_widget("reports_tab").add(self.reports)
+
+        self.range_combo = gtk.combo_box_new_text()
+        self.range_combo.append_text(_("Week"))
+        self.range_combo.append_text(_("Month"))
+        self.range_combo.append_text(_("Date Range"))
+        self.range_combo.set_row_separator_func(lambda row, iter: row[iter][0] == "-")
+        self.range_combo.append_text("-")
+        self.range_combo.append_text("All")
+        self.range_combo.set_active(0)
+        self.range_combo.connect("changed", self.on_range_combo_changed)
+
+
+
+        self.get_widget("range_pick").add(self.range_combo)
+
+
+        self.start_date_input = widgets.DateInput(self.start_date)
+        self.start_date_input.connect("date-entered", self.on_start_date_entered)
+        self.get_widget("range_start_box").add(self.start_date_input)
+
+        self.end_date_input = widgets.DateInput(self.end_date)
+        self.end_date_input.connect("date-entered", self.on_end_date_entered)
+        self.get_widget("range_end_box").add(self.end_date_input)
+
+        self.timechart = widgets.TimeChart()
+        self.get_widget("by_day_box").add(self.timechart)
+
+        self._gui.connect_signals(self)
+        runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
+        runtime.dispatcher.add_handler('day_updated', self.after_activity_update)
+        runtime.dispatcher.add_handler('conf_changed', self.on_conf_change)
+
+        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.move(x, y)
+                self.window.resize(w, h)
+            else:
+                self.window.set_position(gtk.WIN_POS_CENTER)
+
+        self.window.show_all()
+        self.search()
+
+
+    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
+
+        self.start_date_input.set_date(self.start_date)
+        self.end_date_input.set_date(self.end_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("report_button").set_sensitive(len(self.facts) > 0)
+
+        self.set_title()
+
+        self.timechart.draw(self.facts, 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):
+        start_date, end_date = self.start_date, self.end_date
+        dates_dict = stuff.dateDict(start_date, "start_")
+        dates_dict.update(stuff.dateDict(end_date, "end_"))
+
+        if start_date == end_date:
+            # date format for overview label when only single day is visible
+            # Using python datetime formatting syntax. See:
+            # http://docs.python.org/library/time.html#time.strftime
+            start_date_str = start_date.strftime(_("%B %d, %Y"))
+            # Overview label if looking on single day
+            self.title = start_date_str
+        elif start_date.year != end_date.year:
+            # overview label if start and end years don't match
+            # letter after prefixes (start_, end_) is the one of
+            # standard python date formatting ones- you can use all of them
+            # see http://docs.python.org/library/time.html#time.strftime
+            self.title = _(u"%(start_B)s %(start_d)s, %(start_Y)s â?? %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
+        elif start_date.month != end_date.month:
+            # overview label if start and end month do not match
+            # letter after prefixes (start_, end_) is the one of
+            # standard python date formatting ones- you can use all of them
+            # see http://docs.python.org/library/time.html#time.strftime
+            self.title = _(u"%(start_B)s %(start_d)s â?? %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
+        else:
+            # overview label for interval in same month
+            # letter after prefixes (start_, end_) is the one of
+            # standard python date formatting ones- you can use all of them
+            # see http://docs.python.org/library/time.html#time.strftime
+            self.title = _(u"%(start_B)s %(start_d)s â?? %(end_d)s, %(end_Y)s") % dates_dict
+
+        self.get_widget("range_title").set_text(self.title)
+
+
+    def on_conf_change(self, event, data):
+        key, value = data
+        if key == "day_start_minutes":
+            self.day_start = dt.time(value / 60, value % 60)
+            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, dict)
+
+        self.get_widget('remove').set_sensitive(real_fact)
+        self.get_widget('edit').set_sensitive(real_fact)
+
+        return True
+
+    def after_activity_update(self, widget, renames):
+        self.search()
+
+    def on_search_activate(self, widget):
+        self.search()
+
+    def on_report_button_clicked(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:
+                gtk.show_uri(gtk.gdk.Screen(),
+                             "file://%s" % os.path.split(path)[0], 0L)
+
+        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 on_range_combo_changed(self, combo):
+        idx = combo.get_active()
+
+        self.get_widget("preset_range").hide()
+        self.get_widget("range_box").hide()
+
+        if idx == 2: # date range
+            self.get_widget("range_box").show()
+        else:
+            if idx == 0: # 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)
+                self.get_widget("preset_range").show()
+
+            elif idx == 1: #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)
+                self.get_widget("preset_range").show()
+
+
+            self.search()
+
+    def on_start_date_entered(self, input):
+        self.start_date = input.get_date().date()
+        self.view_date = self.start_date
+        self.search()
+
+    def on_end_date_entered(self, input):
+        self.end_date = input.get_date().date()
+        self.search()
+
+    def _chosen_range(self):
+        return self.range_combo.get_active()
+
+    def on_prev_clicked(self, button):
+        if self._chosen_range() == 0:  # week
+            self.start_date -= dt.timedelta(7)
+            self.end_date -= dt.timedelta(7)
+        elif self._chosen_range() == 1: # 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)
+
+        self.view_date = self.start_date
+        self.search()
+
+    def on_next_clicked(self, button):
+        if self._chosen_range() == 0:  # week
+            self.start_date += dt.timedelta(7)
+            self.end_date += dt.timedelta(7)
+        elif self._chosen_range() == 1: # 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)
+
+        self.view_date = self.start_date
+        self.search()
+
+
+    def on_home_clicked(self, button):
+        self.view_date = (dt.datetime.today() - dt.timedelta(hours = self.day_start.hour,
+                                                        minutes = self.day_start.minute)).date()
+        if self._chosen_range() == 0: # 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._chosen_range() == 1: # 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)
+
+        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)
+
+
+    def on_add_clicked(self, button):
+        fact = self.fact_tree.get_selected_fact()
+        if not fact:
+            return
+        
+        if isinstance(fact, dt.date):
+            selected_date = fact
+        else:
+            selected_date = fact["date"]
+        
+        dialogs.edit.show(fact_date = selected_date)
+
+    def on_remove_clicked(self, button):
+        self.overview.delete_selected()
+
+    def on_edit_clicked(self, button):
+        fact = self.fact_tree.get_selected_fact()
+        if not fact or isinstance(fact, date):
+            return
+        dialogs.edit.show(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 close_window(self):
+        runtime.dispatcher.del_handler('activity_updated', self.after_activity_update)
+        runtime.dispatcher.del_handler('day_updated', self.after_activity_update)
+        runtime.dispatcher.del_handler('conf_changed', self.on_conf_change)
+
+        # 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:
+            self.window.destroy()
+            return False
+
+    def show(self):
+        self.window.show()
+
diff --git a/hamster/stats_overview.py b/hamster/overview_activities.py
similarity index 100%
rename from hamster/stats_overview.py
rename to hamster/overview_activities.py
diff --git a/hamster/stats_reports.py b/hamster/overview_totals.py
similarity index 99%
rename from hamster/stats_reports.py
rename to hamster/overview_totals.py
index 1d6820e..e7316a9 100644
--- a/hamster/stats_reports.py
+++ b/hamster/overview_totals.py
@@ -38,7 +38,7 @@ from configuration import runtime, dialogs
 from hamster.i18n import C_
 
 
-class ReportsBox(gtk.VBox):
+class TotalsBox(gtk.VBox):
     def __init__(self):
         gtk.VBox.__init__(self)
         self._gui = stuff.load_ui_file("stats_reports.ui")
diff --git a/hamster/stats.py b/hamster/stats.py
index 8c29d37..524e97b 100644
--- a/hamster/stats.py
+++ b/hamster/stats.py
@@ -17,356 +17,409 @@
 # 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
-import webbrowser
+from itertools import groupby
+from gettext import ngettext
 
 import gtk, gobject
 import pango
 
-import stuff
-from hamster.i18n import C_
-from configuration import runtime, conf, dialogs
-import widgets, reports
-
-from stats_overview import OverviewBox
-from stats_reports import ReportsBox
+import stuff, charting, graphics, widgets
+from configuration import runtime
 
+from hamster.i18n import C_
 
-class StatsViewer(object):
+class Stats(object):
     def __init__(self, parent = None):
-        self.parent = parent# determine if app should shut down on close
-        self._gui = stuff.load_ui_file("stats.ui")
+        self._gui = stuff.load_ui_file("stats_stats.ui")
         self.report_chooser = None
+        self.window = self.get_widget("stats_window")
 
-        self.facts = None
+        self.parent = parent# determine if app should shut down on close
 
-        self.window = self.get_widget("tabs_window")
+        self.stat_facts = None
 
-        self.day_start = conf.get("day_start_minutes")
-        self.day_start = dt.time(self.day_start / 60, self.day_start % 60)
+        self.timechart = widgets.TimeChart()
+        self.get_widget("explore_everything").add(self.timechart)
+        self.get_widget("explore_everything").show_all()
 
-        self.view_date = (dt.datetime.today() - dt.timedelta(hours = self.day_start.hour,
-                                                        minutes = self.day_start.minute)).date()
+        runtime.dispatcher.add_handler('activity_updated', self.after_fact_update)
+        runtime.dispatcher.add_handler('day_updated', self.after_fact_update)
 
-        #set to monday
-        self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1)
+        self.init_stats()
 
-        # look if we need to start on sunday or monday
-        self.start_date = self.start_date + dt.timedelta(stuff.locale_first_weekday())
+        self.window.set_position(gtk.WIN_POS_CENTER)
+        self.window.show_all()
+        self.stats()
 
-        # 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)
 
+    def init_stats(self):
+        self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
 
-        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)
+        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")
+            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()
+
+        self.chart_category_totals = charting.HorizontalBarChart(value_format = "%.1f",
+                                                            bars_beveled = False,
+                                                            max_bar_width = 20,
+                                                            legend_width = 70)
+        self.get_widget("explore_category_totals").add(self.chart_category_totals)
+
+
+        self.chart_weekday_totals = charting.HorizontalBarChart(value_format = "%.1f",
+                                                            bars_beveled = False,
+                                                            max_bar_width = 20,
+                                                            legend_width = 70)
+        self.get_widget("explore_weekday_totals").add(self.chart_weekday_totals)
+
+        self.chart_weekday_starts_ends = charting.HorizontalDayChart(bars_beveled = False,
+                                                                animate = False,
+                                                                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(bars_beveled = False,
+                                                                animate = False,
+                                                                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.Area):
+            def __init__(self, fontsize = 10):
+                graphics.Area.__init__(self)
+                self.text = ""
+                self.fontsize = fontsize
+
+            def set_text(self, text):
+                self.text = text
+                self.redraw_canvas()
+
+            def on_expose(self):
+                # now for the text - we want reduced contrast for relaxed visuals
+                fg_color = self.get_style().fg[gtk.STATE_NORMAL].to_string()
+                if self.colors.is_light(fg_color):
+                    label_color = self.colors.darker(fg_color,  80)
+                else:
+                    label_color = self.colors.darker(fg_color,  -80)
+
+                default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+                default_font.set_size(self.fontsize * pango.SCALE)
+                self.layout.set_font_description(default_font)
+
+                #self.context.set_source_rgb(0,0,0)
+                self.layout.set_markup(self.text)
+
+                self.layout.set_width((self.width) * pango.SCALE)
+                self.context.move_to(0,0)
+                self.set_color(label_color)
+
+                self.context.show_layout(self.layout)
+
+        self.explore_summary = CairoText()
+        self.get_widget("explore_summary").add(self.explore_summary)
+        self.get_widget("explore_summary").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(_("Collecting data â?? check back after a week has passed!"))
 
-        self.reports = ReportsBox()
-        self.get_widget("reports_tab").add(self.reports)
+            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
+        self.timechart.draw(facts, facts[0]["date"], facts[-1]["date"])
 
-        self.range_combo = gtk.combo_box_new_text()
-        self.range_combo.append_text(_("Week"))
-        self.range_combo.append_text(_("Month"))
-        self.range_combo.append_text(_("Date Range"))
-        self.range_combo.set_row_separator_func(lambda row, iter: row[iter][0] == "-")
-        self.range_combo.append_text("-")
-        self.range_combo.append_text("All")
-        self.range_combo.set_active(0)
-        self.range_combo.connect("changed", self.on_range_combo_changed)
 
+        # 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)
 
-        self.get_widget("range_pick").add(self.range_combo)
+        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)
 
 
-        self.start_date_input = widgets.DateInput(self.start_date)
-        self.start_date_input.connect("date-entered", self.on_start_date_entered)
-        self.get_widget("range_start_box").add(self.start_date_input)
+        split_minutes = 5 * 60 + 30 #the mystical hamster midnight
 
-        self.end_date_input = widgets.DateInput(self.end_date)
-        self.end_date_input.connect("date-entered", self.on_end_date_entered)
-        self.get_widget("range_end_box").add(self.end_date_input)
+        # 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, [])
 
-        self.timechart = widgets.TimeChart()
-        self.get_widget("by_day_box").add(self.timechart)
+            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
 
-        self._gui.connect_signals(self)
-        runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
-        runtime.dispatcher.add_handler('day_updated', self.after_activity_update)
-        runtime.dispatcher.add_handler('conf_changed', self.on_conf_change)
+                    if start_time < split_minutes:
+                        start_time += 24 * 60
+                    if end_time < start_time:
+                        end_time += 24 * 60
 
-        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.move(x, y)
-                self.window.resize(w, h)
-            else:
-                self.window.set_position(gtk.WIN_POS_CENTER)
+                    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)))
 
-        self.window.show_all()
-        self.search()
 
+        for day in by_weekday:
+            by_weekday[day] = (sum([fact[0] for fact in by_weekday[day]]) / len(by_weekday[day]),
+                               sum([fact[1] for fact in by_weekday[day]]) / len(by_weekday[day]))
 
-    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
+        min_weekday = min([by_weekday[day][0] for day in by_weekday])
+        max_weekday = max([by_weekday[day][1] for day in by_weekday])
 
-        self.start_date_input.set_date(self.start_date)
-        self.end_date_input.set_date(self.end_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)
+        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
 
-        self.get_widget("report_button").set_sensitive(len(self.facts) > 0)
 
-        self.set_title()
+        # 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"])
 
-        self.timechart.draw(self.facts, self.start_date, self.end_date)
+            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 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)
+                        if start_time < split_minutes:
+                            start_time += 24 * 60
+                        if end_time < start_time:
+                            end_time += 24 * 60
 
-    def set_title(self):
-        start_date, end_date = self.start_date, self.end_date
-        dates_dict = stuff.dateDict(start_date, "start_")
-        dates_dict.update(stuff.dateDict(end_date, "end_"))
+                        start_times.append(start_time)
+                        end_times.append(end_time)
 
-        if start_date == end_date:
-            # date format for overview label when only single day is visible
-            # Using python datetime formatting syntax. See:
-            # http://docs.python.org/library/time.html#time.strftime
-            start_date_str = start_date.strftime(_("%B %d, %Y"))
-            # Overview label if looking on single day
-            self.title = start_date_str
-        elif start_date.year != end_date.year:
-            # overview label if start and end years don't match
-            # letter after prefixes (start_, end_) is the one of
-            # standard python date formatting ones- you can use all of them
-            # see http://docs.python.org/library/time.html#time.strftime
-            self.title = _(u"%(start_B)s %(start_d)s, %(start_Y)s â?? %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
-        elif start_date.month != end_date.month:
-            # overview label if start and end month do not match
-            # letter after prefixes (start_, end_) is the one of
-            # standard python date formatting ones- you can use all of them
-            # see http://docs.python.org/library/time.html#time.strftime
-            self.title = _(u"%(start_B)s %(start_d)s â?? %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
-        else:
-            # overview label for interval in same month
-            # letter after prefixes (start_, end_) is the one of
-            # standard python date formatting ones- you can use all of them
-            # see http://docs.python.org/library/time.html#time.strftime
-            self.title = _(u"%(start_B)s %(start_d)s â?? %(end_d)s, %(end_Y)s") % dates_dict
+                if start_times and end_times:
+                    by_category[category].append((min(start_times), max(end_times)))
 
-        self.get_widget("range_title").set_text(self.title)
+        for cat in by_category:
+            by_category[cat] = (sum([fact[0] for fact in by_category[cat]]) / len(by_category[cat]),
+                                sum([fact[1] for fact in by_category[cat]]) / len(by_category[cat]))
 
+        min_category = min([by_category[day][0] for day in by_category])
+        max_category = max([by_category[day][1] for day in by_category])
 
-    def on_conf_change(self, event, data):
-        key, value = data
-        if key == "day_start_minutes":
-            self.day_start = dt.time(value / 60, value % 60)
-            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, dict)
+        category_keys = sorted(by_category.keys(), key = lambda x: x[0])
+        categories = [by_category[key] for key in category_keys]
 
-        self.get_widget('remove').set_sensitive(real_fact)
-        self.get_widget('edit').set_sensitive(real_fact)
 
-        return True
+        #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
 
-    def after_activity_update(self, widget, renames):
-        self.search()
+        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)
 
-    def on_search_activate(self, widget):
-        self.search()
 
-    def on_report_button_clicked(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)
+        #now the factoids!
+        summary = ""
 
-            if format == ("html"):
-                webbrowser.open_new("file://%s" % path)
-            else:
-                gtk.show_uri(gtk.gdk.Screen(),
-                             "file://%s" % os.path.split(path)[0], 0L)
-
-        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)
+        # 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:
-            self.report_chooser.present()
+            # 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>%.2f</b>" % (total_delta.days / 365.0)}
+            working_years_str = ngettext("%(num)s year",
+                                         "%(num)s years",
+                                         total_delta.days * 3 / 365) % {
+                         'num': "<b>%.2f</b>" % (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>%.1f</b>" % (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}
 
-    def on_range_combo_changed(self, combo):
-        idx = combo.get_active()
+        # total records (in selected scope)
+        summary += " " + ngettext("There is %s record.",
+                                  "There are %s records.",
+                                  len(facts)) % ("<b>%d</b>" % len(facts))
 
-        self.get_widget("preset_range").hide()
-        self.get_widget("range_box").hide()
 
-        if idx == 2: # date range
-            self.get_widget("range_box").show()
-        else:
-            if idx == 0: # 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)
-                self.get_widget("preset_range").show()
-
-            elif idx == 1: #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)
-                self.get_widget("preset_range").show()
-
-
-            self.search()
-
-    def on_start_date_entered(self, input):
-        self.start_date = input.get_date().date()
-        self.view_date = self.start_date
-        self.search()
-
-    def on_end_date_entered(self, input):
-        self.end_date = input.get_date().date()
-        self.search()
-
-    def _chosen_range(self):
-        return self.range_combo.get_active()
-
-    def on_prev_clicked(self, button):
-        if self._chosen_range() == 0:  # week
-            self.start_date -= dt.timedelta(7)
-            self.end_date -= dt.timedelta(7)
-        elif self._chosen_range() == 1: # 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)
-
-        self.view_date = self.start_date
-        self.search()
-
-    def on_next_clicked(self, button):
-        if self._chosen_range() == 0:  # week
-            self.start_date += dt.timedelta(7)
-            self.end_date += dt.timedelta(7)
-        elif self._chosen_range() == 1: # 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)
-
-        self.view_date = self.start_date
-        self.search()
-
-
-    def on_home_clicked(self, button):
-        self.view_date = (dt.datetime.today() - dt.timedelta(hours = self.day_start.hour,
-                                                        minutes = self.day_start.minute)).date()
-        if self._chosen_range() == 0: # 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._chosen_range() == 1: # 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)
-
-        self.search()
+        early_start, early_end = dt.time(5,0), dt.time(9,0)
+        late_start, late_end = dt.time(20,0), dt.time(5,0)
 
-    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)
+        fact_count = len(facts)
+        def percent(condition):
+            matches = [fact for fact in facts if condition(fact)]
+            return round(len(matches) / float(fact_count) * 100)
 
 
-    def on_add_clicked(self, button):
-        fact = self.fact_tree.get_selected_fact()
-        if not fact:
-            return
-        
-        if isinstance(fact, dt.date):
-            selected_date = fact
-        else:
-            selected_date = fact["date"]
-        
-        dialogs.edit.show(fact_date = selected_date)
+        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))
 
-    def on_remove_clicked(self, button):
-        self.overview.delete_selected()
+        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 facts 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 facts 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 tasks being shorter \
+than 15 minutes you seem to be a busy bee." % ("<b>%d</b>" % short_percent))
 
-    def on_edit_clicked(self, button):
-        fact = self.fact_tree.get_selected_fact()
-        if not fact or isinstance(fact, date):
-            return
-        dialogs.edit.show(fact_id = fact["id"])
+        self.explore_summary.set_text(summary)
 
-    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 close_window(self):
-        runtime.dispatcher.del_handler('activity_updated', self.after_activity_update)
-        runtime.dispatcher.del_handler('day_updated', self.after_activity_update)
-        runtime.dispatcher.del_handler('conf_changed', self.on_conf_change)
-
-        # 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])
-            
+    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, date):
+        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 close_window(self):
         if not self.parent:
             gtk.main_quit()
         else:
             self.window.destroy()
             return False
 
-    def show(self):
-        self.window.show()
 
+if __name__ == "__main__":
+    stats_viewer = StatsViewer2()
+    gtk.main()



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]