[hamster-applet] as the code has grown, split overview into three files matching the tabs to be able to manage it som
- From: Toms Baugis <tbaugis src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [hamster-applet] as the code has grown, split overview into three files matching the tabs to be able to manage it som
- Date: Sat, 5 Dec 2009 19:24:15 +0000 (UTC)
commit 4b278970ed97f2391021e13c2560b1909b2502d3
Author: Toms Bauģis <toms baugis gmail com>
Date: Sat Dec 5 19:23:19 2009 +0000
as the code has grown, split overview into three files matching the tabs to be able to manage it somehow
data/Makefile.am | 3 +
data/stats.ui | 784 ++++-----------------------------
data/stats_overview.ui | 236 ++++++++++
data/stats_reports.ui | 335 ++++++++++++++
data/stats_stats.ui | 273 ++++++++++++
hamster/Makefile.am | 9 +-
hamster/stats.py | 1025 +------------------------------------------
hamster/stats_overview.py | 204 +++++++++
hamster/stats_reports.py | 612 ++++++++++++++++++++++++++
hamster/stats_stats.py | 416 ++++++++++++++++++
hamster/widgets/facttree.py | 189 ++++++++
po/POTFILES.in | 6 +
12 files changed, 2377 insertions(+), 1715 deletions(-)
---
diff --git a/data/Makefile.am b/data/Makefile.am
index 1a89823..7f941f8 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -23,6 +23,9 @@ resources_DATA = \
Hamster_Applet.xml \
preferences.ui \
stats.ui \
+ stats_overview.ui \
+ stats_reports.ui \
+ stats_stats.ui \
edit_activity.ui \
applet.ui \
hamster.db
diff --git a/data/stats.ui b/data/stats.ui
index 097053d..8c8a229 100644
--- a/data/stats.ui
+++ b/data/stats.ui
@@ -1,694 +1,11 @@
<?xml version="1.0"?>
<interface>
- <!-- interface-requires gtk+ 2.12 -->
- <!-- interface-naming-policy toplevel-contextual -->
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
<!-- interface-local-resource-path /home/toms/cvs/hamster/data/art -->
- <object class="GtkWindow" id="stats_window">
- <property name="width_request">600</property>
- <property name="height_request">400</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="border_width">12</property>
- <property name="title" translatable="yes">Overview - Hamster</property>
- <property name="window_position">center</property>
- <property name="default_width">800</property>
- <property name="default_height">600</property>
- <property name="icon_name">hamster-applet</property>
- <signal name="key_press_event" handler="on_window_key_pressed"/>
- <signal name="delete_event" handler="on_close"/>
- <child>
- <object class="GtkNotebook" id="pages">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="tab_hborder">8</property>
- <property name="tab_vborder">4</property>
- <signal name="switch_page" handler="on_pages_switch_page"/>
- <child>
- <object class="GtkVBox" id="vbox1">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkToolbar" id="toolbar3">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <child>
- <object class="GtkToolButton" id="prev">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="tooltip_text" translatable="yes">Previous week</property>
- <property name="label" translatable="yes">Previous</property>
- <property name="stock_id">gtk-go-back</property>
- <signal name="clicked" handler="on_prev_clicked"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkToolButton" id="home">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="tooltip_text" translatable="yes">This week</property>
- <property name="label" translatable="yes">This Week</property>
- <property name="use_underline">True</property>
- <property name="stock_id">gtk-home</property>
- <signal name="clicked" handler="on_home_clicked"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkToolButton" id="next">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="tooltip_text" translatable="yes">Next week</property>
- <property name="label" translatable="yes">Next</property>
- <property name="stock_id">gtk-go-forward</property>
- <signal name="clicked" handler="on_next_clicked"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkSeparatorToolItem" id="toolbutton3">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- </object>
- <packing>
- <property name="expand">False</property>
- </packing>
- </child>
- <child>
- <object class="GtkToolButton" id="add">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="tooltip_text" translatable="yes">Add activity</property>
- <property name="label" translatable="yes">Add</property>
- <property name="stock_id">gtk-add</property>
- <signal name="clicked" handler="on_add_clicked"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkToolButton" id="remove">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="tooltip_text" translatable="yes">Remove activity</property>
- <property name="label" translatable="yes">Remove</property>
- <property name="stock_id">gtk-remove</property>
- <signal name="clicked" handler="on_remove_clicked"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkToolButton" id="edit">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="tooltip_text" translatable="yes">Edit activity</property>
- <property name="label" translatable="yes">Edit</property>
- <property name="stock_id">gtk-edit</property>
- <signal name="clicked" handler="on_edit_clicked"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkSeparatorToolItem" id="toolbutton1">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- </object>
- <packing>
- <property name="expand">False</property>
- </packing>
- </child>
- <child>
- <object class="GtkRadioToolButton" id="day">
- <property name="visible">True</property>
- <property name="tooltip_text" translatable="yes">Show single day</property>
- <property name="is_important">True</property>
- <property name="label" translatable="yes"> _Day</property>
- <property name="use_underline">True</property>
- <property name="icon_widget">day_icon</property>
- <property name="active">True</property>
- <signal name="toggled" handler="on_day_toggled"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkRadioToolButton" id="week">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="tooltip_text" translatable="yes">Show week</property>
- <property name="is_important">True</property>
- <property name="label" translatable="yes"> _Week</property>
- <property name="use_underline">True</property>
- <property name="icon_widget">week_icon</property>
- <property name="active">True</property>
- <signal name="toggled" handler="on_week_toggled"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkRadioToolButton" id="month">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="tooltip_text" translatable="yes">Show month</property>
- <property name="is_important">True</property>
- <property name="label" translatable="yes"> _Month</property>
- <property name="use_underline">True</property>
- <property name="icon_widget">month_icon</property>
- <property name="active">True</property>
- <signal name="toggled" handler="on_month_toggled"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkSeparatorToolItem" id="<separator>">
- <property name="visible">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkToolButton" id="report_button">
- <property name="visible">True</property>
- <property name="tooltip_text" translatable="yes">Save report</property>
- <property name="is_important">True</property>
- <property name="label" translatable="yes">Save Report</property>
- <property name="use_underline">True</property>
- <property name="stock_id">gtk-save</property>
- <signal name="clicked" handler="on_report_button_clicked"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="homogeneous">True</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkVBox" id="vbox6">
- <property name="visible">True</property>
- <property name="border_width">12</property>
- <property name="orientation">vertical</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkLabel" id="overview_label">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes"><b>Overview</b></property>
- <property name="use_markup">True</property>
- <property name="single_line_mode">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkHPaned" id="hpaned1">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="position">300</property>
- <property name="position_set">True</property>
- <child>
- <object class="GtkScrolledWindow" id="scrolledwindow1">
- <property name="width_request">300</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="hscrollbar_policy">automatic</property>
- <property name="vscrollbar_policy">automatic</property>
- <property name="shadow_type">in</property>
- <child>
- <object class="GtkTreeView" id="facts">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="enable_search">False</property>
- <signal name="key_press_event" handler="on_key_pressed"/>
- <signal name="row_activated" handler="on_facts_row_activated"/>
- </object>
- </child>
- </object>
- <packing>
- <property name="resize">False</property>
- <property name="shrink">True</property>
- </packing>
- </child>
- <child>
- <object class="GtkFrame" id="frame1">
- <property name="visible">True</property>
- <property name="label_xalign">0</property>
- <child>
- <object class="GtkEventBox" id="graph_frame">
- <property name="visible">True</property>
- <signal name="size_allocate" handler="on_graph_frame_size_allocate"/>
- <child>
- <object class="GtkVBox" id="vbox4">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkVBox" id="graphs">
- <property name="width_request">390</property>
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="border_width">4</property>
- <property name="orientation">vertical</property>
- <property name="spacing">24</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkFrame" id="fram1">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <object class="GtkHBox" id="week_box">
- <property name="visible">True</property>
- <property name="border_width">9</property>
- <property name="spacing">36</property>
- <child>
- <object class="GtkEventBox" id="totals_by_category">
- <property name="visible">True</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkEventBox" id="totals_by_day">
- <property name="visible">True</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- <child type="label">
- <object class="GtkLabel" id="dayview_caption">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label" translatable="yes">Week</property>
- <property name="use_markup">True</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkFrame" id="frame39">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <object class="GtkEventBox" id="totals_by_activity">
- <property name="visible">True</property>
- <property name="border_width">12</property>
- <child>
- <placeholder/>
- </child>
- </object>
- </child>
- <child type="label">
- <object class="GtkLabel" id="label5">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label" translatable="yes">Activity</property>
- <property name="use_markup">True</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="no_data_label">
- <property name="label" translatable="yes">No data for this interval</property>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- </child>
- <child type="label_item">
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="resize">True</property>
- <property name="shrink">True</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- <child type="tab">
- <object class="GtkLabel" id="overview_labe">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Overview</property>
- </object>
- <packing>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <object class="GtkVBox" id="vbox2">
- <property name="visible">True</property>
- <property name="border_width">12</property>
- <property name="orientation">vertical</property>
- <property name="spacing">12</property>
- <child>
- <object class="GtkAlignment" id="alignment4">
- <property name="visible">True</property>
- <property name="yscale">0</property>
- <child>
- <object class="GtkTable" id="explore_controls">
- <property name="visible">True</property>
- <property name="n_columns">2</property>
- <property name="column_spacing">12</property>
- <property name="row_spacing">6</property>
- <child>
- <object class="GtkLabel" id="label3">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">Year:</property>
- </object>
- <packing>
- <property name="x_options">GTK_FILL</property>
- <property name="y_options">GTK_FILL</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="alignment5">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="xscale">0</property>
- <child>
- <object class="GtkHBox" id="year_box">
- <property name="visible">True</property>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- </object>
- </child>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="right_attach">2</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkFrame" id="frame2">
- <property name="visible">True</property>
- <property name="label_xalign">0</property>
- <child>
- <object class="GtkEventBox" id="explore_frame">
- <property name="visible">True</property>
- <child>
- <object class="GtkAlignment" id="alignment7">
- <property name="visible">True</property>
- <property name="top_padding">12</property>
- <property name="bottom_padding">12</property>
- <property name="left_padding">12</property>
- <property name="right_padding">12</property>
- <child>
- <object class="GtkVBox" id="vbox3">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkVBox" id="statistics_box">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">12</property>
- <child>
- <object class="GtkAlignment" id="explore_everything">
- <property name="height_request">50</property>
- <property name="visible">True</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkHBox" id="vbox77">
- <property name="visible">True</property>
- <child>
- <object class="GtkVBox" id="vbox5">
- <property name="visible">True</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkHBox" id="hbox2">
- <property name="visible">True</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkLabel" id="label7">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="ypad">8</property>
- <property name="label" translatable="yes"><b>Starts and ends</b></property>
- <property name="use_markup">True</property>
- </object>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label8">
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes"><b>Totals</b></property>
- <property name="use_markup">True</property>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkHBox" id="hbox3">
- <property name="height_request">160</property>
- <property name="visible">True</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkAlignment" id="explore_weekday_starts_ends">
- <property name="visible">True</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="explore_weekday_totals">
- <property name="visible">True</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkHBox" id="hbox4">
- <property name="height_request">100</property>
- <property name="visible">True</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkAlignment" id="explore_category_starts_ends">
- <property name="visible">True</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="explore_category_totals">
- <property name="visible">True</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkAlignment" id="explore_summary">
- <property name="width_request">200</property>
- <property name="visible">True</property>
- <property name="xalign">0</property>
- <property name="yalign">0</property>
- <property name="top_padding">32</property>
- <property name="left_padding">30</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="not_enough_records_label">
- <property name="label">Text we say when there is not enough data</property>
- <property name="wrap">True</property>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- </child>
- </object>
- </child>
- <child type="label_item">
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child type="tab">
- <object class="GtkLabel" id="statistics_label">
- <property name="visible">True</property>
- <property name="label" translatable="yes">Statistics</property>
- </object>
- <packing>
- <property name="position">1</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
<object class="GtkFileChooserDialog" id="save_report_dialog">
<property name="border_width">5</property>
- <property name="title" translatable="yes">Save report â?? Time Tracker</property>
+ <property name="title" translatable="yes">Save report – Time Tracker</property>
<property name="window_position">center</property>
<property name="type_hint">normal</property>
<property name="has_separator">False</property>
@@ -698,7 +15,7 @@
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
- <object class="GtkAlignment" id="alignment3">
+ <object class="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<property name="top_padding">8</property>
<property name="left_padding">8</property>
@@ -721,7 +38,7 @@
</packing>
</child>
<child>
- <object class="GtkLabel" id="label1">
+ <object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
@@ -734,12 +51,12 @@
</packing>
</child>
<child>
- <object class="GtkAlignment" id="alignment1">
+ <object class="GtkAlignment" id="alignment6">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="xscale">0</property>
<child>
- <object class="GtkHBox" id="hbox1">
+ <object class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<child>
<object class="GtkAlignment" id="from_date_box">
@@ -753,7 +70,7 @@
</packing>
</child>
<child>
- <object class="GtkLabel" id="label2">
+ <object class="GtkLabel" id="label11">
<property name="visible">True</property>
<property name="xpad">6</property>
<property name="label" translatable="yes">to</property>
@@ -783,7 +100,7 @@
</packing>
</child>
<child>
- <object class="GtkAlignment" id="alignment2">
+ <object class="GtkAlignment" id="alignment8">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="xscale">0</property>
@@ -866,17 +183,76 @@
<action-widget response="0">save_button</action-widget>
</action-widgets>
</object>
- <object class="GtkImage" id="day_icon">
- <property name="visible">True</property>
- <property name="pixbuf">art/stock_calendar-view-day.png</property>
- </object>
- <object class="GtkImage" id="week_icon">
- <property name="visible">True</property>
- <property name="pixbuf">art/stock_calendar-view-week.png</property>
- </object>
- <object class="GtkImage" id="month_icon">
- <property name="visible">True</property>
- <property name="tooltip_text" translatable="yes">Show month</property>
- <property name="pixbuf">art/stock_calendar-view-month.png</property>
+ <object class="GtkWindow" id="tabs_window">
+ <property name="border_width">12</property>
+ <property name="title" translatable="yes">Overview - Hamster</property>
+ <child>
+ <object class="GtkNotebook" id="window_tabs">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="tab_hborder">8</property>
+ <property name="tab_vborder">4</property>
+ <signal name="switch_page" handler="on_window_tabs_switch_page"/>
+ <child>
+ <object class="GtkAlignment" id="overview_tab">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="overview_labe1">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Overview</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="reports_tab">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="statistics_label1">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Reports</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="stats_tab">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label17">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Statistics</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
</object>
</interface>
diff --git a/data/stats_overview.ui b/data/stats_overview.ui
new file mode 100644
index 0000000..72f74d1
--- /dev/null
+++ b/data/stats_overview.ui
@@ -0,0 +1,236 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkWindow" id="overview_window">
+ <child>
+ <object class="GtkVBox" id="overview_box">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkToolbar" id="toolbar3">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkToolButton" id="prev2">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Previous</property>
+ <property name="stock_id">gtk-go-back</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="home2">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">This Week</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-home</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="next2">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Next</property>
+ <property name="stock_id">gtk-go-forward</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparatorToolItem" id="toolbutton1">
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="add">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Add</property>
+ <property name="stock_id">gtk-add</property>
+ <signal name="clicked" handler="on_add_clicked"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="remove">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Remove</property>
+ <property name="stock_id">gtk-remove</property>
+ <signal name="clicked" handler="on_remove_clicked"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="edit">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="label" translatable="yes">Edit</property>
+ <property name="stock_id">gtk-edit</property>
+ <signal name="clicked" handler="on_edit_clicked"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="border_width">12</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkAlignment" id="alignment1">
+ <property name="width_request">600</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="overview_facts_box">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame4">
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkVBox" id="vbox9">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">8</property>
+ <child>
+ <object class="GtkVBox" id="vbox10">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Categories</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTreeView" id="treeview1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox11">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">4</property>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Tags</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTreeView" id="treeview2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Filter</property>
+ <property name="use_markup">True</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/data/stats_reports.ui b/data/stats_reports.ui
new file mode 100644
index 0000000..68a2cd5
--- /dev/null
+++ b/data/stats_reports.ui
@@ -0,0 +1,335 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkWindow" id="reports_window">
+ <child>
+ <object class="GtkVBox" id="reports_box">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkToolbar" id="toolbar1">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <child>
+ <object class="GtkToolButton" id="reports_prev">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Previous</property>
+ <property name="stock_id">gtk-go-back</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="reports_home">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">This Week</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-home</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="reports_next">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Next</property>
+ <property name="stock_id">gtk-go-forward</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparatorToolItem" id="toolbutton2">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioToolButton" id="reports_day_view">
+ <property name="visible">True</property>
+ <property name="is_important">True</property>
+ <property name="label" translatable="yes"> _Day</property>
+ <property name="use_underline">True</property>
+ <property name="icon_widget">day_icon1</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioToolButton" id="reports_week_view">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="is_important">True</property>
+ <property name="label" translatable="yes"> _Week</property>
+ <property name="use_underline">True</property>
+ <property name="icon_widget">week_icon1</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioToolButton" id="reports_month_view">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="is_important">True</property>
+ <property name="label" translatable="yes"> _Month</property>
+ <property name="use_underline">True</property>
+ <property name="icon_widget">month_icon1</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparatorToolItem" id="<separator>1">
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToolButton" id="report_button">
+ <property name="visible">True</property>
+ <property name="is_important">True</property>
+ <property name="label" translatable="yes">Save Report</property>
+ <property name="use_underline">True</property>
+ <property name="stock_id">gtk-save</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox6">
+ <property name="visible">True</property>
+ <property name="border_width">12</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="overview_label">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes"><b>Overview</b></property>
+ <property name="use_markup">True</property>
+ <property name="single_line_mode">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHPaned" id="hpaned1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="position">300</property>
+ <property name="position_set">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="width_request">300</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTreeView" id="facts">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="enable_search">False</property>
+ <signal name="key_press_event" handler="on_key_pressed"/>
+ <signal name="row_activated" handler="on_facts_row_activated"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">False</property>
+ <property name="shrink">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <property name="visible">True</property>
+ <property name="label_xalign">0</property>
+ <child>
+ <object class="GtkEventBox" id="graph_frame">
+ <property name="visible">True</property>
+ <signal name="size_allocate" handler="on_graph_frame_size_allocate"/>
+ <child>
+ <object class="GtkVBox" id="vbox4">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkVBox" id="graphs">
+ <property name="width_request">390</property>
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="border_width">4</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">24</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkFrame" id="fram1">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkHBox" id="week_box">
+ <property name="visible">True</property>
+ <property name="border_width">9</property>
+ <property name="spacing">36</property>
+ <child>
+ <object class="GtkEventBox" id="totals_by_category">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEventBox" id="totals_by_day">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="dayview_caption">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Week</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame39">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkEventBox" id="totals_by_activity">
+ <property name="visible">True</property>
+ <property name="border_width">12</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Activity</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="no_data_label">
+ <property name="no_show_all">True</property>
+ <property name="label" translatable="yes">No data for this interval</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label_item">
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="resize">True</property>
+ <property name="shrink">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">300</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkImage" id="day_icon1">
+ <property name="visible">True</property>
+ <property name="pixbuf">art/stock_calendar-view-day.png</property>
+ </object>
+ <object class="GtkImage" id="week_icon1">
+ <property name="visible">True</property>
+ <property name="pixbuf">art/stock_calendar-view-week.png</property>
+ </object>
+ <object class="GtkImage" id="month_icon1">
+ <property name="visible">True</property>
+ <property name="tooltip_text" translatable="yes">Show month</property>
+ <property name="pixbuf">art/stock_calendar-view-month.png</property>
+ </object>
+</interface>
diff --git a/data/stats_stats.ui b/data/stats_stats.ui
new file mode 100644
index 0000000..839dd1f
--- /dev/null
+++ b/data/stats_stats.ui
@@ -0,0 +1,273 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkWindow" id="stats_window">
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="border_width">12</property>
+ <property name="window_position">center</property>
+ <property name="default_width">800</property>
+ <property name="default_height">600</property>
+ <property name="icon_name">hamster-applet</property>
+ <signal name="key_press_event" handler="on_window_key_pressed"/>
+ <signal name="delete_event" handler="on_close"/>
+ <child>
+ <object class="GtkVBox" id="stats_box">
+ <property name="visible">True</property>
+ <property name="border_width">12</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkAlignment" id="alignment3">
+ <property name="visible">True</property>
+ <property name="yscale">0</property>
+ <child>
+ <object class="GtkTable" id="explore_controls">
+ <property name="visible">True</property>
+ <property name="n_columns">2</property>
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label10">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Year:</property>
+ </object>
+ <packing>
+ <property name="x_options">GTK_FILL</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment5">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="xscale">0</property>
+ <child>
+ <object class="GtkHBox" id="year_box">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame" id="frame2">
+ <property name="visible">True</property>
+ <property name="label_xalign">0</property>
+ <child>
+ <object class="GtkEventBox" id="explore_frame">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkAlignment" id="alignment7">
+ <property name="visible">True</property>
+ <property name="top_padding">12</property>
+ <property name="bottom_padding">12</property>
+ <property name="left_padding">12</property>
+ <property name="right_padding">12</property>
+ <child>
+ <object class="GtkVBox" id="vbox3">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkVBox" id="statistics_box">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkAlignment" id="explore_everything">
+ <property name="height_request">50</property>
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="vbox77">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkVBox" id="vbox5">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkHBox" id="hbox5">
+ <property name="visible">True</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="ypad">8</property>
+ <property name="label" translatable="yes"><b>Starts and ends</b></property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes"><b>Totals</b></property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox3">
+ <property name="height_request">160</property>
+ <property name="visible">True</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkAlignment" id="explore_weekday_starts_ends">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="explore_weekday_totals">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox4">
+ <property name="height_request">100</property>
+ <property name="visible">True</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkAlignment" id="explore_category_starts_ends">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="explore_category_totals">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="explore_summary">
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="top_padding">32</property>
+ <property name="left_padding">30</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="not_enough_records_label">
+ <property name="label">Text we say when there is not enough data</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label_item">
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/hamster/Makefile.am b/hamster/Makefile.am
index a93c842..0773f3b 100644
--- a/hamster/Makefile.am
+++ b/hamster/Makefile.am
@@ -18,9 +18,12 @@ hamster_PYTHON = \
db.py \
eds.py \
stats.py \
+ stats_overview.py \
+ stats_reports.py \
+ stats_stats.py \
reports.py \
charting.py \
- graphics.py \
+ graphics.py \
configuration.py \
KeyBinder.py \
preferences.py \
@@ -30,8 +33,8 @@ hamster_PYTHON = \
about.py \
hamsterdbus.py \
idle.py \
- i18n.py \
- pytweener.py \
+ i18n.py \
+ pytweener.py \
__init__.py
BUILT_SOURCES = \
diff --git a/hamster/stats.py b/hamster/stats.py
index a529e23..678088a 100644
--- a/hamster/stats.py
+++ b/hamster/stats.py
@@ -26,1043 +26,52 @@ import gtk, gobject
import pango
import stuff
-import charting
-
-from edit_activity import CustomFactController
-import reports, graphics
import widgets
from configuration import runtime, GconfStore
-import webbrowser
-
-from itertools import groupby
-from gettext import ngettext
-import datetime as dt
-import calendar
-import time
-from hamster.i18n import C_
+from stats_overview import OverviewBox
+from stats_reports import ReportsBox
+from stats_stats import StatsBox
class StatsViewer(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.window = self.get_widget('stats_window')
- self.stat_facts = None
-
- #id, caption, duration, date (invisible), description, category
- self.fact_store = gtk.TreeStore(int, str, str, str, str, str, gobject.TYPE_PYOBJECT)
- self.setup_tree()
-
-
- #graphs
- self.background = (0.975, 0.975, 0.975)
- self.get_widget("graph_frame").modify_bg(gtk.STATE_NORMAL,
- gtk.gdk.Color(*[int(b*65536.0) for b in self.background]))
- self.get_widget("explore_frame").modify_bg(gtk.STATE_NORMAL,
- gtk.gdk.Color(*[int(b*65536.0) for b in self.background]))
-
- x_offset = 90 # align all graphs to the left edge
+ self.window = self.get_widget("tabs_window")
- self.category_chart = charting.BarChart(background = self.background,
- bar_base_color = (238,221,221),
- legend_width = x_offset,
- max_bar_width = 35,
- show_stack_labels = True
- )
- self.get_widget("totals_by_category").add(self.category_chart)
-
-
- self.day_chart = charting.BarChart(background = self.background,
- bar_base_color = (220, 220, 220),
- show_scale = True,
- max_bar_width = 35,
- grid_stride = 4,
- legend_width = 20)
- self.get_widget("totals_by_day").add(self.day_chart)
+ self.overview = OverviewBox()
+ self.get_widget("overview_tab").add(self.overview)
+ self.reports = ReportsBox()
+ self.get_widget("reports_tab").add(self.reports)
- self.activity_chart = charting.HorizontalBarChart(orient = "horizontal",
- max_bar_width = 25,
- values_on_bars = True,
- stretch_grid = True,
- legend_width = x_offset,
- value_format = "%.1f",
- background = self.background,
- bars_beveled = False,
- animate = False)
- self.get_widget("totals_by_activity").add(self.activity_chart);
-
-
- self.view_date = dt.date.today()
-
- #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())
-
- self.end_date = self.start_date + dt.timedelta(6)
-
-
- self.week_view = self.get_widget("week")
- self.month_view = self.get_widget("month")
- self.month_view.set_group(self.week_view)
- self.day_view = self.get_widget("day")
- self.day_view.set_group(self.week_view)
-
- #initiate the form in the week view
- self.week_view.set_active(True)
-
-
- runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
- runtime.dispatcher.add_handler('day_updated', self.after_fact_update)
-
- selection = self.fact_tree.get_selection()
- selection.connect('changed', self.on_fact_selection_changed,
- self.fact_store)
- self.popular_categories = [cat[0] for cat in runtime.storage.get_popular_categories()]
+ self.stats = StatsBox()
+ self.get_widget("stats_tab").add(self.stats)
self._gui.connect_signals(self)
- self.fact_tree.grab_focus()
-
- self.timeline = widgets.TimeLine()
- self.get_widget("explore_everything").add(self.timeline)
- self.get_widget("explore_everything").show_all()
-
-
- self.config = GconfStore()
- runtime.dispatcher.add_handler('gconf_on_day_start_changed', self.on_day_start_changed)
-
- self.report_chooser = None
- self.do_graph()
- self.init_stats()
-
-
- def on_day_start_changed(self, event, new_minutes):
- self.do_graph()
-
- def init_stats(self):
- self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
-
- 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,
- background = self.background,
- 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,
- background = self.background,
- 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,
- background = self.background,
- 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,
- background = self.background,
- 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, background = None, fontsize = 10):
- graphics.Area.__init__(self)
- self.background = background
- self.text = ""
- self.fontsize = fontsize
-
- def set_text(self, text):
- self.text = text
- self.redraw_canvas()
-
- def on_expose(self):
- if self.background:
- self.fill_area(0, 0, self.width, self.height, self.background)
-
- 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(charting.graphics.Colors.aluminium[5])
-
- self.context.show_layout(self.layout)
-
- self.explore_summary = CairoText(self.background)
- 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(_("Still 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
- self.timeline.draw(facts)
-
-
- # 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:
- 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]))
-
- 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
+ self.window.show_all()
- # 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:
- 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])
-
- 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>%.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}
-
- # 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 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))
-
- self.explore_summary.set_text(summary)
-
- def setup_tree(self):
- def parent_painter(column, cell, model, iter):
- cell_text = model.get_value(iter, 1)
- if model.iter_parent(iter) is None:
- if model.get_path(iter) == (0,):
- text = '<span weight="heavy">%s</span>' % cell_text
- else:
- text = '<span weight="heavy" rise="-20000">%s</span>' % cell_text
-
- cell.set_property('markup', text)
-
- else:
- activity_name = stuff.escape_pango(cell_text)
- description = stuff.escape_pango(model.get_value(iter, 4))
- category = stuff.escape_pango(model.get_value(iter, 5))
-
- markup = stuff.format_activity(activity_name,
- category,
- description,
- pad_description = True)
- cell.set_property('markup', markup)
-
- def duration_painter(column, cell, model, iter):
- cell.set_property('xalign', 1)
- cell.set_property('yalign', 0)
-
-
- text = model.get_value(iter, 2)
- if model.iter_parent(iter) is None:
- if model.get_path(iter) == (0,):
- text = '<span weight="heavy">%s</span>' % text
- else:
- text = '<span weight="heavy" rise="-20000">%s</span>' % text
- cell.set_property('markup', text)
-
-
- self.fact_tree = self.get_widget("facts")
- self.fact_tree.set_headers_visible(False)
- self.fact_tree.set_tooltip_column(1)
- self.fact_tree.set_property("show-expanders", False)
-
- # name
- nameColumn = gtk.TreeViewColumn()
- nameColumn.set_expand(True)
- nameCell = gtk.CellRendererText()
- nameCell.set_property("ellipsize", pango.ELLIPSIZE_END)
- nameColumn.pack_start(nameCell, True)
- nameColumn.set_cell_data_func(nameCell, parent_painter)
- self.fact_tree.append_column(nameColumn)
-
- # duration
- timeColumn = gtk.TreeViewColumn()
- timeCell = gtk.CellRendererText()
- timeColumn.pack_end(timeCell, True)
- timeColumn.set_cell_data_func(timeCell, duration_painter)
-
-
-
-
- self.fact_tree.append_column(timeColumn)
-
- self.fact_tree.set_model(self.fact_store)
-
- def on_graph_frame_size_allocate(self, widget, new_size):
- w = min(new_size.width / 4, 200)
-
- self.activity_chart.legend_width = w
- self.category_chart.legend_width = w
- self.get_widget("totals_by_category").set_size_request(w + 40, -1)
-
- def fill_tree(self, facts):
- day_dict = {}
- for day, facts in groupby(facts, lambda fact: fact["date"]):
- day_dict[day] = sorted(list(facts),
- key=lambda fact: fact["start_time"])
-
- for i in range((self.end_date - self.start_date).days + 1):
- current_date = self.start_date + dt.timedelta(i)
-
- # Date format for the label in overview window fact listing
- # Using python datetime formatting syntax. See:
- # http://docs.python.org/library/time.html#time.strftime
- fact_date = current_date.strftime(C_("overview list", "%A, %b %d"))
-
- day_total = dt.timedelta()
- for fact in day_dict.get(current_date, []):
- day_total += fact["delta"]
-
- day_row = self.fact_store.append(None,
- [-1,
- fact_date,
- stuff.format_duration(day_total),
- current_date.strftime('%Y-%m-%d'),
- "",
- "",
- None])
-
- for fact in day_dict.get(current_date, []):
- self.fact_store.append(day_row,
- [fact["id"],
- fact["start_time"].strftime('%H:%M') + " " +
- fact["name"],
- stuff.format_duration(fact["delta"]),
- fact["start_time"].strftime('%Y-%m-%d'),
- fact["description"],
- fact["category"],
- fact
- ])
-
- self.fact_tree.expand_all()
-
-
- def do_charts(self, facts):
- all_categories = self.popular_categories
-
-
- #the single "totals" (by category) bar
- category_sums = stuff.totals(facts, lambda fact: fact["category"],
- lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0)
- category_totals = [category_sums.get(cat, 0) for cat in all_categories]
- category_keys = ["%s %.1f" % (cat, category_sums.get(cat, 0.0))
- for cat in all_categories]
- self.category_chart.plot([_("Total")],
- [category_totals],
- stack_keys = category_keys)
-
- # day / category chart
- all_days = [self.start_date + dt.timedelta(i)
- for i in range((self.end_date - self.start_date).days + 1)]
-
- by_date_cat = stuff.totals(facts,
- lambda fact: (fact["date"], fact["category"]),
- lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0)
-
- res = [[by_date_cat.get((day, cat), 0)
- for cat in all_categories] for day in all_days]
-
-
- #show days or dates depending on scale
- if (self.end_date - self.start_date).days < 20:
- day_keys = [day.strftime("%a") for day in all_days]
- else:
- # date format used in the overview graph when month view is selected
- # Using python datetime formatting syntax. See:
- # http://docs.python.org/library/time.html#time.strftime
- day_keys = [day.strftime(C_("overview graph", "%b %d"))
- for day in all_days]
-
- self.day_chart.plot(day_keys, res, stack_keys = all_categories)
-
-
- #totals by activity, disguised under a stacked bar chart to get category colors
- activity_sums = stuff.totals(facts,
- lambda fact: (fact["name"],
- fact["category"]),
- lambda fact: stuff.duration_minutes(fact["delta"]))
-
- #now join activities with same name
- activities = {}
- for key in activity_sums.keys():
- activities.setdefault(key[0], [0.0] * len(all_categories))
- activities[key[0]][all_categories.index(key[1])] = activity_sums[key] / 60.0
-
- by_duration = sorted(activities.items(),
- key = lambda x: sum(x[1]),
- reverse = True)
- by_duration_keys = [entry[0] for entry in by_duration]
-
- by_duration = [entry[1] for entry in by_duration]
-
- self.activity_chart.plot(by_duration_keys,
- by_duration,
- stack_keys = all_categories)
-
-
- def set_title(self):
- if self.day_view.get_active():
- # 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 = self.view_date.strftime(C_("single day overview",
- "%B %d, %Y"))
- # Overview label if looking on single day
- overview_label = _(u"Overview for %(date)s") % \
- ({"date": start_date_str})
- else:
- dates_dict = stuff.dateDict(self.start_date, "start_")
- dates_dict.update(stuff.dateDict(self.end_date, "end_"))
-
- if self.start_date.year != self.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
- overview_label = _(u"Overview for %(start_B)s %(start_d)s, %(start_Y)s â?? %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
- elif self.start_date.month != self.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
- overview_label = _(u"Overview for %(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
- overview_label = _(u"Overview for %(start_B)s %(start_d)s â?? %(end_d)s, %(end_Y)s") % dates_dict
-
- if self.week_view.get_active():
- dayview_caption = _("Week")
- elif self.month_view.get_active():
- dayview_caption = _("Month")
- else:
- dayview_caption = _("Day")
-
- self.get_widget("overview_label").set_markup("<b>%s</b>" % overview_label)
- self.get_widget("dayview_caption").set_markup("%s" % (dayview_caption))
-
-
- def do_graph(self):
- self.set_title()
-
- if self.day_view.get_active():
- facts = runtime.storage.get_facts(self.view_date)
- else:
- facts = runtime.storage.get_facts(self.start_date, self.end_date)
-
-
- self.get_widget("report_button").set_sensitive(len(facts) > 0)
- self.fact_store.clear()
-
- self.fill_tree(facts)
-
- if not facts:
- self.get_widget("graphs").hide()
- self.get_widget("no_data_label").show()
- return
-
-
- self.get_widget("no_data_label").hide()
- self.get_widget("graphs").show()
- self.do_charts(facts)
-
-
-
def get_widget(self, name):
""" skip one variable (huh) """
return self._gui.get_object(name)
- def on_pages_switch_page(self, notebook, page, pagenum):
- if pagenum == 1:
+ def on_window_tabs_switch_page(self, notebook, page, pagenum):
+ if pagenum == 2:
year = None
- for child in self.get_widget("year_box").get_children():
+ for child in self.stats.get_widget("year_box").get_children():
if child.get_active():
year = child.year
- self.stats(year)
- else:
- self.do_graph()
-
- 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 on_prev_clicked(self, button):
- if self.day_view.get_active():
- self.view_date -= dt.timedelta(1)
- if self.view_date < self.start_date:
- self.start_date -= dt.timedelta(7)
- self.end_date -= dt.timedelta(7)
- else:
- if self.week_view.get_active():
- self.start_date -= dt.timedelta(7)
- self.end_date -= dt.timedelta(7)
-
- elif self.month_view.get_active():
- 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.do_graph()
-
- def on_next_clicked(self, button):
- if self.day_view.get_active():
- self.view_date += dt.timedelta(1)
- if self.view_date > self.end_date:
- self.start_date += dt.timedelta(7)
- self.end_date += dt.timedelta(7)
- else:
- if self.week_view.get_active():
- self.start_date += dt.timedelta(7)
- self.end_date += dt.timedelta(7)
- elif self.month_view.get_active():
- 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.do_graph()
-
- def on_home_clicked(self, button):
- self.view_date = dt.date.today()
- if self.week_view.get_active():
- 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.month_view.get_active():
- 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.do_graph()
-
- def on_day_toggled(self, button):
- 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("prev").set_tooltip_text(_("Previous day"))
- self.get_widget("next").set_tooltip_text(_("Next day"))
- self.get_widget("home").set_tooltip_text(_("Today"))
- self.get_widget("home").set_label(_("Today"))
-
- self.do_graph()
-
- def on_week_toggled(self, button):
- 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("prev").set_tooltip_text(_("Previous week"))
- self.get_widget("next").set_tooltip_text(_("Next week"))
- self.get_widget("home").set_tooltip_text(_("This week"))
- self.get_widget("home").set_label(_("This Week"))
- self.do_graph()
-
-
- def on_month_toggled(self, button):
- 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("prev").set_tooltip_text(_("Previous month"))
- self.get_widget("next").set_tooltip_text(_("Next month"))
- self.get_widget("home").set_tooltip_text(_("This month"))
- self.get_widget("home").set_label(_("This Month"))
- self.do_graph()
-
- def on_remove_clicked(self, button):
- self.delete_selected()
-
- def on_edit_clicked(self, button):
- selection = self.fact_tree.get_selection()
- (model, iter) = selection.get_selected()
-
- if model[iter][0] == -1:
- return #not a fact
-
- custom_fact = CustomFactController(self, None, model[iter][0])
- custom_fact.show()
-
- def delete_selected(self):
- selection = self.fact_tree.get_selection()
- (model, iter) = selection.get_selected()
-
- if model[iter][0] == -1:
- return #not a fact
-
- next_row = model.iter_next(iter)
-
- if next_row:
- selection.select_iter(next_row)
- else:
- path = model.get_path(iter)[0] - 1
- if path > 0:
- selection.select_path(path)
-
- runtime.storage.remove_fact(model[iter][0])
-
- def copy_selected(self):
- selection = self.fact_tree.get_selection()
- (model, iter) = selection.get_selected()
-
- fact = model[iter][6]
- if not fact:
- return #not a fact
-
- fact_str = "%s-%s %s" % (fact["start_time"].strftime("%H:%M"),
- (fact["end_time"] or dt.datetime.now()).strftime("%H:%M"),
- fact["name"])
-
- if fact["category"]:
- fact_str += "@%s" % fact["category"]
-
- if fact["description"]:
- fact_str += ", %s" % fact["description"]
-
- clipboard = gtk.Clipboard()
- clipboard.set_text(fact_str)
-
- def check_clipboard(self):
- clipboard = gtk.Clipboard()
- clipboard.request_text(self.on_clipboard_text)
-
- def on_clipboard_text(self, clipboard, text, data):
- # first check that we have a date selected
- selection = self.fact_tree.get_selection()
- (model, iter) = selection.get_selected()
-
- selected_date = self.view_date
- if iter:
- selected_date = model[iter][3].split("-")
- selected_date = dt.date(int(selected_date[0]),
- int(selected_date[1]),
- int(selected_date[2]))
- if not selected_date:
- return
-
- res = stuff.parse_activity_input(text)
-
- if res.start_time is None or res.end_time is None:
- return
-
- start_time = res.start_time.replace(year = selected_date.year,
- month = selected_date.month,
- day = selected_date.day)
- end_time = res.end_time.replace(year = selected_date.year,
- month = selected_date.month,
- day = selected_date.day)
-
- activity_name = res.activity_name
- if res.category_name:
- activity_name += "@%s" % res.category_name
-
- if res.description:
- activity_name += ", %s" % res.description
-
- activity_name = activity_name.decode("utf-8")
+ self.stats.stats(year)
+ elif pagenum == 1:
+ self.reports.do_graph()
- # TODO - set cursor to the pasted entry when done
- # TODO - revisit parsing of selected date
- added_fact = runtime.storage.add_fact(activity_name, start_time, end_time)
-
-
- """keyboard events"""
- def on_key_pressed(self, tree, event):
- if (event.keyval == gtk.keysyms.Delete):
- self.delete_selected()
- elif event.keyval == gtk.keysyms.c and event.state & gtk.gdk.CONTROL_MASK:
- self.copy_selected()
- elif event.keyval == gtk.keysyms.v and event.state & gtk.gdk.CONTROL_MASK:
- self.check_clipboard()
-
- def on_fact_selection_changed(self, selection, model):
- """ enables and disables action buttons depending on selected item """
- (model, iter) = selection.get_selected()
-
- id = -1
- if iter:
- id = model[iter][0]
-
- self.get_widget('remove').set_sensitive(id != -1)
- self.get_widget('edit').set_sensitive(id != -1)
-
- return True
-
- def on_facts_row_activated(self, tree, path, column):
- selection = tree.get_selection()
- (model, iter) = selection.get_selected()
- custom_fact = CustomFactController(self, None, model[iter][0])
- custom_fact.show()
-
- def on_add_clicked(self, button):
- selection = self.fact_tree.get_selection()
- (model, iter) = selection.get_selected()
-
- selected_date = self.view_date
- if iter:
- selected_date = model[iter][3].split("-")
- selected_date = dt.date(int(selected_date[0]),
- int(selected_date[1]),
- int(selected_date[2]))
-
- custom_fact = CustomFactController(self, selected_date)
- custom_fact.show()
-
- def init_report_dialog(self):
- chooser = self.get_widget('save_report_dialog')
- chooser.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
- """
- chooser.set
-
- chooser = gtk.FileChooserDialog(title = _("Save report - Time Tracker"),
- parent = None,
- buttons=(gtk.STOCK_CANCEL,
- gtk.RESPONSE_CANCEL,
- gtk.STOCK_SAVE,
- gtk.RESPONSE_OK))
- """
- chooser.set_current_folder(os.path.expanduser("~"))
-
- filters = {}
-
- filter = gtk.FileFilter()
- filter.set_name(_("HTML Report"))
- filter.add_mime_type("text/html")
- filter.add_pattern("*.html")
- filter.add_pattern("*.htm")
- filters[filter] = "html"
- chooser.add_filter(filter)
-
- filter = gtk.FileFilter()
- filter.set_name(_("Tab-Separated Values (TSV)"))
- filter.add_mime_type("text/plain")
- filter.add_pattern("*.tsv")
- filter.add_pattern("*.txt")
- filters[filter] = "tsv"
- chooser.add_filter(filter)
-
- filter = gtk.FileFilter()
- filter.set_name(_("XML"))
- filter.add_mime_type("text/xml")
- filter.add_pattern("*.xml")
- filters[filter] = "xml"
- chooser.add_filter(filter)
-
- filter = gtk.FileFilter()
- filter.set_name(_("iCal"))
- filter.add_mime_type("text/calendar")
- filter.add_pattern("*.ics")
- filters[filter] = "ical"
- chooser.add_filter(filter)
-
- filter = gtk.FileFilter()
- filter.set_name("All files")
- filter.add_pattern("*")
- chooser.add_filter(filter)
-
- def on_report_chosen(self, widget, format, path, start_date, end_date,
- categories):
- self.report_chooser = None
-
- facts = runtime.storage.get_facts(start_date, end_date, category_id = categories)
- reports.simple(facts,
- start_date,
- 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(self, widget):
- self.report_chooser = None
-
- def on_report_button_clicked(self, widget):
- if not self.report_chooser:
- self.report_chooser = widgets.ReportChooserDialog()
- self.report_chooser.connect("report-chosen", self.on_report_chosen)
- self.report_chooser.connect("report-chooser-closed",
- self.on_report_chooser_closed)
- self.report_chooser.show(self.start_date, self.end_date)
- else:
- self.report_chooser.present()
-
-
- def after_activity_update(self, widget, renames):
- self.do_graph()
-
- def after_fact_update(self, event, date):
- self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
- self.popular_categories = [cat[0] for cat in runtime.storage.get_popular_categories()]
-
- if self.get_widget("pages").get_current_page() == 0:
- self.do_graph()
- else:
- self.stats()
-
def on_close(self, widget, event):
runtime.dispatcher.del_handler('activity_updated',
self.after_activity_update)
diff --git a/hamster/stats_overview.py b/hamster/stats_overview.py
new file mode 100644
index 0000000..ea356da
--- /dev/null
+++ b/hamster/stats_overview.py
@@ -0,0 +1,204 @@
+# - 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 gtk, gobject
+import pango
+
+import stuff
+import charting
+
+from edit_activity import CustomFactController
+import reports, graphics
+
+import widgets
+
+from configuration import runtime, GconfStore
+import webbrowser
+
+from itertools import groupby
+from gettext import ngettext
+
+import datetime as dt
+import calendar
+import time
+from hamster.i18n import C_
+
+
+
+class OverviewBox(gtk.VBox):
+ def __init__(self):
+ gtk.VBox.__init__(self)
+
+ self._gui = stuff.load_ui_file("stats_overview.ui")
+
+
+ self.get_widget("overview_box").reparent(self) #mine!
+
+
+
+ self.view_date = dt.date.today()
+
+ #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())
+
+ self.end_date = self.start_date + dt.timedelta(6)
+
+
+
+ facts_tree = widgets.FactTree()
+ self.get_widget("overview_facts_box").add(facts_tree)
+
+ facts = runtime.storage.get_facts(self.start_date, self.end_date)
+
+
+ #create list of all required dates
+ dates = [(self.start_date + dt.timedelta(i), []) for i in range((self.end_date - self.start_date).days + 1)]
+
+ #update with facts for the day
+ for date, facts in groupby(facts, lambda fact: fact["date"]):
+ dates[dates.index((date, []))] = (date, list(facts))
+
+ # push them in tree
+ for date, facts in dates:
+ fact_date = date.strftime(C_("overview list", "%A, %b %d"))
+
+ facts_tree.add_group(fact_date, facts)
+
+ def on_remove_clicked(self, button):
+ self.delete_selected()
+
+ def on_edit_clicked(self, button):
+ selection = self.fact_tree.get_selection()
+ (model, iter) = selection.get_selected()
+
+ if model[iter][0] == -1:
+ return #not a fact
+
+ custom_fact = CustomFactController(self, None, model[iter][0])
+ custom_fact.show()
+
+ def delete_selected(self):
+ selection = self.fact_tree.get_selection()
+ (model, iter) = selection.get_selected()
+
+ if model[iter][0] == -1:
+ return #not a fact
+
+ next_row = model.iter_next(iter)
+
+ if next_row:
+ selection.select_iter(next_row)
+ else:
+ path = model.get_path(iter)[0] - 1
+ if path > 0:
+ selection.select_path(path)
+
+ runtime.storage.remove_fact(model[iter][0])
+
+ def copy_selected(self):
+ selection = self.fact_tree.get_selection()
+ (model, iter) = selection.get_selected()
+
+ fact = model[iter][6]
+ if not fact:
+ return #not a fact
+
+ fact_str = "%s-%s %s" % (fact["start_time"].strftime("%H:%M"),
+ (fact["end_time"] or dt.datetime.now()).strftime("%H:%M"),
+ fact["name"])
+
+ if fact["category"]:
+ fact_str += "@%s" % fact["category"]
+
+ if fact["description"]:
+ fact_str += ", %s" % fact["description"]
+
+ clipboard = gtk.Clipboard()
+ clipboard.set_text(fact_str)
+
+ def check_clipboard(self):
+ clipboard = gtk.Clipboard()
+ clipboard.request_text(self.on_clipboard_text)
+
+ def on_clipboard_text(self, clipboard, text, data):
+ # first check that we have a date selected
+ selection = self.fact_tree.get_selection()
+ (model, iter) = selection.get_selected()
+
+ selected_date = self.view_date
+ if iter:
+ selected_date = model[iter][3].split("-")
+ selected_date = dt.date(int(selected_date[0]),
+ int(selected_date[1]),
+ int(selected_date[2]))
+ if not selected_date:
+ return
+
+ res = stuff.parse_activity_input(text)
+
+ if res.start_time is None or res.end_time is None:
+ return
+
+ start_time = res.start_time.replace(year = selected_date.year,
+ month = selected_date.month,
+ day = selected_date.day)
+ end_time = res.end_time.replace(year = selected_date.year,
+ month = selected_date.month,
+ day = selected_date.day)
+
+ activity_name = res.activity_name
+ if res.category_name:
+ activity_name += "@%s" % res.category_name
+
+ if res.description:
+ activity_name += ", %s" % res.description
+
+ activity_name = activity_name.decode("utf-8")
+
+ # TODO - set cursor to the pasted entry when done
+ # TODO - revisit parsing of selected date
+ added_fact = runtime.storage.add_fact(activity_name, start_time, end_time)
+
+
+ """keyboard events"""
+ def on_key_pressed(self, tree, event):
+ if (event.keyval == gtk.keysyms.Delete):
+ self.delete_selected()
+ elif event.keyval == gtk.keysyms.c and event.state & gtk.gdk.CONTROL_MASK:
+ self.copy_selected()
+ elif event.keyval == gtk.keysyms.v and event.state & gtk.gdk.CONTROL_MASK:
+ self.check_clipboard()
+
+
+ def get_widget(self, name):
+ """ skip one variable (huh) """
+ return self._gui.get_object(name)
+
+
+
+
diff --git a/hamster/stats_reports.py b/hamster/stats_reports.py
new file mode 100644
index 0000000..c20487e
--- /dev/null
+++ b/hamster/stats_reports.py
@@ -0,0 +1,612 @@
+# - 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 gtk, gobject
+import pango
+
+import stuff
+import charting
+
+from edit_activity import CustomFactController
+import reports, graphics
+
+import widgets
+
+from configuration import runtime, GconfStore
+import webbrowser
+
+from itertools import groupby
+from gettext import ngettext
+
+import datetime as dt
+import calendar
+import time
+from hamster.i18n import C_
+
+
+
+class ReportsBox(gtk.VBox):
+ def __init__(self):
+ gtk.VBox.__init__(self)
+ self._gui = stuff.load_ui_file("stats_reports.ui")
+ self.get_widget("reports_box").reparent(self) #mine!
+
+ self.view_date = dt.date.today()
+
+ #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())
+
+ self.end_date = self.start_date + dt.timedelta(6)
+
+
+
+ self.fact_store = gtk.TreeStore(int, str, str, str, str, str, gobject.TYPE_PYOBJECT)
+ self.setup_tree()
+
+
+ #graphs
+ self.background = (0.975, 0.975, 0.975)
+ self.get_widget("graph_frame").modify_bg(gtk.STATE_NORMAL,
+ gtk.gdk.Color(*[int(b*65536.0) for b in self.background]))
+
+
+ x_offset = 90 # align all graphs to the left edge
+
+ self.category_chart = charting.BarChart(background = self.background,
+ bar_base_color = (238,221,221),
+ legend_width = x_offset,
+ max_bar_width = 35,
+ show_stack_labels = True
+ )
+ self.get_widget("totals_by_category").add(self.category_chart)
+
+
+ self.day_chart = charting.BarChart(background = self.background,
+ bar_base_color = (220, 220, 220),
+ show_scale = True,
+ max_bar_width = 35,
+ grid_stride = 4,
+ legend_width = 20)
+ self.get_widget("totals_by_day").add(self.day_chart)
+
+
+ self.activity_chart = charting.HorizontalBarChart(orient = "horizontal",
+ max_bar_width = 25,
+ values_on_bars = True,
+ stretch_grid = True,
+ legend_width = x_offset,
+ value_format = "%.1f",
+ background = self.background,
+ bars_beveled = False,
+ animate = False)
+ self.get_widget("totals_by_activity").add(self.activity_chart);
+
+
+
+ self.week_view = self.get_widget("reports_week_view")
+ self.month_view = self.get_widget("reports_month_view")
+ self.month_view.set_group(self.week_view)
+ self.day_view = self.get_widget("reports_day_view")
+ self.day_view.set_group(self.week_view)
+
+ #initiate the form in the week view
+ self.week_view.set_active(True)
+
+
+ runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
+ runtime.dispatcher.add_handler('day_updated', self.after_fact_update)
+
+ selection = self.fact_tree.get_selection()
+ selection.connect('changed', self.on_fact_selection_changed,
+ self.fact_store)
+ self.popular_categories = [cat[0] for cat in runtime.storage.get_popular_categories()]
+
+ self._gui.connect_signals(self)
+ self.fact_tree.grab_focus()
+
+
+ self.config = GconfStore()
+ runtime.dispatcher.add_handler('gconf_on_day_start_changed', self.on_day_start_changed)
+
+ self.report_chooser = None
+ self.do_graph()
+
+ def setup_tree(self):
+ def parent_painter(column, cell, model, iter):
+ cell_text = model.get_value(iter, 1)
+ if model.iter_parent(iter) is None:
+ if model.get_path(iter) == (0,):
+ text = '<span weight="heavy">%s</span>' % cell_text
+ else:
+ text = '<span weight="heavy" rise="-20000">%s</span>' % cell_text
+
+ cell.set_property('markup', text)
+
+ else:
+ activity_name = stuff.escape_pango(cell_text)
+ description = stuff.escape_pango(model.get_value(iter, 4))
+ category = stuff.escape_pango(model.get_value(iter, 5))
+
+ markup = stuff.format_activity(activity_name,
+ category,
+ description,
+ pad_description = True)
+ cell.set_property('markup', markup)
+
+ def duration_painter(column, cell, model, iter):
+ cell.set_property('xalign', 1)
+ cell.set_property('yalign', 0)
+
+
+ text = model.get_value(iter, 2)
+ if model.iter_parent(iter) is None:
+ if model.get_path(iter) == (0,):
+ text = '<span weight="heavy">%s</span>' % text
+ else:
+ text = '<span weight="heavy" rise="-20000">%s</span>' % text
+ cell.set_property('markup', text)
+
+
+ self.fact_tree = self.get_widget("facts")
+ self.fact_tree.set_headers_visible(False)
+ self.fact_tree.set_tooltip_column(1)
+ self.fact_tree.set_property("show-expanders", False)
+
+ # name
+ nameColumn = gtk.TreeViewColumn()
+ nameColumn.set_expand(True)
+ nameCell = gtk.CellRendererText()
+ nameCell.set_property("ellipsize", pango.ELLIPSIZE_END)
+ nameColumn.pack_start(nameCell, True)
+ nameColumn.set_cell_data_func(nameCell, parent_painter)
+ self.fact_tree.append_column(nameColumn)
+
+ # duration
+ timeColumn = gtk.TreeViewColumn()
+ timeCell = gtk.CellRendererText()
+ timeColumn.pack_end(timeCell, True)
+ timeColumn.set_cell_data_func(timeCell, duration_painter)
+
+
+
+
+ self.fact_tree.append_column(timeColumn)
+
+ self.fact_tree.set_model(self.fact_store)
+
+ def on_graph_frame_size_allocate(self, widget, new_size):
+ w = min(new_size.width / 4, 200)
+
+ self.activity_chart.legend_width = w
+ self.category_chart.legend_width = w
+ self.get_widget("totals_by_category").set_size_request(w + 40, -1)
+
+ def fill_tree(self, facts):
+ day_dict = {}
+ for day, facts in groupby(facts, lambda fact: fact["date"]):
+ day_dict[day] = sorted(list(facts),
+ key=lambda fact: fact["start_time"])
+
+ for i in range((self.end_date - self.start_date).days + 1):
+ current_date = self.start_date + dt.timedelta(i)
+
+ # Date format for the label in overview window fact listing
+ # Using python datetime formatting syntax. See:
+ # http://docs.python.org/library/time.html#time.strftime
+ fact_date = current_date.strftime(C_("overview list", "%A, %b %d"))
+
+ day_total = dt.timedelta()
+ for fact in day_dict.get(current_date, []):
+ day_total += fact["delta"]
+
+ day_row = self.fact_store.append(None,
+ [-1,
+ fact_date,
+ stuff.format_duration(day_total),
+ current_date.strftime('%Y-%m-%d'),
+ "",
+ "",
+ None])
+
+ for fact in day_dict.get(current_date, []):
+ self.fact_store.append(day_row,
+ [fact["id"],
+ fact["start_time"].strftime('%H:%M') + " " +
+ fact["name"],
+ stuff.format_duration(fact["delta"]),
+ fact["start_time"].strftime('%Y-%m-%d'),
+ fact["description"],
+ fact["category"],
+ fact
+ ])
+
+ self.fact_tree.expand_all()
+
+
+ def do_charts(self, facts):
+ all_categories = self.popular_categories
+
+
+ #the single "totals" (by category) bar
+ category_sums = stuff.totals(facts, lambda fact: fact["category"],
+ lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0)
+ category_totals = [category_sums.get(cat, 0) for cat in all_categories]
+ category_keys = ["%s %.1f" % (cat, category_sums.get(cat, 0.0))
+ for cat in all_categories]
+ self.category_chart.plot([_("Total")],
+ [category_totals],
+ stack_keys = category_keys)
+
+ # day / category chart
+ all_days = [self.start_date + dt.timedelta(i)
+ for i in range((self.end_date - self.start_date).days + 1)]
+
+ by_date_cat = stuff.totals(facts,
+ lambda fact: (fact["date"], fact["category"]),
+ lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0)
+
+ res = [[by_date_cat.get((day, cat), 0)
+ for cat in all_categories] for day in all_days]
+
+
+ #show days or dates depending on scale
+ if (self.end_date - self.start_date).days < 20:
+ day_keys = [day.strftime("%a") for day in all_days]
+ else:
+ # date format used in the overview graph when month view is selected
+ # Using python datetime formatting syntax. See:
+ # http://docs.python.org/library/time.html#time.strftime
+ day_keys = [day.strftime(C_("overview graph", "%b %d"))
+ for day in all_days]
+
+ self.day_chart.plot(day_keys, res, stack_keys = all_categories)
+
+
+ #totals by activity, disguised under a stacked bar chart to get category colors
+ activity_sums = stuff.totals(facts,
+ lambda fact: (fact["name"],
+ fact["category"]),
+ lambda fact: stuff.duration_minutes(fact["delta"]))
+
+ #now join activities with same name
+ activities = {}
+ for key in activity_sums.keys():
+ activities.setdefault(key[0], [0.0] * len(all_categories))
+ activities[key[0]][all_categories.index(key[1])] = activity_sums[key] / 60.0
+
+ by_duration = sorted(activities.items(),
+ key = lambda x: sum(x[1]),
+ reverse = True)
+ by_duration_keys = [entry[0] for entry in by_duration]
+
+ by_duration = [entry[1] for entry in by_duration]
+
+ self.activity_chart.plot(by_duration_keys,
+ by_duration,
+ stack_keys = all_categories)
+
+
+ def set_title(self):
+ if self.day_view.get_active():
+ # 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 = self.view_date.strftime(C_("single day overview",
+ "%B %d, %Y"))
+ # Overview label if looking on single day
+ overview_label = _(u"Overview for %(date)s") % \
+ ({"date": start_date_str})
+ else:
+ dates_dict = stuff.dateDict(self.start_date, "start_")
+ dates_dict.update(stuff.dateDict(self.end_date, "end_"))
+
+ if self.start_date.year != self.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
+ overview_label = _(u"Overview for %(start_B)s %(start_d)s, %(start_Y)s â?? %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
+ elif self.start_date.month != self.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
+ overview_label = _(u"Overview for %(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
+ overview_label = _(u"Overview for %(start_B)s %(start_d)s â?? %(end_d)s, %(end_Y)s") % dates_dict
+
+ if self.week_view.get_active():
+ dayview_caption = _("Week")
+ elif self.month_view.get_active():
+ dayview_caption = _("Month")
+ else:
+ dayview_caption = _("Day")
+
+ self.get_widget("overview_label").set_markup("<b>%s</b>" % overview_label)
+ self.get_widget("dayview_caption").set_markup("%s" % (dayview_caption))
+
+
+ def do_graph(self):
+ self.set_title()
+
+ if self.day_view.get_active():
+ facts = runtime.storage.get_facts(self.view_date)
+ else:
+ facts = runtime.storage.get_facts(self.start_date, self.end_date)
+
+
+ self.get_widget("report_button").set_sensitive(len(facts) > 0)
+ self.fact_store.clear()
+
+ self.fill_tree(facts)
+
+ if not facts:
+ self.get_widget("graphs").hide()
+ self.get_widget("no_data_label").show()
+ return
+
+
+ self.get_widget("no_data_label").hide()
+ self.get_widget("graphs").show()
+ self.do_charts(facts)
+
+
+
+
+
+ def get_widget(self, name):
+ """ skip one variable (huh) """
+ return self._gui.get_object(name)
+
+
+
+
+ def after_activity_update(self, widget, renames):
+ self.do_graph()
+
+ def after_fact_update(self, event, date):
+ self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
+ self.popular_categories = [cat[0] for cat in runtime.storage.get_popular_categories()]
+
+ if self.get_widget("pages").get_current_page() == 0:
+ self.do_graph()
+ else:
+ self.stats()
+
+ def on_fact_selection_changed(self, selection, model):
+ """ enables and disables action buttons depending on selected item """
+ (model, iter) = selection.get_selected()
+
+ id = -1
+ if iter:
+ id = model[iter][0]
+
+ self.get_widget('remove').set_sensitive(id != -1)
+ self.get_widget('edit').set_sensitive(id != -1)
+
+ return True
+
+ def on_facts_row_activated(self, tree, path, column):
+ selection = tree.get_selection()
+ (model, iter) = selection.get_selected()
+ custom_fact = CustomFactController(self, None, model[iter][0])
+ custom_fact.show()
+
+ def on_add_clicked(self, button):
+ selection = self.fact_tree.get_selection()
+ (model, iter) = selection.get_selected()
+
+ selected_date = self.view_date
+ if iter:
+ selected_date = model[iter][3].split("-")
+ selected_date = dt.date(int(selected_date[0]),
+ int(selected_date[1]),
+ int(selected_date[2]))
+
+ custom_fact = CustomFactController(self, selected_date)
+ custom_fact.show()
+
+ def on_prev_clicked(self, button):
+ if self.day_view.get_active():
+ self.view_date -= dt.timedelta(1)
+ if self.view_date < self.start_date:
+ self.start_date -= dt.timedelta(7)
+ self.end_date -= dt.timedelta(7)
+ else:
+ if self.week_view.get_active():
+ self.start_date -= dt.timedelta(7)
+ self.end_date -= dt.timedelta(7)
+
+ elif self.month_view.get_active():
+ 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.do_graph()
+
+ def on_next_clicked(self, button):
+ if self.day_view.get_active():
+ self.view_date += dt.timedelta(1)
+ if self.view_date > self.end_date:
+ self.start_date += dt.timedelta(7)
+ self.end_date += dt.timedelta(7)
+ else:
+ if self.week_view.get_active():
+ self.start_date += dt.timedelta(7)
+ self.end_date += dt.timedelta(7)
+ elif self.month_view.get_active():
+ 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.do_graph()
+
+ def on_home_clicked(self, button):
+ self.view_date = dt.date.today()
+ if self.week_view.get_active():
+ 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.month_view.get_active():
+ 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.do_graph()
+
+ def on_day_toggled(self, button):
+ 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("prev").set_tooltip_text(_("Previous day"))
+ self.get_widget("next").set_tooltip_text(_("Next day"))
+ self.get_widget("home").set_tooltip_text(_("Today"))
+ self.get_widget("home").set_label(_("Today"))
+
+ self.do_graph()
+
+ def on_week_toggled(self, button):
+ 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("prev").set_tooltip_text(_("Previous week"))
+ self.get_widget("next").set_tooltip_text(_("Next week"))
+ self.get_widget("home").set_tooltip_text(_("This week"))
+ self.get_widget("home").set_label(_("This Week"))
+ self.do_graph()
+
+
+ def on_month_toggled(self, button):
+ 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("prev").set_tooltip_text(_("Previous month"))
+ self.get_widget("next").set_tooltip_text(_("Next month"))
+ self.get_widget("home").set_tooltip_text(_("This month"))
+ self.get_widget("home").set_label(_("This Month"))
+ self.do_graph()
+
+
+ def init_report_dialog(self):
+ chooser = self.get_widget('save_report_dialog')
+ chooser.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
+ """
+ chooser.set
+
+ chooser = gtk.FileChooserDialog(title = _("Save report - Time Tracker"),
+ parent = None,
+ buttons=(gtk.STOCK_CANCEL,
+ gtk.RESPONSE_CANCEL,
+ gtk.STOCK_SAVE,
+ gtk.RESPONSE_OK))
+ """
+ chooser.set_current_folder(os.path.expanduser("~"))
+
+ filters = {}
+
+ filter = gtk.FileFilter()
+ filter.set_name(_("HTML Report"))
+ filter.add_mime_type("text/html")
+ filter.add_pattern("*.html")
+ filter.add_pattern("*.htm")
+ filters[filter] = "html"
+ chooser.add_filter(filter)
+
+ filter = gtk.FileFilter()
+ filter.set_name(_("Tab-Separated Values (TSV)"))
+ filter.add_mime_type("text/plain")
+ filter.add_pattern("*.tsv")
+ filter.add_pattern("*.txt")
+ filters[filter] = "tsv"
+ chooser.add_filter(filter)
+
+ filter = gtk.FileFilter()
+ filter.set_name(_("XML"))
+ filter.add_mime_type("text/xml")
+ filter.add_pattern("*.xml")
+ filters[filter] = "xml"
+ chooser.add_filter(filter)
+
+ filter = gtk.FileFilter()
+ filter.set_name(_("iCal"))
+ filter.add_mime_type("text/calendar")
+ filter.add_pattern("*.ics")
+ filters[filter] = "ical"
+ chooser.add_filter(filter)
+
+ filter = gtk.FileFilter()
+ filter.set_name("All files")
+ filter.add_pattern("*")
+ chooser.add_filter(filter)
+
+ def on_report_chosen(self, widget, format, path, start_date, end_date,
+ categories):
+ self.report_chooser = None
+
+ facts = runtime.storage.get_facts(start_date, end_date, category_id = categories)
+ reports.simple(facts,
+ start_date,
+ 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(self, widget):
+ self.report_chooser = None
+
+ def on_report_button_clicked(self, widget):
+ if not self.report_chooser:
+ self.report_chooser = widgets.ReportChooserDialog()
+ self.report_chooser.connect("report-chosen", self.on_report_chosen)
+ self.report_chooser.connect("report-chooser-closed",
+ self.on_report_chooser_closed)
+ self.report_chooser.show(self.start_date, self.end_date)
+ else:
+ self.report_chooser.present()
+
+
+ def on_day_start_changed(self, event, new_minutes):
+ self.do_graph()
+
diff --git a/hamster/stats_stats.py b/hamster/stats_stats.py
new file mode 100644
index 0000000..10988c1
--- /dev/null
+++ b/hamster/stats_stats.py
@@ -0,0 +1,416 @@
+# - 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 gtk, gobject
+import pango
+
+import stuff
+import charting
+
+from edit_activity import CustomFactController
+import reports, graphics
+
+import widgets
+
+from configuration import runtime, GconfStore
+import webbrowser
+
+from itertools import groupby
+from gettext import ngettext
+
+import datetime as dt
+import calendar
+import time
+from hamster.i18n import C_
+
+
+
+class StatsBox(gtk.VBox):
+ def __init__(self):
+ gtk.VBox.__init__(self)
+
+ self._gui = stuff.load_ui_file("stats_stats.ui")
+ self.get_widget("stats_box").reparent(self) #mine!
+
+ self.background = (0.975, 0.975, 0.975)
+ self.get_widget("explore_frame").modify_bg(gtk.STATE_NORMAL,
+ gtk.gdk.Color(*[int(b*65536.0) for b in self.background]))
+
+ self.stat_facts = None
+
+ self.timeline = widgets.TimeLine()
+ self.get_widget("explore_everything").add(self.timeline)
+ self.get_widget("explore_everything").show_all()
+
+
+ self.init_stats()
+
+
+
+ def init_stats(self):
+ self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
+
+ 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,
+ background = self.background,
+ 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,
+ background = self.background,
+ 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,
+ background = self.background,
+ 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,
+ background = self.background,
+ 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, background = None, fontsize = 10):
+ graphics.Area.__init__(self)
+ self.background = background
+ self.text = ""
+ self.fontsize = fontsize
+
+ def set_text(self, text):
+ self.text = text
+ self.redraw_canvas()
+
+ def on_expose(self):
+ if self.background:
+ self.fill_area(0, 0, self.width, self.height, self.background)
+
+ 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(charting.graphics.Colors.aluminium[5])
+
+ self.context.show_layout(self.layout)
+
+ self.explore_summary = CairoText(self.background)
+ 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(_("Still 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
+ self.timeline.draw(facts)
+
+
+ # 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:
+ 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]))
+
+ 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:
+ 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])
+
+ 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>%.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}
+
+ # 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 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))
+
+ 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 get_widget(self, name):
+ """ skip one variable (huh) """
+ return self._gui.get_object(name)
+
+
+
+
diff --git a/hamster/widgets/facttree.py b/hamster/widgets/facttree.py
new file mode 100644
index 0000000..b44c7c5
--- /dev/null
+++ b/hamster/widgets/facttree.py
@@ -0,0 +1,189 @@
+# - 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 gtk, gobject
+import datetime as dt
+
+from hamster.configuration import runtime
+
+from hamster import stuff
+from hamster.stuff import format_duration, format_activity
+from tags import TagCellRenderer
+
+import pango
+
+class ActivityColumn(gtk.TreeViewColumn):
+ def __init__(self, name, description, category = None):
+ gtk.TreeViewColumn.__init__(self)
+
+ self.name, self.description, self.category = name, description, category
+ self.set_expand(True)
+ cell = gtk.CellRendererText()
+ self.pack_start(cell, True)
+ cell.set_property("ellipsize", pango.ELLIPSIZE_END)
+ self.set_cell_data_func(cell, self.activity_painter)
+
+ def activity_painter(self, column, cell, model, iter):
+ activity_name = model.get_value(iter, self.name)
+ description = model.get_value(iter, self.description)
+ category = model.get_value(iter, self.category)
+
+ markup = format_activity(activity_name, category, description)
+ cell.set_property('markup', markup)
+ return
+
+def parent_painter(column, cell, model, iter):
+ cell_text = model.get_value(iter, 1)
+
+ if model.get_value(iter, 6) is None:
+ if model.get_path(iter) == (0,):
+ text = '<span weight="heavy">%s</span>' % cell_text
+ else:
+ text = '<span weight="heavy" rise="-20000">%s</span>' % cell_text
+
+ cell.set_property('markup', text)
+
+ else:
+ activity_name = stuff.escape_pango(cell_text)
+ description = stuff.escape_pango(model.get_value(iter, 4))
+ category = stuff.escape_pango(model.get_value(iter, 5))
+
+ markup = stuff.format_activity(activity_name,
+ category,
+ description,
+ pad_description = True)
+ cell.set_property('markup', markup)
+
+def duration_painter(column, cell, model, iter):
+ cell.set_property('xalign', 1)
+
+
+ text = model.get_value(iter, 2)
+ if model.get_value(iter, 6) is None:
+ if model.get_path(iter) == (0,):
+ text = '<span weight="heavy">%s</span>' % text
+ else:
+ text = '<span weight="heavy" rise="-20000">%s</span>' % text
+ cell.set_property('markup', text)
+
+def action_painter(column, cell, model, iter):
+ cell.set_property('xalign', 1)
+
+
+ text = model.get_value(iter, 2)
+ if model.get_value(iter, 6) is None:
+ cell.set_property("stock_id", "")
+ else:
+ cell.set_property("stock_id", "gtk-edit")
+
+
+class FactTree(gtk.TreeView):
+ __gsignals__ = {
+ 'value-entered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ }
+
+
+ def __init__(self):
+ gtk.TreeView.__init__(self)
+
+ self.set_headers_visible(False)
+ self.set_show_expanders(False)
+
+ #id, caption, duration, date (invisible), description, category
+ self.set_model(gtk.TreeStore(int, str, str, str, str, str, gobject.TYPE_PYOBJECT))
+
+
+ # name
+ nameColumn = gtk.TreeViewColumn()
+ nameColumn.set_expand(True)
+ nameCell = gtk.CellRendererText()
+ nameCell.set_property("ellipsize", pango.ELLIPSIZE_END)
+ nameColumn.pack_start(nameCell, True)
+ nameColumn.set_cell_data_func(nameCell, parent_painter)
+ self.append_column(nameColumn)
+
+ tag_cell = TagCellRenderer()
+ tag_cell.font_size = 8;
+ self.append_column(gtk.TreeViewColumn("", tag_cell, data=6))
+
+
+ # duration
+ timeColumn = gtk.TreeViewColumn()
+ timeCell = gtk.CellRendererText()
+ timeColumn.pack_end(timeCell, True)
+ timeColumn.set_cell_data_func(timeCell, duration_painter)
+ self.append_column(timeColumn)
+
+ edit_cell = gtk.CellRendererPixbuf()
+ edit_cell.set_property("mode", gtk.CELL_RENDERER_MODE_ACTIVATABLE)
+ self.edit_column = gtk.TreeViewColumn("", edit_cell)
+ self.edit_column.set_cell_data_func(edit_cell, action_painter)
+ self.append_column(self.edit_column)
+
+
+ runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
+ runtime.dispatcher.add_handler('fact_updated', self.after_activity_update)
+
+ self.show()
+
+ def clear(self):
+ self.model.clear()
+
+ @property
+ def model(self):
+ return self.get_model()
+
+ def add_fact(self, fact, parent = None):
+ duration = 24 * 60 * fact["delta"].days + fact["delta"].seconds / 60
+
+ if fact["end_time"]:
+ fact_time = "%s - %s " % (fact["start_time"].strftime("%H:%M"),
+ fact["end_time"].strftime("%H:%M"))
+ else:
+ fact_time = fact["start_time"].strftime("%H:%M ")
+
+ self.model.append(parent, [fact["id"],
+ "%s %s" % (fact_time, fact["name"]),
+ stuff.format_duration(fact["delta"]),
+ fact["start_time"].strftime('%Y-%m-%d'),
+ fact["description"],
+ fact["category"],
+ fact])
+
+ def add_group(self, group_label, facts):
+ total = sum([fact["delta"].seconds for fact in facts]) / 60.0
+
+ # adds group of facts with the given label
+ group_row = self.model.append(None,
+ [-1,
+ group_label,
+ stuff.format_duration(total),
+ "",
+ "",
+ "",
+ None])
+
+ for fact in facts:
+ self.add_fact(fact, group_row)
+
+ self.expand_all()
+
+
+ def after_activity_update(self, event, whatnot):
+ print "catch me!"
\ No newline at end of file
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d5c4d87..0ef5579 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -7,6 +7,9 @@ data/Hamster_Applet.xml
data/hamster-applet.schemas.in
[type: gettext/glade]data/applet.ui
[type: gettext/glade]data/stats.ui
+[type: gettext/glade]data/stats_overview.ui
+[type: gettext/glade]data/stats_reports.ui
+[type: gettext/glade]data/stats_stats.ui
hamster/about.py
hamster/applet.py
hamster/db.py
@@ -14,5 +17,8 @@ hamster/edit_activity.py
hamster/preferences.py
hamster/reports.py
hamster/stats.py
+hamster/stats_overview.py
+hamster/stats_reports.py
+hamster/stats_stats.py
hamster/stuff.py
hamster/widgets/reportchooserdialog.py
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]