~ubuntu-branches/ubuntu/vivid/hamster-applet/vivid

« back to all changes in this revision

Viewing changes to hamster/stats.py

  • Committer: Bazaar Package Importer
  • Author(s): Emilio Pozuelo Monfort
  • Date: 2009-10-22 22:01:54 UTC
  • mfrom: (1.2.4 upstream) (5.2.2 sid)
  • mto: This revision was merged to the branch mainline in revision 18.
  • Revision ID: james.westby@ubuntu.com-20091022220154-do4zoetlf35l36pe
Tags: 2.28.1-1
New upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# - coding: utf-8 -
2
2
 
3
 
# Copyright (C) 2008 Toms Bauģis <toms.baugis at gmail.com>
 
3
# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
4
4
 
5
5
# This file is part of Project Hamster.
6
6
 
22
22
pygtk.require('2.0')
23
23
 
24
24
import os
25
 
import gtk
 
25
import gtk, gobject
26
26
import pango
27
27
 
28
 
from hamster import dispatcher, storage, SHARED_DATA_DIR, stuff
29
 
from hamster.charting import Chart
30
 
from hamster.add_custom_fact import CustomFactController
 
28
import stuff
 
29
import charting
 
30
 
 
31
from edit_activity import CustomFactController
 
32
import reports, widgets, graphics
 
33
from configuration import runtime
 
34
import webbrowser
 
35
 
 
36
from itertools import groupby
 
37
from gettext import ngettext
31
38
 
32
39
import datetime as dt
33
40
import calendar
34
 
import gobject
35
41
import time
36
 
 
37
 
class StatsViewer:
38
 
    def __init__(self):
39
 
        self.glade = gtk.glade.XML(os.path.join(SHARED_DATA_DIR, "stats.glade"))
 
42
from hamster.i18n import C_
 
43
 
 
44
class ReportChooserDialog(gtk.Dialog):
 
45
    __gsignals__ = {
 
46
        # format, path, start_date, end_date
 
47
        'report-chosen': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
 
48
                          (gobject.TYPE_STRING, gobject.TYPE_STRING,
 
49
                           gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
 
50
                           gobject.TYPE_PYOBJECT)),
 
51
        'report-chooser-closed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
 
52
    }
 
53
    def __init__(self):
 
54
        gtk.Dialog.__init__(self)
 
55
        ui = stuff.load_ui_file("stats.ui")
 
56
        self.dialog = ui.get_object('save_report_dialog')
 
57
 
 
58
        self.dialog.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
 
59
        self.dialog.set_current_folder(os.path.expanduser("~"))
 
60
 
 
61
        self.filters = {}
 
62
 
 
63
        filter = gtk.FileFilter()
 
64
        filter.set_name(_("HTML Report"))
 
65
        filter.add_mime_type("text/html")
 
66
        filter.add_pattern("*.html")
 
67
        filter.add_pattern("*.htm")
 
68
        self.filters[filter] = "html"
 
69
        self.dialog.add_filter(filter)
 
70
 
 
71
        filter = gtk.FileFilter()
 
72
        filter.set_name(_("Tab-Separated Values (TSV)"))
 
73
        filter.add_mime_type("text/plain")
 
74
        filter.add_pattern("*.tsv")
 
75
        filter.add_pattern("*.txt")
 
76
        self.filters[filter] = "tsv"
 
77
        self.dialog.add_filter(filter)
 
78
 
 
79
        filter = gtk.FileFilter()
 
80
        filter.set_name(_("XML"))
 
81
        filter.add_mime_type("text/xml")
 
82
        filter.add_pattern("*.xml")
 
83
        self.filters[filter] = "xml"
 
84
        self.dialog.add_filter(filter)
 
85
 
 
86
        filter = gtk.FileFilter()
 
87
        filter.set_name(_("iCal"))
 
88
        filter.add_mime_type("text/calendar")
 
89
        filter.add_pattern("*.ics")
 
90
        self.filters[filter] = "ical"
 
91
        self.dialog.add_filter(filter)
 
92
 
 
93
        filter = gtk.FileFilter()
 
94
        filter.set_name("All files")
 
95
        filter.add_pattern("*")
 
96
        self.dialog.add_filter(filter)
 
97
        
 
98
        self.start_date = widgets.DateInput()
 
99
        ui.get_object('from_date_box').add(self.start_date)
 
100
        self.end_date = widgets.DateInput()
 
101
        ui.get_object('to_date_box').add(self.end_date)
 
102
 
 
103
        self.category_box = ui.get_object('category_box')
 
104
 
 
105
        ui.get_object('save_button').connect("clicked", self.on_save_button_clicked)
 
106
        ui.get_object('cancel_button').connect("clicked", self.on_cancel_button_clicked)
 
107
        
 
108
 
 
109
    def show(self, start_date, end_date):
 
110
        #set suggested name to something readable, replace backslashes with dots
 
111
        #so the name is valid in linux
 
112
        filename = "Time track %s - %s" % (start_date.strftime("%x").replace("/", "."),
 
113
                                           end_date.strftime("%x").replace("/", "."))
 
114
        self.dialog.set_current_name(filename)
 
115
        
 
116
        self.start_date.set_date(start_date)
 
117
        self.end_date.set_date(end_date)
 
118
        
 
119
        #add unsorted category
 
120
        button_all = gtk.CheckButton(C_("categories", "All").encode("utf-8"))
 
121
        button_all.value = None
 
122
        button_all.set_active(True)
 
123
        
 
124
        def on_category_all_clicked(checkbox):
 
125
            active = checkbox.get_active()
 
126
            for checkbox in self.category_box.get_children():
 
127
                checkbox.set_active(active)
 
128
        
 
129
        button_all.connect("clicked", on_category_all_clicked)
 
130
        self.category_box.attach(button_all, 0, 1, 0, 1)
 
131
 
 
132
        categories = runtime.storage.get_category_list()
 
133
        col, row = 0, 0
 
134
        for category in categories:
 
135
            col +=1
 
136
            if col % 4 == 0:
 
137
                col = 0
 
138
                row +=1
 
139
 
 
140
            button = gtk.CheckButton(category['name'].encode("utf-8"))
 
141
            button.value = category['id']
 
142
            button.set_active(True)
 
143
            self.category_box.attach(button, col, col+1, row, row+1)
 
144
 
 
145
        
 
146
 
 
147
        response = self.dialog.show_all()
 
148
 
 
149
    def present(self):
 
150
        self.dialog.present()
 
151
 
 
152
    def on_save_button_clicked(self, widget):
 
153
        path, format = None,  None
 
154
 
 
155
        format = "html"
 
156
        if self.dialog.get_filter() in self.filters:
 
157
            format = self.filters[self.dialog.get_filter()]
 
158
        path = self.dialog.get_filename()
 
159
        
 
160
        categories = []
 
161
        for button in self.category_box.get_children():
 
162
            if button.get_active() and button.value:
 
163
                categories.append(button.value)
 
164
        
 
165
        # format, path, start_date, end_date
 
166
        self.emit("report-chosen", format, path,
 
167
                           self.start_date.get_date().date(),
 
168
                           self.end_date.get_date().date(),
 
169
                           categories)
 
170
        self.dialog.destroy()
 
171
        
 
172
 
 
173
    def on_cancel_button_clicked(self, widget):
 
174
        self.emit("report-chooser-closed")
 
175
        self.dialog.destroy()
 
176
 
 
177
class TimeLine(graphics.Area):
 
178
    MODE_YEAR = 0
 
179
    MODE_MONTH = 1
 
180
    MODE_WEEK = 1
 
181
    MODE_DAY = 3
 
182
    def __init__(self):
 
183
        graphics.Area.__init__(self)
 
184
        self.start_date, self.end_date = None, None
 
185
        self.draw_mode = None
 
186
        self.max_hours = None
 
187
        
 
188
    
 
189
    def draw(self, facts):
 
190
        import itertools
 
191
        self.facts = {}
 
192
        for date, date_facts in itertools.groupby(facts, lambda x: x["start_time"].date()):
 
193
            date_facts = list(date_facts)
 
194
            self.facts[date] = date_facts
 
195
            self.max_hours = max(self.max_hours,
 
196
                                 sum([fact["delta"].seconds / 60 / float(60) +
 
197
                               fact["delta"].days * 24 for fact in date_facts]))
 
198
        
 
199
        start_date = facts[0]["start_time"].date()
 
200
        end_date = facts[-1]["start_time"].date()
 
201
 
 
202
        self.draw_mode = self.MODE_YEAR
 
203
        self.start_date = start_date.replace(month=1, day=1)
 
204
        self.end_date = end_date.replace(month=12, day=31)
 
205
        
 
206
 
 
207
        """
 
208
        #TODO - for now we have only the year mode        
 
209
        if start_date.year != end_date.year or start_date.month != end_date.month:
 
210
            self.draw_mode = self.MODE_YEAR
 
211
            self.start_date = start_date.replace(month=1, day=1)
 
212
            self.end_date = end_date.replace(month=12, day=31)
 
213
        elif start_date.strftime("%W") != end_date.strftime("%W"):
 
214
            self.draw_mode = self.MODE_MONTH
 
215
            self.start_date = start_date.replace(day=1)
 
216
            self.end_date = end_date.replace(date =
 
217
                                    calendar.monthrange(self.end_date.year,
 
218
                                                        self.end_date.month)[1])
 
219
        elif start_date != end_date:
 
220
            self.draw_mode = self.MODE_WEEK
 
221
        else:
 
222
            self.draw_mode = self.MODE_DAY
 
223
        """
 
224
        
 
225
        self.redraw_canvas()
 
226
        
 
227
        
 
228
    def _render(self):
 
229
        import calendar
 
230
        
 
231
        if self.draw_mode != self.MODE_YEAR:
 
232
            return
 
233
 
 
234
        self.fill_area(0, 0, self.width, self.height, (0.975,0.975,0.975))
 
235
        charting.set_color(self.context, (100,100,100))
 
236
 
 
237
        self.set_value_range(x_min = 1, x_max = (self.end_date - self.start_date).days)        
 
238
        month_label_fits = True
 
239
        for month in range(1, 13):
 
240
            self.layout.set_text(calendar.month_abbr[month])
 
241
            label_w, label_h = self.layout.get_pixel_size()
 
242
            if label_w * 2 > self.x_factor * 30:
 
243
                month_label_fits = False
 
244
                break
 
245
        
 
246
        
 
247
        ticker_date = self.start_date
 
248
        
 
249
        year_pos = 0
 
250
        
 
251
        for year in range(self.start_date.year, self.end_date.year + 1):
 
252
            #due to how things lay over, we are putting labels on backwards, so that they don't overlap
 
253
            
 
254
            self.context.set_line_width(1)
 
255
            for month in range(1, 13):
 
256
                for day in range(1, calendar.monthrange(year, month)[1] + 1):
 
257
                    ticker_pos = year_pos + ticker_date.timetuple().tm_yday
 
258
                    
 
259
                    #if ticker_date.weekday() in [0, 6]:
 
260
                    #    self.fill_area(ticker_pos * self.x_factor + 1, 20, self.x_factor, self.height - 20, (240, 240, 240))
 
261
                    #    self.context.stroke()
 
262
                        
 
263
    
 
264
                    if self.x_factor > 5:
 
265
                        self.move_to(ticker_pos, self.height - 20)
 
266
                        self.line_to(ticker_pos, self.height)
 
267
                   
 
268
                        self.layout.set_text(ticker_date.strftime("%d"))
 
269
                        label_w, label_h = self.layout.get_pixel_size()
 
270
                        
 
271
                        if label_w < self.x_factor / 1.2: #if label fits
 
272
                            self.context.move_to(self.get_pixel(ticker_pos) + 2,
 
273
                                                 self.height - 20)
 
274
                            self.context.show_layout(self.layout)
 
275
                    
 
276
                        self.context.stroke()
 
277
                        
 
278
                    #now facts
 
279
                    facts_today = self.facts.get(ticker_date, [])
 
280
                    if facts_today:
 
281
                        total_length = dt.timedelta()
 
282
                        for fact in facts_today:
 
283
                            total_length += fact["delta"]
 
284
                        total_length = total_length.seconds / 60 / 60.0 + total_length.days * 24
 
285
                        total_length = total_length / float(self.max_hours) * self.height - 16
 
286
                        self.fill_area(ticker_pos * self.x_factor,
 
287
                                       self.height - total_length,
 
288
                                       self.x_factor, total_length,
 
289
                                       (190,190,190))
 
290
 
 
291
 
 
292
                        
 
293
 
 
294
                    ticker_date += dt.timedelta(1)
 
295
                
 
296
            
 
297
                
 
298
                if month_label_fits:
 
299
                    #roll back a little
 
300
                    month_pos = ticker_pos - calendar.monthrange(year, month)[1] + 1
 
301
 
 
302
                    self.move_to(month_pos, 0)
 
303
                    #self.line_to(month_pos, 20)
 
304
                    
 
305
                    self.layout.set_text(dt.date(year, month, 1).strftime("%b"))
 
306
    
 
307
                    self.move_to(month_pos, 0)
 
308
                    self.context.show_layout(self.layout)
 
309
 
 
310
 
 
311
    
 
312
            
 
313
    
 
314
            self.layout.set_text("%d" % year)
 
315
            label_w, label_h = self.layout.get_pixel_size()
 
316
                        
 
317
            self.move_to(year_pos + 2 / self.x_factor, month_label_fits * label_h * 1.2)
 
318
    
 
319
            self.context.show_layout(self.layout)
 
320
            
 
321
            self.context.stroke()
 
322
 
 
323
            year_pos = ticker_pos #save current state for next year
 
324
 
 
325
 
 
326
 
 
327
class StatsViewer(object):
 
328
    def __init__(self, parent = None):
 
329
        self.parent = parent# determine if app shut shut down on close
 
330
        self._gui = stuff.load_ui_file("stats.ui")
40
331
        self.window = self.get_widget('stats_window')
 
332
        self.stat_facts = None
 
333
 
 
334
        #id, caption, duration, date (invisible), description, category
 
335
        self.fact_store = gtk.TreeStore(int, str, str, str, str, str, gobject.TYPE_PYOBJECT) 
 
336
        self.setup_tree()
 
337
        
 
338
        
 
339
        #graphs
 
340
        self.background = (0.975,0.975,0.975)
 
341
        self.get_widget("graph_frame").modify_bg(gtk.STATE_NORMAL,
 
342
                      gtk.gdk.Color(*[int(b*65536.0) for b in self.background]))
 
343
        self.get_widget("explore_frame").modify_bg(gtk.STATE_NORMAL,
 
344
                      gtk.gdk.Color(*[int(b*65536.0) for b in self.background]))
 
345
 
 
346
 
 
347
        x_offset = 90 # let's nicely align all graphs
 
348
        
 
349
        self.category_chart = charting.BarChart(background = self.background,
 
350
                                             bar_base_color = (238,221,221),
 
351
                                             legend_width = x_offset,
 
352
                                             max_bar_width = 35,
 
353
                                             show_stack_labels = True
 
354
                                             )
 
355
        self.get_widget("totals_by_category").add(self.category_chart)
 
356
        
 
357
 
 
358
        self.day_chart = charting.BarChart(background = self.background,
 
359
                                           bar_base_color = (220, 220, 220),
 
360
                                           show_scale = True,
 
361
                                           max_bar_width = 35,
 
362
                                           grid_stride = 4,
 
363
                                           legend_width = 20)
 
364
        self.get_widget("totals_by_day").add(self.day_chart)
 
365
 
 
366
 
 
367
        self.activity_chart = charting.HorizontalBarChart(orient = "horizontal",
 
368
                                                   max_bar_width = 25,
 
369
                                                   values_on_bars = True,
 
370
                                                   stretch_grid = True,
 
371
                                                   legend_width = x_offset,
 
372
                                                   value_format = "%.1f",
 
373
                                                   background = self.background,
 
374
                                                   bars_beveled = False,
 
375
                                                   animate = False)
 
376
        self.get_widget("totals_by_activity").add(self.activity_chart);
 
377
 
 
378
        
 
379
        self.view_date = dt.date.today()
 
380
        
 
381
        #set to monday
 
382
        self.start_date = self.view_date - \
 
383
                                      dt.timedelta(self.view_date.weekday() + 1)
 
384
        # look if we need to start on sunday or monday
 
385
        self.start_date = self.start_date + \
 
386
                                      dt.timedelta(stuff.locale_first_weekday())
 
387
        
 
388
        self.end_date = self.start_date + dt.timedelta(6)
 
389
 
 
390
        
 
391
        self.week_view = self.get_widget("week")
 
392
        self.month_view = self.get_widget("month")
 
393
        self.month_view.set_group(self.week_view)
 
394
        self.day_view = self.get_widget("day")
 
395
        self.day_view.set_group(self.week_view)
 
396
        
 
397
        #initiate the form in the week view
 
398
        self.week_view.set_active(True)
 
399
 
 
400
 
 
401
        runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
 
402
        runtime.dispatcher.add_handler('day_updated', self.after_fact_update)
 
403
 
 
404
        selection = self.fact_tree.get_selection()
 
405
        selection.connect('changed', self.on_fact_selection_changed,
 
406
                          self.fact_store)
 
407
        self.popular_categories = [cat[0] for cat in runtime.storage.get_popular_categories()]
 
408
 
 
409
        self._gui.connect_signals(self)
 
410
        self.fact_tree.grab_focus()
 
411
 
 
412
        self.timeline = TimeLine()
 
413
        self.get_widget("explore_everything").add(self.timeline)
 
414
        self.get_widget("explore_everything").show_all()
 
415
 
 
416
 
 
417
        self.report_chooser = None
 
418
        self.do_graph()
 
419
        self.init_stats()
 
420
 
 
421
    def init_stats(self):
 
422
        self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
 
423
        
 
424
        by_year = stuff.totals(self.stat_facts,
 
425
                               lambda fact: fact["start_time"].year,
 
426
                               lambda fact: 1)
 
427
        
 
428
        year_box = self.get_widget("year_box")
 
429
        class YearButton(gtk.ToggleButton):
 
430
            def __init__(self, label, year, on_clicked):
 
431
                gtk.ToggleButton.__init__(self, label)
 
432
                self.year = year
 
433
                self.connect("clicked", on_clicked)
 
434
        
 
435
        all_button = YearButton(C_("years", "All").encode("utf-8"),
 
436
                                None,
 
437
                                self.on_year_changed)
 
438
        year_box.pack_start(all_button)
 
439
        self.bubbling = True # TODO figure out how to properly work with togglebuttons as radiobuttons
 
440
        all_button.set_active(True)
 
441
        self.bubbling = False # TODO figure out how to properly work with togglebuttons as radiobuttons
 
442
 
 
443
        years = sorted(by_year.keys())
 
444
        for year in years:
 
445
            year_box.pack_start(YearButton(str(year), year, self.on_year_changed))
 
446
 
 
447
        year_box.show_all()
 
448
 
 
449
        self.chart_category_totals = charting.HorizontalBarChart(value_format = "%.1f",
 
450
                                                            bars_beveled = False,
 
451
                                                            background = self.background,
 
452
                                                            max_bar_width = 20,
 
453
                                                            legend_width = 70)
 
454
        self.get_widget("explore_category_totals").add(self.chart_category_totals)
 
455
 
 
456
 
 
457
        self.chart_weekday_totals = charting.HorizontalBarChart(value_format = "%.1f",
 
458
                                                            bars_beveled = False,
 
459
                                                            background = self.background,
 
460
                                                            max_bar_width = 20,
 
461
                                                            legend_width = 70)
 
462
        self.get_widget("explore_weekday_totals").add(self.chart_weekday_totals)
 
463
 
 
464
        self.chart_weekday_starts_ends = charting.HorizontalDayChart(bars_beveled = False,
 
465
                                                                animate = False,
 
466
                                                                background = self.background,
 
467
                                                                max_bar_width = 20,
 
468
                                                                legend_width = 70)
 
469
        self.get_widget("explore_weekday_starts_ends").add(self.chart_weekday_starts_ends)
 
470
        
 
471
        self.chart_category_starts_ends = charting.HorizontalDayChart(bars_beveled = False,
 
472
                                                                animate = False,
 
473
                                                                background = self.background,
 
474
                                                                max_bar_width = 20,
 
475
                                                                legend_width = 70)
 
476
        self.get_widget("explore_category_starts_ends").add(self.chart_category_starts_ends)
 
477
 
 
478
 
 
479
        #ah, just want summary look just like all the other text on the page
 
480
        class CairoText(graphics.Area):
 
481
            def __init__(self, background = None, fontsize = 10):
 
482
                graphics.Area.__init__(self)
 
483
                self.background = background
 
484
                self.text = ""
 
485
                self.fontsize = fontsize
 
486
                
 
487
            def set_text(self, text):
 
488
                self.text = text
 
489
                self.redraw_canvas()
 
490
                
 
491
            def _render(self):
 
492
                if self.background:
 
493
                    self.fill_area(0, 0, self.width, self.height, self.background)
 
494
 
 
495
                default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
 
496
                default_font.set_size(self.fontsize * pango.SCALE)
 
497
                self.layout.set_font_description(default_font)
 
498
                
 
499
                #self.context.set_source_rgb(0,0,0)
 
500
                self.layout.set_markup(self.text)
 
501
 
 
502
                self.layout.set_width((self.width) * pango.SCALE)
 
503
                self.context.move_to(0,0)
 
504
                charting.set_color(self.context, charting.dark[8])
 
505
                
 
506
                self.context.show_layout(self.layout)
 
507
 
 
508
        self.explore_summary = CairoText(self.background)
 
509
        self.get_widget("explore_summary").add(self.explore_summary)
 
510
        self.get_widget("explore_summary").show_all()
 
511
 
 
512
    def stats(self, year = None):
 
513
        facts = self.stat_facts
 
514
        if year:
 
515
            facts = filter(lambda fact: fact["start_time"].year == year,
 
516
                           facts)
 
517
 
 
518
        if not facts or (facts[-1]["start_time"] - facts[0]["start_time"]) < dt.timedelta(days=6):
 
519
            self.get_widget("statistics_box").hide()
 
520
            self.get_widget("explore_controls").hide()
 
521
            label = self.get_widget("not_enough_records_label")
 
522
 
 
523
            if not facts:
 
524
                label.set_text(_("""There is no data to generate statistics yet.
 
525
A week of usage would be nice!"""))
 
526
            else:
 
527
                label.set_text(_("Still collecting data — check back after a week has passed!"))
 
528
 
 
529
            label.show()
 
530
            return
 
531
        else:
 
532
            self.get_widget("statistics_box").show()
 
533
            self.get_widget("explore_controls").show()
 
534
            self.get_widget("not_enough_records_label").hide()
 
535
 
 
536
        # All dates in the scope
 
537
        self.timeline.draw(facts)
 
538
 
 
539
 
 
540
        # Totals by category
 
541
        categories = stuff.totals(facts,
 
542
                                  lambda fact: fact["category"],
 
543
                                  lambda fact: fact['delta'].seconds / 60 / 60.0)
 
544
        category_keys = sorted(categories.keys())
 
545
        categories = [categories[key] for key in category_keys]
 
546
        self.chart_category_totals.plot(category_keys, categories)
 
547
        
 
548
        # Totals by weekday
 
549
        weekdays = stuff.totals(facts,
 
550
                                lambda fact: (fact["start_time"].weekday(),
 
551
                                              fact["start_time"].strftime("%a")),
 
552
                                lambda fact: fact['delta'].seconds / 60 / 60.0)
 
553
        
 
554
        weekday_keys = sorted(weekdays.keys(), key = lambda x: x[0]) #sort 
 
555
        weekdays = [weekdays[key] for key in weekday_keys] #get values in the order
 
556
        weekday_keys = [key[1] for key in weekday_keys] #now remove the weekday and keep just the abbreviated one
 
557
        self.chart_weekday_totals.plot(weekday_keys, weekdays)
 
558
 
 
559
 
 
560
        split_minutes = 5 * 60 + 30 #the mystical hamster midnight
 
561
        
 
562
        # starts and ends by weekday
 
563
        by_weekday = {}
 
564
        for date, date_facts in groupby(facts, lambda fact: fact["start_time"].date()):
 
565
            date_facts = list(date_facts)
 
566
            weekday = (date_facts[0]["start_time"].weekday(),
 
567
                       date_facts[0]["start_time"].strftime("%a"))
 
568
            by_weekday.setdefault(weekday, [])
 
569
            
 
570
            start_times, end_times = [], []
 
571
            for fact in date_facts:
 
572
                start_time = fact["start_time"].time()
 
573
                start_time = start_time.hour * 60 + start_time.minute
 
574
                if fact["end_time"]:
 
575
                    end_time = fact["end_time"].time()
 
576
                    end_time = end_time.hour * 60 + end_time.minute
 
577
                
 
578
                    if start_time < split_minutes:
 
579
                        start_time += 24 * 60
 
580
                    if end_time < start_time:
 
581
                        end_time += 24 * 60
 
582
                    
 
583
                    start_times.append(start_time)
 
584
                    end_times.append(end_time)
 
585
            if start_times and end_times:            
 
586
                by_weekday[weekday].append((min(start_times), max(end_times)))
 
587
 
 
588
 
 
589
        for day in by_weekday:
 
590
            by_weekday[day] = (sum([fact[0] for fact in by_weekday[day]]) / len(by_weekday[day]),
 
591
                               sum([fact[1] for fact in by_weekday[day]]) / len(by_weekday[day]))
 
592
 
 
593
        min_weekday = min([by_weekday[day][0] for day in by_weekday])
 
594
        max_weekday = max([by_weekday[day][1] for day in by_weekday])
 
595
 
 
596
 
 
597
        weekday_keys = sorted(by_weekday.keys(), key = lambda x: x[0])
 
598
        weekdays = [by_weekday[key] for key in weekday_keys]
 
599
        weekday_keys = [key[1] for key in weekday_keys] # get rid of the weekday number as int
 
600
 
 
601
        
 
602
        # starts and ends by category
 
603
        by_category = {}
 
604
        for date, date_facts in groupby(facts, lambda fact: fact["start_time"].date()):
 
605
            date_facts = sorted(list(date_facts), key = lambda x: x["category"])
 
606
            
 
607
            for category, category_facts in groupby(date_facts, lambda x: x["category"]):
 
608
                category_facts = list(category_facts)
 
609
                by_category.setdefault(category, [])
 
610
                
 
611
                start_times, end_times = [], []
 
612
                for fact in category_facts:
 
613
                    start_time = fact["start_time"]
 
614
                    start_time = start_time.hour * 60 + start_time.minute
 
615
                    if fact["end_time"]:
 
616
                        end_time = fact["end_time"].time()
 
617
                        end_time = end_time.hour * 60 + end_time.minute
 
618
                        
 
619
                        if start_time < split_minutes:
 
620
                            start_time += 24 * 60
 
621
                        if end_time < start_time:
 
622
                            end_time += 24 * 60
 
623
 
 
624
                        start_times.append(start_time)
 
625
                        end_times.append(end_time)
 
626
 
 
627
                if start_times and end_times:            
 
628
                    by_category[category].append((min(start_times), max(end_times)))
 
629
 
 
630
        for cat in by_category:
 
631
            by_category[cat] = (sum([fact[0] for fact in by_category[cat]]) / len(by_category[cat]),
 
632
                                sum([fact[1] for fact in by_category[cat]]) / len(by_category[cat]))
 
633
 
 
634
        min_category = min([by_category[day][0] for day in by_category])
 
635
        max_category = max([by_category[day][1] for day in by_category])
 
636
 
 
637
        category_keys = sorted(by_category.keys(), key = lambda x: x[0])
 
638
        categories = [by_category[key] for key in category_keys]
 
639
 
 
640
 
 
641
        #get starting and ending hours for graph and turn them into exact hours that divide by 3
 
642
        min_hour = min([min_weekday, min_category]) / 60 * 60
 
643
        max_hour = max([max_weekday, max_category]) / 60 * 60
 
644
 
 
645
        self.chart_weekday_starts_ends.plot_day(weekday_keys, weekdays, min_hour, max_hour)
 
646
        self.chart_category_starts_ends.plot_day(category_keys, categories, min_hour, max_hour)
 
647
 
 
648
 
 
649
        #now the factoids!
 
650
        summary = ""
 
651
 
 
652
        # first record        
 
653
        if not year:
 
654
            # date format for the first record if the year has not been selected
 
655
            # Using python datetime formatting syntax. See:
 
656
            # http://docs.python.org/library/time.html#time.strftime
 
657
            first_date = facts[0]["start_time"].strftime(C_("first record", "%b %d, %Y"))
 
658
        else:
 
659
            # date of first record when year has been selected
 
660
            # Using python datetime formatting syntax. See:
 
661
            # http://docs.python.org/library/time.html#time.strftime
 
662
            first_date = facts[0]["start_time"].strftime(C_("first record", "%(b)s %(d)s"))
 
663
 
 
664
        summary += _("First activity was recorded on %s.") % \
 
665
                                                     ("<b>%s</b>" % first_date)
 
666
        
 
667
        # total time tracked
 
668
        total_delta = dt.timedelta(days=0)
 
669
        for fact in facts:
 
670
            total_delta += fact["delta"]
 
671
        
 
672
        if total_delta.days > 1:
 
673
            human_years_str = ngettext("%(num)s year",
 
674
                                       "%(num)s years",
 
675
                                       total_delta.days / 365) % {
 
676
                              'num': "<b>%.2f</b>" % (total_delta.days / 365.0)}
 
677
            working_years_str = ngettext("%(num)s year",
 
678
                                         "%(num)s years",
 
679
                                         total_delta.days * 3 / 365) % {
 
680
                         'num': "<b>%.2f</b>" % (total_delta.days * 3 / 365.0) }
 
681
            #FIXME: difficult string to properly pluralize
 
682
            summary += " " + _("""Time tracked so far is %(human_days)s human days \
 
683
(%(human_years)s) or %(working_days)s working days (%(working_years)s).""") % {
 
684
              "human_days": ("<b>%d</b>" % total_delta.days),
 
685
              "human_years": human_years_str,
 
686
              "working_days": ("<b>%d</b>" % (total_delta.days * 3)), # 8 should be pretty much an average working day
 
687
              "working_years": working_years_str }
 
688
        
 
689
 
 
690
        # longest fact
 
691
        max_fact = None
 
692
        for fact in facts:
 
693
            if not max_fact or fact["delta"] > max_fact["delta"]:
 
694
                max_fact = fact
 
695
 
 
696
        longest_date = max_fact["start_time"].strftime(
 
697
            # How the date of the longest activity should be displayed in statistics
 
698
            # Using python datetime formatting syntax. See:
 
699
            # http://docs.python.org/library/time.html#time.strftime
 
700
            C_("date of the longest activity", "%b %d, %Y"))
 
701
        
 
702
        num_hours = max_fact["delta"].seconds / 60 / 60.0 + max_fact["delta"].days * 24
 
703
        hours = "<b>%.1f</b>" % (num_hours)
 
704
        
 
705
        summary += "\n" + ngettext("Longest continuous work happened on \
 
706
%(date)s and was %(hours)s hour.",
 
707
                                  "Longest continuous work happened on \
 
708
%(date)s and was %(hours)s hours.",
 
709
                                  int(num_hours)) % {"date": longest_date,
 
710
                                                     "hours": hours}
 
711
 
 
712
        # total records (in selected scope)
 
713
        summary += " " + ngettext("There is %s record.",
 
714
                                  "There are %s records.",
 
715
                                  len(facts)) % ("<b>%d</b>" % len(facts))
 
716
 
 
717
 
 
718
        early_start, early_end = dt.time(5,0), dt.time(9,0)
 
719
        late_start, late_end = dt.time(20,0), dt.time(5,0)
 
720
        
 
721
        
 
722
        fact_count = len(facts)
 
723
        def percent(condition):
 
724
            matches = [fact for fact in facts if condition(fact)]
 
725
            return round(len(matches) / float(fact_count) * 100)
 
726
        
 
727
        
 
728
        early_percent = percent(lambda fact: early_start < fact["start_time"].time() < early_end)
 
729
        late_percent = percent(lambda fact: fact["start_time"].time() > late_start or fact["start_time"].time() < late_end)
 
730
        short_percent = percent(lambda fact: fact["delta"] <= dt.timedelta(seconds = 60 * 15))
 
731
 
 
732
        if fact_count < 100:
 
733
            summary += "\n\n" + _("Hamster would like to observe you some more!")
 
734
        elif early_percent >= 20:
 
735
            summary += "\n\n" + _("With %s percent of all facts starting before \
 
736
9am you seem to be an early bird." % ("<b>%d</b>" % early_percent))
 
737
        elif late_percent >= 20:
 
738
            summary += "\n\n" + _("With %s percent of all facts starting after \
 
739
11pm you seem to be a night owl." % ("<b>%d</b>" % late_percent))
 
740
        elif short_percent >= 20:
 
741
            summary += "\n\n" + _("With %s percent of all tasks being shorter \
 
742
than 15 minutes you seem to be a busy bee." % ("<b>%d</b>" % short_percent))
 
743
 
 
744
        self.explore_summary.set_text(summary)
 
745
 
 
746
    def setup_tree(self):
 
747
        def parent_painter(column, cell, model, iter):
 
748
            cell_text = model.get_value(iter, 1)
 
749
            if model.iter_parent(iter) is None:
 
750
                if model.get_path(iter) == (0,):
 
751
                    text = '<span weight="heavy">%s</span>' % cell_text
 
752
                else:
 
753
                    text = '<span weight="heavy" rise="-20000">%s</span>' % cell_text
 
754
                    
 
755
                cell.set_property('markup', text)
 
756
    
 
757
            else:
 
758
                activity_name = stuff.escape_pango(cell_text)
 
759
                description = stuff.escape_pango(model.get_value(iter, 4))
 
760
                category = stuff.escape_pango(model.get_value(iter, 5))
 
761
 
 
762
                markup = stuff.format_activity(activity_name,
 
763
                                               category,
 
764
                                               description,
 
765
                                               pad_description = True)            
 
766
                cell.set_property('markup', markup)
 
767
 
 
768
        def duration_painter(column, cell, model, iter):
 
769
            cell.set_property('xalign', 1)
 
770
            cell.set_property('yalign', 0)
 
771
    
 
772
 
 
773
            text = model.get_value(iter, 2)
 
774
            if model.iter_parent(iter) is None:
 
775
                if model.get_path(iter) == (0,):
 
776
                    text = '<span weight="heavy">%s</span>' % text
 
777
                else:
 
778
                    text = '<span weight="heavy" rise="-20000">%s</span>' % text
 
779
            cell.set_property('markup', text)
 
780
    
41
781
 
42
782
        self.fact_tree = self.get_widget("facts")
43
783
        self.fact_tree.set_headers_visible(False)
44
784
        self.fact_tree.set_tooltip_column(1)
45
785
        self.fact_tree.set_property("show-expanders", False)
46
786
 
47
 
        nameColumn = gtk.TreeViewColumn(_("Name"))
 
787
        # name
 
788
        nameColumn = gtk.TreeViewColumn()
48
789
        nameColumn.set_expand(True)
49
790
        nameCell = gtk.CellRendererText()
50
791
        nameCell.set_property("ellipsize", pango.ELLIPSIZE_END)
51
792
        nameColumn.pack_start(nameCell, True)
52
 
        nameColumn.set_cell_data_func(nameCell, self.parent_painter)
 
793
        nameColumn.set_cell_data_func(nameCell, parent_painter)
53
794
        self.fact_tree.append_column(nameColumn)
54
795
 
55
 
        timeColumn = gtk.TreeViewColumn(_("Duration"))
 
796
        # duration
 
797
        timeColumn = gtk.TreeViewColumn()
56
798
        timeCell = gtk.CellRendererText()
57
799
        timeColumn.pack_end(timeCell, True)
58
 
        timeColumn.set_cell_data_func(timeCell, self.duration_painter)
 
800
        timeColumn.set_cell_data_func(timeCell, duration_painter)
 
801
 
 
802
 
 
803
 
 
804
 
59
805
        self.fact_tree.append_column(timeColumn)
60
806
        
61
 
        self.fact_store = gtk.TreeStore(int, str, str, str, str) #id, caption, duration, date (invisible), description
62
807
        self.fact_tree.set_model(self.fact_store)
63
 
        
64
 
        x_offset = 80 # let's nicely align all graphs
65
 
        
66
 
        self.day_chart = Chart(max_bar_width = 40,
67
 
                               collapse_whitespace = True,
68
 
                               legend_width = x_offset)
69
 
        eventBox = gtk.EventBox()
70
 
        place = self.get_widget("totals_by_day")
71
 
        eventBox.add(self.day_chart);
72
 
        place.add(eventBox)
73
 
        
74
 
        self.category_chart = Chart(orient = "horizontal",
75
 
                                    max_bar_width = 30,
76
 
                                    animate=False,
77
 
                                    values_on_bars = True,
78
 
                                    stretch_grid = True,
79
 
                                    legend_width = x_offset)
80
 
        eventBox = gtk.EventBox()
81
 
        place = self.get_widget("totals_by_category")
82
 
        eventBox.add(self.category_chart);
83
 
        place.add(eventBox)
84
 
        
85
 
        self.activity_chart = Chart(orient = "horizontal",
86
 
                                    max_bar_width = 25,
87
 
                                    animate = False,
88
 
                                    values_on_bars = True,
89
 
                                    stretch_grid = True,
90
 
                                    legend_width = x_offset)
91
 
        eventBox = gtk.EventBox()
92
 
        place = self.get_widget("totals_by_activity")
93
 
        eventBox.add(self.activity_chart);
94
 
        place.add(eventBox)
95
 
        
96
 
        self.view_date = dt.date.today()
97
 
        
98
 
        self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1) #set to monday
99
 
        # look if we need to start on sunday or monday
100
 
        self.start_date = self.start_date + dt.timedelta(self.locale_first_weekday())
101
 
        
102
 
        self.end_date = self.start_date + dt.timedelta(6)
103
 
 
104
 
        
105
 
        self.day_view = self.get_widget("day")
106
 
        self.week_view = self.get_widget("week")
107
 
        self.month_view = self.get_widget("month")
108
 
 
109
 
        self.week_view.set_group(self.day_view)
110
 
        self.month_view.set_group(self.day_view)
111
 
        
112
 
        #initiate the form in the week view
113
 
        self.week_view.set_active(True)
114
 
 
115
 
 
116
 
        dispatcher.add_handler('activity_updated', self.after_activity_update)
117
 
        dispatcher.add_handler('day_updated', self.after_fact_update)
118
 
 
119
 
        selection = self.fact_tree.get_selection()
120
 
        selection.connect('changed', self.on_fact_selection_changed, self.fact_store)
121
 
 
122
 
        self.glade.signal_autoconnect(self)
123
 
        self.fact_tree.grab_focus()
124
 
        self.do_graph()
125
 
 
126
 
    def locale_first_weekday(self):
127
 
        """figure if week starts on monday or sunday"""
128
 
        import os
129
 
        first_weekday = 6 #by default settle on monday
130
 
 
131
 
        try:
132
 
            process = os.popen("locale first_weekday week-1stday")
133
 
            week_offset, week_start = process.read().split('\n')[:2]
134
 
            process.close()
135
 
            week_start = dt.date(*time.strptime(week_start, "%Y%m%d")[:3])
136
 
            week_offset = dt.timedelta(int(week_offset) - 1)
137
 
            beginning = week_start + week_offset
138
 
            first_weekday = int(beginning.strftime("%w"))
139
 
        except:
140
 
            print "WARNING - Failed to get first weekday from locale"
141
 
            pass
142
 
            
143
 
        return first_weekday
144
 
        
145
 
    def parent_painter(self, column, cell, model, iter):
146
 
        cell_text = model.get_value(iter, 1)
147
 
        if model.iter_parent(iter) == None:
148
 
            if model.get_path(iter) == (0,):
149
 
                text = '<span weight="heavy">%s</span>' % cell_text
150
 
            else:
151
 
                text = '<span weight="heavy" rise="-20000">%s</span>' % cell_text
152
 
                
153
 
            cell.set_property('markup', text)
154
 
 
155
 
        else:
156
 
            activity_name = stuff.escape_pango(cell_text)
157
 
            description = stuff.escape_pango(model.get_value(iter, 4))
158
 
    
159
 
            text = "   %s" % activity_name
160
 
            if description:
161
 
                text+= """\n             <span style="italic" size="small">%s</span>""" % (description)
162
 
                
163
 
            cell.set_property('markup', text)
164
 
 
165
 
    def duration_painter(self, column, cell, model, iter):
166
 
        text = model.get_value(iter, 2)
167
 
        if model.iter_parent(iter) == None:
168
 
            if model.get_path(iter) == (0,):
169
 
                text = '<span weight="heavy">%s</span>' % text
170
 
            else:
171
 
                text = '<span weight="heavy" rise="-20000">%s</span>' % text
172
 
        cell.set_property('markup', text)
173
 
 
174
 
    def get_facts(self):
175
 
        self.fact_store.clear()
176
 
        totals = {}
177
 
 
178
 
        by_activity = {}
179
 
        by_category = {}
180
 
        by_day = {}
181
 
 
182
 
        week = {"days": [], "totals": []}
183
 
 
184
 
        facts = storage.get_facts(self.start_date, self.end_date)
185
 
 
 
808
    
 
809
    def on_graph_frame_size_allocate(self, widget, new_size):
 
810
        w = min(new_size.width / 4, 200)
 
811
        
 
812
        self.activity_chart.legend_width = w
 
813
        self.category_chart.legend_width = w
 
814
        self.get_widget("totals_by_category").set_size_request(w + 40, -1)
 
815
    
 
816
    def fill_tree(self, facts):
 
817
        day_dict = {}
 
818
        for day, facts in groupby(facts, lambda fact: fact["date"]):
 
819
            day_dict[day] = sorted(list(facts),
 
820
                                   key=lambda fact: fact["start_time"])
 
821
        
186
822
        for i in range((self.end_date - self.start_date).days  + 1):
187
823
            current_date = self.start_date + dt.timedelta(i)
188
 
            # date format in overview window fact listing
189
 
            # prefix is "o_",letter after prefix is regular python format. you can use all of them
190
 
            fact_date = _("%(o_A)s, %(o_b)s %(o_d)s") %  stuff.dateDict(current_date, "o_")
191
 
 
192
 
            day_row = self.fact_store.append(None, [-1,
193
 
                                                    fact_date,
194
 
                                                    "",
195
 
                                                    current_date.strftime('%Y-%m-%d'),
196
 
                                                    ""])
197
 
            by_day[self.start_date + dt.timedelta(i)] = {"duration": 0, "row_pointer": day_row}
198
 
 
199
 
        for fact in facts:
200
 
            start_date = fact["start_time"].date()
201
 
 
202
 
            duration = None
203
 
            if fact["end_time"]: # not set if just started
204
 
                delta = fact["end_time"] - fact["start_time"]
205
 
                duration = 24 * delta.days + delta.seconds / 60
206
 
            elif fact["start_time"].date() == dt.date.today():
207
 
                delta = dt.datetime.now() - fact["start_time"]
208
 
                duration = 24 * delta.days + delta.seconds / 60
209
 
 
210
 
            self.fact_store.append(by_day[start_date]["row_pointer"],
211
 
                                   [fact["id"],
212
 
                                    fact["start_time"].strftime('%H:%M') + " " +
213
 
                                    fact["name"],
214
 
                                    stuff.format_duration(duration),
215
 
                                    fact["start_time"].strftime('%Y-%m-%d'),
216
 
                                    fact["description"]
217
 
                                    ])
218
 
 
219
 
            if fact["name"] not in by_activity: by_activity[fact["name"]] = 0
220
 
            if fact["category"] not in by_category: by_category[fact["category"]] = 0
221
 
 
222
 
            if duration:
223
 
                by_day[start_date]["duration"] += duration
224
 
                by_activity[fact["name"]] += duration
225
 
                by_category[fact["category"]] += duration
226
 
 
227
 
        days = 30
228
 
        if self.week_view.get_active():
229
 
            days = 7
230
 
 
231
 
 
232
 
        date_sort = lambda a, b: (b[4] < a[4]) - (a[4] < b[4])
233
 
        totals["by_day"] = []
234
 
 
235
 
        for day in by_day:
236
 
            self.fact_store.set_value(by_day[day]["row_pointer"], 2,
237
 
                stuff.format_duration(by_day[day]["duration"]))
238
 
            if (self.end_date - self.start_date).days < 20:
239
 
                strday = stuff.locale_to_utf8(day.strftime('%a'))
240
 
                totals["by_day"].append([strday, by_day[day]["duration"] / 60.0, None, None, day])
241
 
            else:
242
 
                # date format in month chart in overview window (click on "month" to see it)
243
 
                # prefix is "m_", letter after prefix is regular python format. you can use all of them
244
 
                strday = _("%(m_b)s %(m_d)s") %  stuff.dateDict(day, "m_")
245
 
 
246
 
                background = None
247
 
                if day.weekday() in [5, 6]:
248
 
                    background = 7
249
 
 
250
 
                totals["by_day"].append([strday, by_day[day]["duration"] / 60.0, None, background, day])
251
 
        totals["by_day"].sort(date_sort)
252
 
            
253
 
            
254
 
        duration_sort = lambda a, b: (a[1] < b[1]) - (b[1] < a[1])
255
 
        totals["by_activity"] = []
256
 
        for activity in by_activity:
257
 
            totals["by_activity"].append([activity, by_activity[activity] / 60.0])
258
 
        totals["by_activity"].sort(duration_sort)
259
 
        
260
 
        #now we will limit bars to 6 and sum everything else into others
261
 
        if len(totals["by_activity"]) > 12:
262
 
            other_total = 0.0
263
 
 
264
 
            for i in range(11, len(totals["by_activity"]) - 1):
265
 
                other_total += totals["by_activity"][i][1]
266
 
                
267
 
            totals["by_activity"] = totals["by_activity"][:11]
268
 
            totals["by_activity"].append([_("Other"), other_total, 1])
269
 
        totals["by_activity"].sort(duration_sort) #sort again, since maybe others summed is bigger
270
 
            
271
 
        totals["by_category"] = []
272
 
        for category in by_category:
273
 
            totals["by_category"].append([category, by_category[category] / 60.0])
274
 
        totals["by_category"].sort(duration_sort)
275
 
        
 
824
            
 
825
            # Date format for the label in overview window fact listing
 
826
            # Using python datetime formatting syntax. See:
 
827
            # http://docs.python.org/library/time.html#time.strftime
 
828
            fact_date = current_date.strftime(C_("overview list", "%A, %b %d"))
 
829
            
 
830
            day_total = dt.timedelta()
 
831
            for fact in day_dict.get(current_date, []):
 
832
                day_total += fact["delta"]
 
833
 
 
834
            day_row = self.fact_store.append(None,
 
835
                                             [-1,
 
836
                                              fact_date,
 
837
                                              stuff.format_duration(day_total),
 
838
                                              current_date.strftime('%Y-%m-%d'),
 
839
                                              "",
 
840
                                              "",
 
841
                                              None])
 
842
 
 
843
            for fact in day_dict.get(current_date, []):
 
844
                self.fact_store.append(day_row,
 
845
                                       [fact["id"],
 
846
                                        fact["start_time"].strftime('%H:%M') + " " +
 
847
                                        fact["name"],
 
848
                                        stuff.format_duration(fact["delta"]),
 
849
                                        fact["start_time"].strftime('%Y-%m-%d'),
 
850
                                        fact["description"],
 
851
                                        fact["category"],
 
852
                                        fact
 
853
                                        ])
276
854
 
277
855
        self.fact_tree.expand_all()
278
 
        
279
 
        self.get_widget("report_button").set_sensitive(len(facts) > 0)
280
 
 
281
 
 
282
 
        week["totals"] = totals
 
856
 
 
857
        
 
858
    def do_charts(self, facts):
 
859
        all_categories = self.popular_categories
 
860
        
 
861
        
 
862
        #the single "totals" (by category) bar
 
863
        category_sums = stuff.totals(facts, lambda fact: fact["category"],
 
864
                      lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0)
 
865
        category_totals = [category_sums.get(cat, 0)
 
866
                                                      for cat in all_categories]
 
867
        category_keys = ["%s %.1f" % (cat, category_sums.get(cat, 0.0))
 
868
                                                      for cat in all_categories]
 
869
        self.category_chart.plot([_("Total")],
 
870
                                 [category_totals],
 
871
                                 stack_keys = category_keys)
 
872
        
 
873
        # day / category chart
 
874
        all_days = [self.start_date + dt.timedelta(i)
 
875
                    for i in range((self.end_date - self.start_date).days  + 1)]        
 
876
 
 
877
        by_date_cat = stuff.totals(facts,
 
878
                                   lambda fact: (fact["date"],
 
879
                                                 fact["category"]),
 
880
                                   lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0)
 
881
        res = [[by_date_cat.get((day, cat), 0)
 
882
                                 for cat in all_categories] for day in all_days]
 
883
 
 
884
        #show days or dates depending on scale
 
885
        if (self.end_date - self.start_date).days < 20:
 
886
            day_keys = [day.strftime("%a") for day in all_days]
 
887
        else:
 
888
            # date format used in the overview graph when month view is selected
 
889
            # Using python datetime formatting syntax. See:
 
890
            # http://docs.python.org/library/time.html#time.strftime
 
891
            day_keys = [day.strftime(C_("overview graph", "%b %d"))
 
892
                                                            for day in all_days]
 
893
 
 
894
        self.day_chart.plot(day_keys, res, stack_keys = all_categories)
 
895
 
 
896
 
 
897
        #totals by activity, disguised under a stacked bar chart to get category colors
 
898
        activity_sums = stuff.totals(facts,
 
899
                                     lambda fact: (fact["name"],
 
900
                                                   fact["category"]),
 
901
                                     lambda fact: stuff.duration_minutes(fact["delta"]))
 
902
        by_duration = sorted(activity_sums.items(),
 
903
                             key = lambda x: x[1],
 
904
                             reverse = True)
 
905
        by_duration_keys = [entry[0][0] for entry in by_duration]
 
906
 
 
907
        category_sums = [[entry[1] / 60.0 * (entry[0][1] == cat)
 
908
                            for cat in all_categories] for entry in by_duration]
 
909
        self.activity_chart.plot(by_duration_keys,
 
910
                                 category_sums,
 
911
                                 stack_keys = all_categories)
 
912
        
 
913
 
 
914
    def set_title(self):
 
915
        if self.day_view.get_active():
 
916
            # date format for overview label when only single day is visible
 
917
            # Using python datetime formatting syntax. See:
 
918
            # http://docs.python.org/library/time.html#time.strftime
 
919
            start_date_str = self.view_date.strftime(C_("single day overview",
 
920
                                                        "%B %d, %Y"))
 
921
            # Overview label if looking on single day
 
922
            overview_label = _(u"Overview for %(date)s") % \
 
923
                                                      ({"date": start_date_str})
 
924
        else:
 
925
            dates_dict = stuff.dateDict(self.start_date, "start_")
 
926
            dates_dict.update(stuff.dateDict(self.end_date, "end_"))
283
927
            
284
 
        return week
285
 
        
286
 
 
287
 
    def do_graph(self):
288
 
        dates_dict = stuff.dateDict(self.start_date, "start_")
289
 
        dates_dict.update(stuff.dateDict(self.end_date, "end_"))
290
 
        
291
 
        
292
 
        if self.start_date.year != self.end_date.year:
293
 
            # overview label if start and end years don't match
294
 
            # letter after prefixes (start_, end_) is the one of
295
 
            # standard python date formatting ones- you can use all of them
296
 
            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
297
 
        elif self.start_date.month != self.end_date.month:
298
 
            #overview label if start and end month do not match
299
 
            # letter after prefixes (start_, end_) is the one of
300
 
            # standard python date formatting ones- you can use all of them
301
 
            overview_label = _(u"Overview for %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
302
 
        else:
303
 
            #overview label for interval in same month
304
 
            # letter after prefixes (start_, end_) is the one of
305
 
            # standard python date formatting ones- you can use all of them
306
 
            overview_label = _(u"Overview for %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s") % dates_dict
307
 
 
308
 
        if self.day_view.get_active():
309
 
            # overview label for single day
310
 
            # letter after prefixes (start_, end_) is the one of
311
 
            # standard python date formatting ones- you can use all of them
312
 
            overview_label = _("Overview for %(start_B)s %(start_d)s, %(start_Y)s") % dates_dict
313
 
            dayview_caption = _("Day")
314
 
        elif self.week_view.get_active():
 
928
            if self.start_date.year != self.end_date.year:
 
929
                # overview label if start and end years don't match
 
930
                # letter after prefixes (start_, end_) is the one of
 
931
                # standard python date formatting ones- you can use all of them
 
932
                # see http://docs.python.org/library/time.html#time.strftime
 
933
                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
 
934
            elif self.start_date.month != self.end_date.month:
 
935
                # overview label if start and end month do not match
 
936
                # letter after prefixes (start_, end_) is the one of
 
937
                # standard python date formatting ones- you can use all of them
 
938
                # see http://docs.python.org/library/time.html#time.strftime
 
939
                overview_label = _(u"Overview for %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
 
940
            else:
 
941
                # overview label for interval in same month
 
942
                # letter after prefixes (start_, end_) is the one of
 
943
                # standard python date formatting ones- you can use all of them
 
944
                # see http://docs.python.org/library/time.html#time.strftime
 
945
                overview_label = _(u"Overview for %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s") % dates_dict
 
946
 
 
947
        if self.week_view.get_active():
315
948
            dayview_caption = _("Week")
316
 
        else:
 
949
        elif self.month_view.get_active():
317
950
            dayview_caption = _("Month")
318
 
        
319
 
        
320
 
        label = self.get_widget("overview_label")
321
 
        label.set_text(overview_label)
322
 
 
323
 
        label2 = self.get_widget("dayview_caption")
324
 
        label2.set_markup("<b>%s</b>" % (dayview_caption))
325
 
        
326
 
        facts = self.get_facts()
327
 
 
328
 
        self.day_chart.plot(facts["totals"]["by_day"])
329
 
        self.category_chart.plot(facts["totals"]["by_category"])
330
 
        self.activity_chart.plot(facts["totals"]["by_activity"])
331
 
 
332
 
 
333
 
 
 
951
        else:
 
952
            dayview_caption = _("Day")
 
953
        
 
954
        self.get_widget("overview_label").set_markup("<b>%s</b>" % overview_label)
 
955
        self.get_widget("dayview_caption").set_markup("%s" % (dayview_caption))
 
956
        
 
957
 
 
958
    def do_graph(self):
 
959
        self.set_title()
 
960
        
 
961
        if self.day_view.get_active():
 
962
            facts = runtime.storage.get_facts(self.view_date)
 
963
        else:
 
964
            facts = runtime.storage.get_facts(self.start_date, self.end_date)
 
965
 
 
966
 
 
967
        self.get_widget("report_button").set_sensitive(len(facts) > 0)
 
968
        self.fact_store.clear()
 
969
        
 
970
        self.fill_tree(facts)
 
971
 
 
972
        if not facts:
 
973
            self.get_widget("graphs").hide()
 
974
            self.get_widget("no_data_label").show()
 
975
            return 
 
976
 
 
977
 
 
978
        self.get_widget("no_data_label").hide()
 
979
        self.get_widget("graphs").show()
 
980
        self.do_charts(facts)
 
981
            
334
982
 
335
983
 
336
984
    def get_widget(self, name):
337
985
        """ skip one variable (huh) """
338
 
        return self.glade.get_widget(name)
 
986
        return self._gui.get_object(name)
 
987
 
 
988
    def on_pages_switch_page(self, notebook, page, pagenum):
 
989
        if pagenum == 1:
 
990
            year = None
 
991
            for child in self.get_widget("year_box").get_children():
 
992
                if child.get_active():
 
993
                    year = child.year
 
994
            
 
995
            self.stats(year)
 
996
        else:
 
997
            self.do_graph()
 
998
        
 
999
    def on_year_changed(self, button):
 
1000
        if self.bubbling: return
 
1001
        
 
1002
        for child in button.parent.get_children():
 
1003
            if child != button and child.get_active():
 
1004
                self.bubbling = True
 
1005
                child.set_active(False)
 
1006
                self.bubbling = False
 
1007
        
 
1008
        self.stats(button.year)
339
1009
 
340
1010
    def on_prev_clicked(self, button):
341
1011
        if self.day_view.get_active():
342
 
            self.start_date -= dt.timedelta(1)
343
 
            self.end_date -= dt.timedelta(1)
344
 
        
345
 
        elif self.week_view.get_active():
346
 
            self.start_date -= dt.timedelta(7)
347
 
            self.end_date -= dt.timedelta(7)
348
 
        
349
 
        elif self.month_view.get_active():
350
 
            self.end_date = self.start_date - dt.timedelta(1)
351
 
            first_weekday, days_in_month = calendar.monthrange(self.end_date.year, self.end_date.month)
352
 
            self.start_date = self.end_date - dt.timedelta(days_in_month - 1)
353
 
 
354
 
        self.view_date = self.start_date        
 
1012
            self.view_date -= dt.timedelta(1)
 
1013
            if self.view_date < self.start_date:
 
1014
                self.start_date -= dt.timedelta(7)
 
1015
                self.end_date -= dt.timedelta(7)
 
1016
        else:
 
1017
            if self.week_view.get_active():
 
1018
                self.start_date -= dt.timedelta(7)
 
1019
                self.end_date -= dt.timedelta(7)
 
1020
            
 
1021
            elif self.month_view.get_active():
 
1022
                self.end_date = self.start_date - dt.timedelta(1)
 
1023
                first_weekday, days_in_month = calendar.monthrange(self.end_date.year, self.end_date.month)
 
1024
                self.start_date = self.end_date - dt.timedelta(days_in_month - 1)
 
1025
 
 
1026
            self.view_date = self.start_date
 
1027
 
355
1028
        self.do_graph()
356
1029
 
357
1030
    def on_next_clicked(self, button):
358
1031
        if self.day_view.get_active():
359
 
            self.start_date += dt.timedelta(1)
360
 
            self.end_date += dt.timedelta(1)
361
 
        
362
 
        elif self.week_view.get_active():
363
 
            self.start_date += dt.timedelta(7)
364
 
            self.end_date += dt.timedelta(7)
365
 
        
366
 
        elif self.month_view.get_active():
367
 
            self.start_date = self.end_date + dt.timedelta(1)
368
 
            first_weekday, days_in_month = calendar.monthrange(self.start_date.year, self.start_date.month)
369
 
            self.end_date = self.start_date + dt.timedelta(days_in_month - 1)
370
 
        
371
 
        self.view_date = self.start_date
 
1032
            self.view_date += dt.timedelta(1)
 
1033
            if self.view_date > self.end_date:
 
1034
                self.start_date += dt.timedelta(7)
 
1035
                self.end_date += dt.timedelta(7)
 
1036
        else:
 
1037
            if self.week_view.get_active():
 
1038
                self.start_date += dt.timedelta(7)
 
1039
                self.end_date += dt.timedelta(7)        
 
1040
            elif self.month_view.get_active():
 
1041
                self.start_date = self.end_date + dt.timedelta(1)
 
1042
                first_weekday, days_in_month = calendar.monthrange(self.start_date.year, self.start_date.month)
 
1043
                self.end_date = self.start_date + dt.timedelta(days_in_month - 1)
 
1044
        
 
1045
            self.view_date = self.start_date
 
1046
 
372
1047
        self.do_graph()
373
1048
    
374
1049
    def on_home_clicked(self, button):
375
1050
        self.view_date = dt.date.today()
376
 
        if self.day_view.get_active():
377
 
            self.start_date = self.view_date
378
 
            self.end_date = self.view_date
379
 
        
380
 
        elif self.week_view.get_active():
 
1051
        if self.week_view.get_active():
381
1052
            self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1)
382
 
            self.start_date = self.start_date + dt.timedelta(self.locale_first_weekday())
 
1053
            self.start_date = self.start_date + dt.timedelta(stuff.locale_first_weekday())
383
1054
            self.end_date = self.start_date + dt.timedelta(6)
384
1055
        
385
1056
        elif self.month_view.get_active():
390
1061
        self.do_graph()
391
1062
        
392
1063
    def on_day_toggled(self, button):
393
 
        self.start_date = self.view_date
394
 
        self.end_date = self.view_date
 
1064
        self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1)
 
1065
        self.start_date = self.start_date + dt.timedelta(stuff.locale_first_weekday())
 
1066
 
 
1067
        self.end_date = self.start_date + dt.timedelta(6)
395
1068
        self.do_graph()
396
1069
 
397
1070
    def on_week_toggled(self, button):
398
1071
        self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1)
399
 
        self.start_date = self.start_date + dt.timedelta(self.locale_first_weekday())
 
1072
        self.start_date = self.start_date + dt.timedelta(stuff.locale_first_weekday())
400
1073
 
401
1074
        self.end_date = self.start_date + dt.timedelta(6)
402
1075
        self.do_graph()
419
1092
        if model[iter][0] == -1:
420
1093
            return #not a fact
421
1094
 
422
 
        custom_fact = CustomFactController(None, model[iter][0])
 
1095
        custom_fact = CustomFactController(self, None, model[iter][0])
423
1096
        custom_fact.show()
424
1097
 
425
1098
    def delete_selected(self):
438
1111
            if path > 0:
439
1112
                selection.select_path(path)
440
1113
 
441
 
        storage.remove_fact(model[iter][0])
 
1114
        runtime.storage.remove_fact(model[iter][0])
 
1115
 
 
1116
    def copy_selected(self):
 
1117
        selection = self.fact_tree.get_selection()
 
1118
        (model, iter) = selection.get_selected()
 
1119
 
 
1120
        fact = model[iter][6]
 
1121
        if not fact:
 
1122
            return #not a fact
 
1123
 
 
1124
        fact_str = "%s-%s %s" % (fact["start_time"].strftime("%H:%M"),
 
1125
                               (fact["end_time"] or dt.datetime.now()).strftime("%H:%M"),
 
1126
                               fact["name"])
 
1127
 
 
1128
        if fact["category"]:
 
1129
            fact_str += "@%s" % fact["category"]
 
1130
 
 
1131
        if fact["description"]:
 
1132
            fact_str += ", %s" % fact["description"]
 
1133
 
 
1134
        clipboard = gtk.Clipboard()
 
1135
        clipboard.set_text(fact_str)
 
1136
    
 
1137
    def check_clipboard(self):
 
1138
        clipboard = gtk.Clipboard()
 
1139
        clipboard.request_text(self.on_clipboard_text)
 
1140
    
 
1141
    def on_clipboard_text(self, clipboard, text, data):
 
1142
        # first check that we have a date selected
 
1143
        selection = self.fact_tree.get_selection()
 
1144
        (model, iter) = selection.get_selected()
 
1145
 
 
1146
        selected_date = self.view_date
 
1147
        if iter:
 
1148
            selected_date = model[iter][3].split("-")
 
1149
            selected_date = dt.date(int(selected_date[0]),
 
1150
                                    int(selected_date[1]),
 
1151
                                    int(selected_date[2]))
 
1152
        if not selected_date:
 
1153
            return
 
1154
        
 
1155
        res = stuff.parse_activity_input(text)
 
1156
 
 
1157
        if res.start_time is None or res.end_time is None:
 
1158
            return
 
1159
        
 
1160
        start_time = res.start_time.replace(year = selected_date.year,
 
1161
                                            month = selected_date.month,
 
1162
                                            day = selected_date.day)
 
1163
        end_time = res.end_time.replace(year = selected_date.year,
 
1164
                                               month = selected_date.month,
 
1165
                                               day = selected_date.day)
 
1166
    
 
1167
        activity_name = res.activity_name
 
1168
        if res.category_name:
 
1169
            activity_name += "@%s" % res.category_name
 
1170
            
 
1171
        if res.description:
 
1172
            activity_name += ", %s" % res.description
 
1173
 
 
1174
        activity_name = activity_name.decode("utf-8")
 
1175
 
 
1176
        # TODO - set cursor to the pasted entry when done
 
1177
        # TODO - revisit parsing of selected date
 
1178
        added_fact = runtime.storage.add_fact(activity_name, start_time, end_time)
 
1179
        
442
1180
 
443
1181
    """keyboard events"""
444
 
    def on_key_pressed(self, tree, event_key):
445
 
      if (event_key.keyval == gtk.keysyms.Delete):
446
 
        self.delete_selected()
 
1182
    def on_key_pressed(self, tree, event):
 
1183
        if (event.keyval == gtk.keysyms.Delete):
 
1184
            self.delete_selected()
 
1185
        elif event.keyval == gtk.keysyms.c and event.state & gtk.gdk.CONTROL_MASK:
 
1186
            self.copy_selected()
 
1187
        elif event.keyval == gtk.keysyms.v and event.state & gtk.gdk.CONTROL_MASK:
 
1188
            self.check_clipboard()
447
1189
    
448
1190
    def on_fact_selection_changed(self, selection, model):
449
1191
        """ enables and disables action buttons depending on selected item """
461
1203
    def on_facts_row_activated(self, tree, path, column):
462
1204
        selection = tree.get_selection()
463
1205
        (model, iter) = selection.get_selected()
464
 
        custom_fact = CustomFactController(None, model[iter][0])
 
1206
        custom_fact = CustomFactController(self, None, model[iter][0])
465
1207
        custom_fact.show()
466
1208
        
467
1209
    def on_add_clicked(self, button):
475
1217
                                    int(selected_date[1]),
476
1218
                                    int(selected_date[2]))
477
1219
 
478
 
        custom_fact = CustomFactController(selected_date)
 
1220
        custom_fact = CustomFactController(self, selected_date)
479
1221
        custom_fact.show()
480
1222
        
 
1223
    def init_report_dialog(self):
 
1224
        chooser = self.get_widget('save_report_dialog')
 
1225
        chooser.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
 
1226
        """
 
1227
        chooser.set
 
1228
        
 
1229
        chooser = gtk.FileChooserDialog(title = _("Save report - Time Tracker"),
 
1230
                                        parent = None,
 
1231
                                        buttons=(gtk.STOCK_CANCEL,
 
1232
                                                 gtk.RESPONSE_CANCEL,
 
1233
                                                 gtk.STOCK_SAVE,
 
1234
                                                 gtk.RESPONSE_OK))
 
1235
        """
 
1236
        chooser.set_current_folder(os.path.expanduser("~"))
 
1237
 
 
1238
        filters = {}
 
1239
 
 
1240
        filter = gtk.FileFilter()
 
1241
        filter.set_name(_("HTML Report"))
 
1242
        filter.add_mime_type("text/html")
 
1243
        filter.add_pattern("*.html")
 
1244
        filter.add_pattern("*.htm")
 
1245
        filters[filter] = "html"
 
1246
        chooser.add_filter(filter)
 
1247
 
 
1248
        filter = gtk.FileFilter()
 
1249
        filter.set_name(_("Tab-Separated Values (TSV)"))
 
1250
        filter.add_mime_type("text/plain")
 
1251
        filter.add_pattern("*.tsv")
 
1252
        filter.add_pattern("*.txt")
 
1253
        filters[filter] = "tsv"
 
1254
        chooser.add_filter(filter)
 
1255
 
 
1256
        filter = gtk.FileFilter()
 
1257
        filter.set_name(_("XML"))
 
1258
        filter.add_mime_type("text/xml")
 
1259
        filter.add_pattern("*.xml")
 
1260
        filters[filter] = "xml"
 
1261
        chooser.add_filter(filter)
 
1262
 
 
1263
        filter = gtk.FileFilter()
 
1264
        filter.set_name(_("iCal"))
 
1265
        filter.add_mime_type("text/calendar")
 
1266
        filter.add_pattern("*.ics")
 
1267
        filters[filter] = "ical"
 
1268
        chooser.add_filter(filter)
 
1269
 
 
1270
        filter = gtk.FileFilter()
 
1271
        filter.set_name("All files")
 
1272
        filter.add_pattern("*")
 
1273
        chooser.add_filter(filter)
 
1274
        
 
1275
    def on_report_chosen(self, widget, format, path, start_date, end_date,
 
1276
                                                                    categories):
 
1277
        self.report_chooser = None
 
1278
        
 
1279
        facts = runtime.storage.get_facts(start_date, end_date, category_id = categories)
 
1280
        reports.simple(facts,
 
1281
                       self.start_date,
 
1282
                       self.end_date,
 
1283
                       format,
 
1284
                       path)
 
1285
 
 
1286
        if format == ("html"):
 
1287
            webbrowser.open_new("file://%s" % path)
 
1288
        else:
 
1289
            gtk.show_uri(gtk.gdk.Screen(),
 
1290
                         "file://%s" % os.path.split(path)[0], 0L)
 
1291
 
 
1292
    def on_report_chooser_closed(self, widget):
 
1293
        self.report_chooser = None
 
1294
        
481
1295
    def on_report_button_clicked(self, widget):
482
 
        from hamster import reports
483
 
        facts = storage.get_facts(self.start_date, self.end_date)
484
 
        reports.simple(facts, self.start_date, self.end_date)
485
 
 
 
1296
        if not self.report_chooser:
 
1297
            self.report_chooser = ReportChooserDialog()
 
1298
            self.report_chooser.connect("report-chosen", self.on_report_chosen)
 
1299
            self.report_chooser.connect("report-chooser-closed",
 
1300
                                        self.on_report_chooser_closed)
 
1301
            self.report_chooser.show(self.start_date, self.end_date)
 
1302
        else:
 
1303
            self.report_chooser.present()
 
1304
        
 
1305
        
486
1306
    def after_activity_update(self, widget, renames):
487
1307
        self.do_graph()
488
1308
    
489
1309
    def after_fact_update(self, event, date):
490
 
        self.do_graph()
 
1310
        self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
 
1311
        self.popular_categories = [cat[0] for cat in runtime.storage.get_popular_categories()]
 
1312
        
 
1313
        if self.get_widget("pages").get_current_page() == 0:
 
1314
            self.do_graph()
 
1315
        else:
 
1316
            self.stats()
491
1317
        
492
1318
    def on_close(self, widget, event):
493
 
        dispatcher.del_handler('activity_updated', self.after_activity_update)
494
 
        dispatcher.del_handler('day_updated', self.after_fact_update)
495
 
        return False
 
1319
        runtime.dispatcher.del_handler('activity_updated',
 
1320
                                       self.after_activity_update)
 
1321
        runtime.dispatcher.del_handler('day_updated', self.after_fact_update)
 
1322
        self.close_window()        
496
1323
 
497
1324
    def on_window_key_pressed(self, tree, event_key):
498
1325
      if (event_key.keyval == gtk.keysyms.Escape
499
1326
          or (event_key.keyval == gtk.keysyms.w 
500
1327
              and event_key.state & gtk.gdk.CONTROL_MASK)):
501
 
        self.window.destroy()
502
 
    
 
1328
        self.close_window()
 
1329
    
 
1330
    
 
1331
    def close_window(self):
 
1332
        if not self.parent:
 
1333
            gtk.main_quit()
 
1334
        else:
 
1335
            self.window.destroy()
 
1336
            return False
 
1337
        
503
1338
    def show(self):
504
 
        self.window.show_all()
 
1339
        self.window.show()
505
1340