4
# Copyright (C) 2009, 2010 Toms Bauģis <toms.baugis at gmail.com>
5
# Copyright (C) 2009 Patryk Zawadzki <patrys at pld-linux.org>
7
# This file is part of Project Hamster.
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.
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.
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/>.
28
import dbus, dbus.service, dbus.mainloop.glib
30
from hamster import eds
31
from hamster.configuration import conf, runtime, dialogs
33
from hamster import stuff
34
from hamster.hamsterdbus import HAMSTER_URI, HamsterDbusController
36
# controllers for other windows
37
from hamster import widgets
38
from hamster import idle
43
logging.warning("Could not import wnck - workspace tracking will be disabled")
48
pynotify.init('Hamster Applet')
50
logging.warning("Could not import pynotify - notifications will be disabled")
53
class ProjectHamster(object):
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)
60
gtk.window_set_default_icon_name("hamster-applet")
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)
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)
73
self.tag_box = widgets.TagBox(interactive = False)
74
self.get_widget("tag_box").add(self.tag_box)
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)
81
self.get_widget("today_box").add(self.treeview)
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)
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)
93
except dbus.DBusException, e:
94
logging.error("Can't init dbus: %s" % e)
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")
102
runtime.dispatcher.add_handler('conf_changed', self.on_conf_changed)
104
# Load today's data, activities and set label
105
self.last_activity = None
108
# Hamster DBusController current fact initialising
111
# refresh hamster every 60 seconds to update duration
112
gobject.timeout_add_seconds(60, self.refresh_hamster)
114
runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
115
runtime.dispatcher.add_handler('day_updated', self.after_fact_update)
118
if self.workspace_tracking:
119
self.init_workspace_tracking()
121
self.notification = None
123
self.notification = pynotify.Notification("Oh hi",
124
"Greetings from hamster!")
125
self.notification.set_urgency(pynotify.URGENCY_LOW) # lower than grass
127
self._gui.connect_signals(self)
129
self.prev_size = None
131
if conf.get("standalone_window_maximized"):
132
self.window.maximize()
134
window_box = conf.get("standalone_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)
141
self.window.set_position(gtk.WIN_POS_CENTER)
145
self.accel_group = self.get_widget("accelgroup")
146
self.window.add_accel_group(self.accel_group)
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)
155
self.window.show_all()
157
def init_workspace_tracking(self):
158
if not wnck: # can't track if we don't have the trackable
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 = {}
166
def refresh_hamster(self):
167
"""refresh hamster every x secs - load today, check last activity etc."""
170
finally: # we want to go on no matter what, so in case of any error we find out about it sooner
173
def check_user(self):
174
if not self.notification:
177
if self.notify_interval <= 0 or self.notify_interval >= 121:
180
now = dt.datetime.now()
182
if self.last_activity:
183
delta = now - self.last_activity['start_time']
184
duration = delta.seconds / 60
186
if duration and duration % self.notify_interval == 0:
187
message = _(u"Working on <b>%s</b>") % self.last_activity['name']
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")
196
self.notification.update(_("Time Tracker"), message, "hamster-applet")
197
self.notification.show()
200
"""sets up today's tree and fills it with records
201
returns information about last activity"""
203
facts = runtime.storage.get_todays_facts()
205
self.treeview.detach_model()
206
self.treeview.clear()
208
if facts and facts[-1]["end_time"] == None:
209
self.last_activity = facts[-1]
211
self.last_activity = None
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)
220
self.treeview.attach_model()
223
self._gui.get_object("today_box").hide()
224
#self._gui.get_object("fact_totals").set_text(_("No records today"))
226
self._gui.get_object("today_box").show()
228
self.set_last_activity()
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)
237
self.get_widget("switch_activity").show()
238
self.get_widget("start_tracking").hide()
240
delta = dt.datetime.now() - activity['start_time']
241
duration = delta.seconds / 60
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()
250
self._gui.get_object("more_info_button").hide()
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()
256
self.tag_box.draw(activity["tags"])
258
self.get_widget("switch_activity").hide()
259
self.get_widget("start_tracking").show()
261
self.get_widget("last_activity_name").set_text(_("No activity"))
262
self.get_widget("last_activity_category").hide()
264
self.get_widget("activity_info_box").hide()
265
self._gui.get_object("more_info_button").show()
267
self.tag_box.draw([])
270
def delete_selected(self):
271
fact = self.treeview.get_selected_fact()
272
runtime.storage.remove_fact(fact["id"])
274
def __update_fact(self):
275
"""dbus controller current fact updating"""
278
if not self.last_activity:
279
self.dbusController.TrackingStopped()
281
last_activity_id = self.last_activity['id']
283
self.dbusController.FactUpdated(last_activity_id)
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()
293
def on_todays_keys(self, tree, event):
294
if (event.keyval == gtk.keysyms.Delete):
295
self.delete_selected()
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"])
304
def on_today_row_activated(self, tree, path, column):
305
fact = tree.get_selected_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)
316
def on_menu_add_earlier_activate(self, menu):
317
dialogs.edit.show(self.window)
319
def on_menu_overview_activate(self, menu_item):
320
dialogs.overview.show(self.window)
322
def on_menu_about_activate(self, component):
323
dialogs.about.show(self.window)
325
def on_menu_statistics_activate(self, component):
326
dialogs.stats.show(self.window)
328
def on_menu_preferences_activate(self, menu_item):
329
runtime.dispatcher.dispatch('panel_visible', False)
330
dialogs.prefs.show(self.window)
332
def on_menu_help_contents_activate(self, *args):
333
gtk.show_uri(gtk.gdk.Screen(), "ghelp:hamster-applet", 0L)
336
def after_activity_update(self, widget, renames):
337
self.new_name.refresh_activities()
340
def after_fact_update(self, event, date):
344
def on_idle_changed(self, event, state):
345
# state values: 0 = active, 1 = idle
347
# refresh when we are out of idle
348
# for example, instantly after coming back from suspend
350
self.refresh_hamster()
351
elif self.timeout_enabled and self.last_activity and \
352
self.last_activity['end_time'] is None:
354
runtime.storage.touch_fact(self.last_activity,
355
end_time = self.dbusIdleListener.getIdleFrom())
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
364
if not self.workspace_tracking:
365
return # default to not doing anything
367
current_workspace = screen.get_active_workspace()
369
# rely on workspace numbers as names change
370
prev = previous_workspace.get_number()
371
new = current_workspace.get_number()
373
# on switch, update our mapping between spaces and activities
374
self.workspace_activities[prev] = self.last_activity
378
if "name" in self.workspace_tracking:
379
# first try to look up activity by desktop name
380
mapping = conf.get("workspace_mapping")
382
parsed_activity = None
383
if new < len(mapping):
384
parsed_activity = stuff.parse_activity_input(mapping[new])
388
if parsed_activity.category_name:
389
category_id = runtime.storage.get_category_by_name(parsed_activity.category_name)
391
activity = runtime.storage.get_activity_by_name(parsed_activity.activity_name,
396
activity = dict(name = activity['name'],
397
category = activity['category'],
398
description = parsed_activity.description,
399
tags = parsed_activity.tags)
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]
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():
420
runtime.storage.add_fact(activity['name'],
421
", ".join(activity['tags']),
422
category_name = activity['category'],
423
description = activity['description'])
425
if self.notification:
426
self.notification.update(_("Changed activity"),
427
_("Switched to '%s'") % activity['name'],
429
self.notification.show()
431
"""global shortcuts"""
432
def on_conf_changed(self, event, data):
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":
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:
450
self.screen.disconnect(self.screen.workspace_handler)
454
def on_activity_text_changed(self, widget):
455
self.get_widget("switch_activity").set_sensitive(widget.get_text() != "")
457
def on_switch_activity_clicked(self, widget):
458
if not self.new_name.get_text():
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)
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)
472
def on_window_configure_event(self, window, event):
473
self.treeview.fix_row_heights()
476
self.window.show_all()
478
def get_widget(self, name):
479
return self._gui.get_object(name)
481
def on_more_info_button_clicked(self, *args):
482
gtk.show_uri(gtk.gdk.Screen(), "ghelp:hamster-applet", 0L)
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)
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])
498
if __name__ == "__main__":
499
gtk.gdk.threads_init()
500
app = ProjectHamster()