~kklimonda/ubuntu/natty/hamster-applet/lp.697667

« back to all changes in this revision

Viewing changes to src/hamster-standalone

  • Committer: Bazaar Package Importer
  • Author(s): Chris Coulson
  • Date: 2010-02-10 02:52:31 UTC
  • mfrom: (1.1.14 upstream)
  • Revision ID: james.westby@ubuntu.com-20100210025231-x0q5h4q7nlvihl09
Tags: 2.29.90-0ubuntu1
* New upstream version
  - workspace tracking - switch activity, when switching desktops
  - chart improvements - theme friendly and less noisier
  - for those without GNOME panel there is now a standalone version, 
    accessible via Applications -> Accessories -> Time Tracker
  - overview window remembers position
  - maintaining cursor on the selected row after edits / refreshes
  - descriptions once again in the main input field, delimited by comma
  - activity suggestion box now sorts items by recency (Patryk Zawadzki)
  - searching
  - simplified save report dialog, thanks to the what you see is what you 
    report revamp
  - overview/stats replaced with activities / totals and stats accessible 
    from totals
  - interactive graphs to drill down in totals
  - miscellaneous performance improvements
  - pixel-perfect graphs
* Updated 01_startup-fix.patch to apply to new source layout
* debian/control:
  - Add build-depend on gnome-doc-utils

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# - coding: utf-8 -
 
3
 
 
4
# Copyright (C) 2009, 2010 Toms Bauģis <toms.baugis at gmail.com>
 
5
# Copyright (C) 2009 Patryk Zawadzki <patrys at pld-linux.org>
 
6
 
 
7
# This file is part of Project Hamster.
 
8
 
 
9
# Project Hamster is free software: you can redistribute it and/or modify
 
10
# it under the terms of the GNU General Public License as published by
 
11
# the Free Software Foundation, either version 3 of the License, or
 
12
# (at your option) any later version.
 
13
 
 
14
# Project Hamster is distributed in the hope that it will be useful,
 
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
17
# GNU General Public License for more details.
 
18
 
 
19
# You should have received a copy of the GNU General Public License
 
20
# along with Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
 
21
 
 
22
import logging
 
23
import datetime as dt
 
24
 
 
25
import gtk
 
26
 
 
27
import gobject
 
28
import dbus, dbus.service, dbus.mainloop.glib
 
29
 
 
30
from hamster import eds
 
31
from hamster.configuration import conf, runtime, dialogs
 
32
 
 
33
from hamster import stuff
 
34
from hamster.hamsterdbus import HAMSTER_URI, HamsterDbusController
 
35
 
 
36
# controllers for other windows
 
37
from hamster import widgets
 
38
from hamster import idle
 
39
 
 
40
try:
 
41
    import wnck
 
42
except:
 
43
    logging.warning("Could not import wnck - workspace tracking will be disabled")
 
44
    wnck = None
 
45
 
 
46
try:
 
47
    import pynotify
 
48
    pynotify.init('Hamster Applet')
 
49
except:
 
50
    logging.warning("Could not import pynotify - notifications will be disabled")
 
51
    pynotify = None
 
52
 
 
53
class ProjectHamster(object):
 
54
    def __init__(self):
 
55
        # load window of activity switcher and todays view
 
56
        self._gui = stuff.load_ui_file("hamster.ui")
 
57
        self.window = self._gui.get_object('hamster-window')
 
58
        self.window.connect("delete_event", self.close_window)
 
59
 
 
60
        gtk.window_set_default_icon_name("hamster-applet")
 
61
 
 
62
        self.new_name = widgets.ActivityEntry()
 
63
        self.new_name.connect("value-entered", self.on_switch_activity_clicked)
 
64
        widgets.add_hint(self.new_name, _("Activity"))
 
65
        self.get_widget("new_name_box").add(self.new_name)
 
66
        self.new_name.connect("changed", self.on_activity_text_changed)
 
67
 
 
68
        self.new_tags = widgets.TagsEntry()
 
69
        self.new_tags.connect("tags_selected", self.on_switch_activity_clicked)
 
70
        widgets.add_hint(self.new_tags, _("Tags"))
 
71
        self.get_widget("new_tags_box").add(self.new_tags)
 
72
 
 
73
        self.tag_box = widgets.TagBox(interactive = False)
 
74
        self.get_widget("tag_box").add(self.tag_box)
 
75
 
 
76
        self.treeview = widgets.FactTree()
 
77
        self.treeview.connect("key-press-event", self.on_todays_keys)
 
78
        self.treeview.connect("edit-clicked", self._open_edit_activity)
 
79
        self.treeview.connect("row-activated", self.on_today_row_activated)
 
80
 
 
81
        self.get_widget("today_box").add(self.treeview)
 
82
 
 
83
        # DBus Setup
 
84
        try:
 
85
            dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
 
86
            name = dbus.service.BusName(HAMSTER_URI, dbus.SessionBus())
 
87
            self.dbusController = HamsterDbusController(bus_name = name)
 
88
 
 
89
            # Set up connection to the screensaver
 
90
            self.dbusIdleListener = idle.DbusIdleListener(runtime.dispatcher)
 
91
            runtime.dispatcher.add_handler('active_changed', self.on_idle_changed)
 
92
 
 
93
        except dbus.DBusException, e:
 
94
            logging.error("Can't init dbus: %s" % e)
 
95
 
 
96
        # configuration
 
97
        self.timeout_enabled = conf.get("enable_timeout")
 
98
        self.notify_on_idle = conf.get("notify_on_idle")
 
99
        self.notify_interval = conf.get("notify_interval")
 
100
        self.workspace_tracking = conf.get("workspace_tracking")
 
101
 
 
102
        runtime.dispatcher.add_handler('conf_changed', self.on_conf_changed)
 
103
 
 
104
        # Load today's data, activities and set label
 
105
        self.last_activity = None
 
106
        self.load_day()
 
107
 
 
108
        # Hamster DBusController current fact initialising
 
109
        self.__update_fact()
 
110
 
 
111
        # refresh hamster every 60 seconds to update duration
 
112
        gobject.timeout_add_seconds(60, self.refresh_hamster)
 
113
 
 
114
        runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
 
115
        runtime.dispatcher.add_handler('day_updated', self.after_fact_update)
 
116
 
 
117
        self.screen = None
 
118
        if self.workspace_tracking:
 
119
            self.init_workspace_tracking()
 
120
 
 
121
        self.notification = None
 
122
        if pynotify:
 
123
            self.notification = pynotify.Notification("Oh hi",
 
124
                                                      "Greetings from hamster!")
 
125
            self.notification.set_urgency(pynotify.URGENCY_LOW) # lower than grass
 
126
 
 
127
        self._gui.connect_signals(self)
 
128
 
 
129
        self.prev_size = None
 
130
 
 
131
        if conf.get("standalone_window_maximized"):
 
132
            self.window.maximize()
 
133
        else:
 
134
            window_box = conf.get("standalone_window_box")
 
135
            if window_box:
 
136
                x,y,w,h = (int(i) for i in window_box)
 
137
                self.window.move(x, y)
 
138
                self.window.move(x, y)
 
139
                self.window.resize(w, h)
 
140
            else:
 
141
                self.window.set_position(gtk.WIN_POS_CENTER)
 
142
 
 
143
 
 
144
        # bindings
 
145
        self.accel_group = self.get_widget("accelgroup")
 
146
        self.window.add_accel_group(self.accel_group)
 
147
 
 
148
        gtk.accel_map_add_entry("<hamster-applet>/tracking/add", gtk.keysyms.n, gtk.gdk.CONTROL_MASK)
 
149
        gtk.accel_map_add_entry("<hamster-applet>/tracking/overview", gtk.keysyms.o, gtk.gdk.CONTROL_MASK)
 
150
        gtk.accel_map_add_entry("<hamster-applet>/tracking/stats", gtk.keysyms.i, gtk.gdk.CONTROL_MASK)
 
151
        gtk.accel_map_add_entry("<hamster-applet>/tracking/quit", gtk.keysyms.Escape, 0)
 
152
        gtk.accel_map_add_entry("<hamster-applet>/edit/prefs", gtk.keysyms.p, gtk.gdk.CONTROL_MASK)
 
153
        gtk.accel_map_add_entry("<hamster-applet>/help/contents", gtk.keysyms.F1, 0)
 
154
 
 
155
        self.window.show_all()
 
156
 
 
157
    def init_workspace_tracking(self):
 
158
        if not wnck: # can't track if we don't have the trackable
 
159
            return
 
160
 
 
161
        self.screen = wnck.screen_get_default()
 
162
        self.screen.workspace_handler = self.screen.connect("active-workspace-changed", self.on_workspace_changed)
 
163
        self.workspace_activities = {}
 
164
 
 
165
    """UI functions"""
 
166
    def refresh_hamster(self):
 
167
        """refresh hamster every x secs - load today, check last activity etc."""
 
168
        try:
 
169
            self.check_user()
 
170
        finally:  # we want to go on no matter what, so in case of any error we find out about it sooner
 
171
            return True
 
172
 
 
173
    def check_user(self):
 
174
        if not self.notification:
 
175
            return
 
176
 
 
177
        if self.notify_interval <= 0 or self.notify_interval >= 121:
 
178
            return
 
179
 
 
180
        now = dt.datetime.now()
 
181
        message = None
 
182
        if self.last_activity:
 
183
            delta = now - self.last_activity['start_time']
 
184
            duration = delta.seconds /  60
 
185
 
 
186
            if duration and duration % self.notify_interval == 0:
 
187
                message = _(u"Working on <b>%s</b>") % self.last_activity['name']
 
188
 
 
189
        elif self.notify_on_idle:
 
190
            #if we have no last activity, let's just calculate duration from 00:00
 
191
            if (now.minute + now.hour *60) % self.notify_interval == 0:
 
192
                message = _(u"No activity")
 
193
 
 
194
 
 
195
        if message:
 
196
            self.notification.update(_("Time Tracker"), message, "hamster-applet")
 
197
            self.notification.show()
 
198
 
 
199
    def load_day(self):
 
200
        """sets up today's tree and fills it with records
 
201
           returns information about last activity"""
 
202
 
 
203
        facts = runtime.storage.get_todays_facts()
 
204
 
 
205
        self.treeview.detach_model()
 
206
        self.treeview.clear()
 
207
 
 
208
        if facts and facts[-1]["end_time"] == None:
 
209
            self.last_activity = facts[-1]
 
210
        else:
 
211
            self.last_activity = None
 
212
 
 
213
        by_category = {}
 
214
        for fact in facts:
 
215
            duration = 24 * 60 * fact["delta"].days + fact["delta"].seconds / 60
 
216
            by_category[fact['category']] = \
 
217
                          by_category.setdefault(fact['category'], 0) + duration
 
218
            self.treeview.add_fact(fact)
 
219
 
 
220
        self.treeview.attach_model()
 
221
 
 
222
        if not facts:
 
223
            self._gui.get_object("today_box").hide()
 
224
            #self._gui.get_object("fact_totals").set_text(_("No records today"))
 
225
        else:
 
226
            self._gui.get_object("today_box").show()
 
227
 
 
228
        self.set_last_activity()
 
229
 
 
230
    def set_last_activity(self):
 
231
        activity = self.last_activity
 
232
        #sets all the labels and everything as necessary
 
233
        self.get_widget("stop_tracking").set_sensitive(activity != None)
 
234
 
 
235
 
 
236
        if activity:
 
237
            self.get_widget("switch_activity").show()
 
238
            self.get_widget("start_tracking").hide()
 
239
 
 
240
            delta = dt.datetime.now() - activity['start_time']
 
241
            duration = delta.seconds /  60
 
242
 
 
243
            self.get_widget("last_activity_name").set_text(activity['name'])
 
244
            if activity['category'] != _("Unsorted"):
 
245
                self.get_widget("last_activity_category") \
 
246
                    .set_text(" - %s" % activity['category'])
 
247
            self.get_widget("last_activity_category").show()
 
248
 
 
249
 
 
250
            self._gui.get_object("more_info_button").hide()
 
251
 
 
252
            self.get_widget("last_activity_duration").set_text(stuff.format_duration(duration) or _("Just started"))
 
253
            self.get_widget("last_activity_description").set_text(activity['description'] or "")
 
254
            self.get_widget("activity_info_box").show()
 
255
 
 
256
            self.tag_box.draw(activity["tags"])
 
257
        else:
 
258
            self.get_widget("switch_activity").hide()
 
259
            self.get_widget("start_tracking").show()
 
260
 
 
261
            self.get_widget("last_activity_name").set_text(_("No activity"))
 
262
            self.get_widget("last_activity_category").hide()
 
263
 
 
264
            self.get_widget("activity_info_box").hide()
 
265
            self._gui.get_object("more_info_button").show()
 
266
 
 
267
            self.tag_box.draw([])
 
268
 
 
269
 
 
270
    def delete_selected(self):
 
271
        fact = self.treeview.get_selected_fact()
 
272
        runtime.storage.remove_fact(fact["id"])
 
273
 
 
274
    def __update_fact(self):
 
275
        """dbus controller current fact updating"""
 
276
        last_activity_id = 0
 
277
 
 
278
        if not self.last_activity:
 
279
            self.dbusController.TrackingStopped()
 
280
        else:
 
281
            last_activity_id = self.last_activity['id']
 
282
 
 
283
        self.dbusController.FactUpdated(last_activity_id)
 
284
 
 
285
    def _delayed_display(self):
 
286
        """show window only when gtk has become idle. otherwise we get
 
287
        mixed results. TODO - this looks like a hack though"""
 
288
        self.window.present()
 
289
        self.new_name.grab_focus()
 
290
 
 
291
 
 
292
    """events"""
 
293
    def on_todays_keys(self, tree, event):
 
294
        if (event.keyval == gtk.keysyms.Delete):
 
295
            self.delete_selected()
 
296
            return True
 
297
 
 
298
        return False
 
299
 
 
300
    def _open_edit_activity(self, row, fact):
 
301
        """opens activity editor for selected row"""
 
302
        dialogs.edit.show(self.window, fact_id = fact["id"])
 
303
 
 
304
    def on_today_row_activated(self, tree, path, column):
 
305
        fact = tree.get_selected_fact()
 
306
 
 
307
        if fact:
 
308
            runtime.storage.add_fact(fact["name"],
 
309
                                     ", ".join(fact["tags"]),
 
310
                                     category_name = fact["category"],
 
311
                                     description = fact["description"])
 
312
            runtime.dispatcher.dispatch('panel_visible', False)
 
313
 
 
314
 
 
315
    """button events"""
 
316
    def on_menu_add_earlier_activate(self, menu):
 
317
        dialogs.edit.show(self.window)
 
318
 
 
319
    def on_menu_overview_activate(self, menu_item):
 
320
        dialogs.overview.show(self.window)
 
321
 
 
322
    def on_menu_about_activate(self, component):
 
323
        dialogs.about.show(self.window)
 
324
 
 
325
    def on_menu_statistics_activate(self, component):
 
326
        dialogs.stats.show(self.window)
 
327
 
 
328
    def on_menu_preferences_activate(self, menu_item):
 
329
        runtime.dispatcher.dispatch('panel_visible', False)
 
330
        dialogs.prefs.show(self.window)
 
331
 
 
332
    def on_menu_help_contents_activate(self, *args):
 
333
        gtk.show_uri(gtk.gdk.Screen(), "ghelp:hamster-applet", 0L)
 
334
 
 
335
    """signals"""
 
336
    def after_activity_update(self, widget, renames):
 
337
        self.new_name.refresh_activities()
 
338
        self.load_day()
 
339
 
 
340
    def after_fact_update(self, event, date):
 
341
        self.load_day()
 
342
        self.__update_fact()
 
343
 
 
344
    def on_idle_changed(self, event, state):
 
345
        # state values: 0 = active, 1 = idle
 
346
 
 
347
        # refresh when we are out of idle
 
348
        # for example, instantly after coming back from suspend
 
349
        if state == 0:
 
350
            self.refresh_hamster()
 
351
        elif self.timeout_enabled and self.last_activity and \
 
352
             self.last_activity['end_time'] is None:
 
353
 
 
354
            runtime.storage.touch_fact(self.last_activity,
 
355
                                       end_time = self.dbusIdleListener.getIdleFrom())
 
356
 
 
357
    def on_workspace_changed(self, screen, previous_workspace):
 
358
        if not previous_workspace:
 
359
            # wnck has a slight hiccup on init and after that calls
 
360
            # workspace changed event with blank previous state that should be
 
361
            # ignored
 
362
            return
 
363
 
 
364
        if not self.workspace_tracking:
 
365
            return # default to not doing anything
 
366
 
 
367
        current_workspace = screen.get_active_workspace()
 
368
 
 
369
        # rely on workspace numbers as names change
 
370
        prev = previous_workspace.get_number()
 
371
        new = current_workspace.get_number()
 
372
 
 
373
        # on switch, update our mapping between spaces and activities
 
374
        self.workspace_activities[prev] = self.last_activity
 
375
 
 
376
 
 
377
        activity = None
 
378
        if "name" in self.workspace_tracking:
 
379
            # first try to look up activity by desktop name
 
380
            mapping = conf.get("workspace_mapping")
 
381
 
 
382
            parsed_activity = None
 
383
            if new < len(mapping):
 
384
                parsed_activity = stuff.parse_activity_input(mapping[new])
 
385
 
 
386
            if parsed_activity:
 
387
                category_id = None
 
388
                if parsed_activity.category_name:
 
389
                    category_id = runtime.storage.get_category_by_name(parsed_activity.category_name)
 
390
 
 
391
                activity = runtime.storage.get_activity_by_name(parsed_activity.activity_name,
 
392
                                                                category_id,
 
393
                                                                ressurect = False)
 
394
                if activity:
 
395
                    # we need dict below
 
396
                    activity = dict(name = activity['name'],
 
397
                                    category = activity['category'],
 
398
                                    description = parsed_activity.description,
 
399
                                    tags = parsed_activity.tags)
 
400
 
 
401
 
 
402
        if not activity and "memory" in self.workspace_tracking:
 
403
            # now see if maybe we have any memory of the new workspace
 
404
            # (as in - user was here and tracking Y)
 
405
            # if the new workspace is in our dict, switch to the specified activity
 
406
            if new in self.workspace_activities and self.workspace_activities[new]:
 
407
                activity = self.workspace_activities[new]
 
408
 
 
409
        if not activity:
 
410
            return
 
411
 
 
412
        # check if maybe there is no need to switch, as field match:
 
413
        if self.last_activity and \
 
414
           self.last_activity['name'].lower() == activity['name'].lower() and \
 
415
           (self.last_activity['category'] or "").lower() == (activity['category'] or "").lower() and \
 
416
           ", ".join(self.last_activity['tags']).lower() == ", ".join(activity['tags']).lower():
 
417
            return
 
418
 
 
419
        # ok, switch
 
420
        runtime.storage.add_fact(activity['name'],
 
421
                                 ", ".join(activity['tags']),
 
422
                                 category_name = activity['category'],
 
423
                                 description = activity['description'])
 
424
 
 
425
        if self.notification:
 
426
            self.notification.update(_("Changed activity"),
 
427
                                     _("Switched to '%s'") % activity['name'],
 
428
                                     "hamster-applet")
 
429
            self.notification.show()
 
430
 
 
431
    """global shortcuts"""
 
432
    def on_conf_changed(self, event, data):
 
433
        key, value = data
 
434
 
 
435
        if key == "enable_timeout":
 
436
            self.timeout_enabled = value
 
437
        elif key == "notify_on_idle":
 
438
            self.notify_on_idle = value
 
439
        elif key == "notify_interval":
 
440
            self.notify_interval = value
 
441
        elif key == "day_start_minutes":
 
442
            self.load_day()
 
443
 
 
444
        elif key == "workspace_tracking":
 
445
            self.workspace_tracking = value
 
446
            if self.workspace_tracking and not self.screen:
 
447
                self.init_workspace_tracking()
 
448
            elif not self.workspace_tracking:
 
449
                if self.screen:
 
450
                    self.screen.disconnect(self.screen.workspace_handler)
 
451
                    self.screen = None
 
452
 
 
453
 
 
454
    def on_activity_text_changed(self, widget):
 
455
        self.get_widget("switch_activity").set_sensitive(widget.get_text() != "")
 
456
 
 
457
    def on_switch_activity_clicked(self, widget):
 
458
        if not self.new_name.get_text():
 
459
            return False
 
460
 
 
461
        runtime.storage.add_fact(self.new_name.get_text().decode("utf8", "replace"),
 
462
                                 self.new_tags.get_text().decode("utf8", "replace"))
 
463
        self.new_name.set_text("")
 
464
        self.new_tags.set_text("")
 
465
        runtime.dispatcher.dispatch('panel_visible', False)
 
466
 
 
467
    def on_stop_tracking_clicked(self, widget):
 
468
        runtime.storage.touch_fact(self.last_activity)
 
469
        self.last_activity = None
 
470
        runtime.dispatcher.dispatch('panel_visible', False)
 
471
 
 
472
    def on_window_configure_event(self, window, event):
 
473
        self.treeview.fix_row_heights()
 
474
 
 
475
    def show(self):
 
476
        self.window.show_all()
 
477
 
 
478
    def get_widget(self, name):
 
479
        return self._gui.get_object(name)
 
480
 
 
481
    def on_more_info_button_clicked(self, *args):
 
482
        gtk.show_uri(gtk.gdk.Screen(), "ghelp:hamster-applet", 0L)
 
483
        return False
 
484
 
 
485
    def close_window(self, *args):
 
486
        # properly saving window state and position
 
487
        maximized = self.window.get_window().get_state() & gtk.gdk.WINDOW_STATE_MAXIMIZED
 
488
        conf.set("standalone_window_maximized", maximized)
 
489
 
 
490
        # make sure to remember dimensions only when in normal state
 
491
        if maximized == False and not self.window.get_window().get_state() & gtk.gdk.WINDOW_STATE_ICONIFIED:
 
492
            x, y = self.window.get_position()
 
493
            w, h = self.window.get_size()
 
494
            conf.set("standalone_window_box", [x, y, w, h])
 
495
 
 
496
        gtk.main_quit()
 
497
 
 
498
if __name__ == "__main__":
 
499
    gtk.gdk.threads_init()
 
500
    app = ProjectHamster()
 
501
    gtk.main()