~ubuntu-branches/ubuntu/wily/hamster-applet/wily

« back to all changes in this revision

Viewing changes to hamster/applet.py

  • Committer: Bazaar Package Importer
  • Author(s): Pedro Fragoso
  • Date: 2008-12-15 17:42:14 UTC
  • mto: (1.1.16 upstream) (20.1.1 lucid-proposed)
  • mto: This revision was merged to the branch mainline in revision 6.
  • Revision ID: james.westby@ubuntu.com-20081215174214-ri1ezw1c2pdnl3i6
Tags: upstream-2.25.3
ImportĀ upstreamĀ versionĀ 2.25.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
import os.path
25
25
import gnomeapplet, gtk
26
26
import gobject
 
27
import dbus
 
28
import dbus.service
 
29
import dbus.mainloop.glib
27
30
 
28
31
from hamster import dispatcher, storage, SHARED_DATA_DIR
29
32
import hamster.eds
31
34
 
32
35
from hamster.stuff import *
33
36
from hamster.KeyBinder import *
 
37
from hamster.hamsterdbus import HAMSTER_URI, HamsterDbusController 
 
38
 
34
39
import idle
35
40
 
 
41
try:
 
42
    import pynotify
 
43
    PYNOTIFY = True
 
44
except:
 
45
    PYNOTIFY = False
 
46
    
 
47
class Notifier(object):
 
48
    def __init__(self, app_name, icon, attach):
 
49
        self._icon = icon
 
50
        self._attach = attach
 
51
        self._notify = None
 
52
        # Title of reminder baloon
 
53
        self.summary = _("Time Tracker")
 
54
      
 
55
        if not pynotify.is_initted():
 
56
            pynotify.init(app_name)
 
57
 
 
58
    def msg(self, body, switch_cb, stop_cb):
 
59
        self._notify = pynotify.Notification(self.summary, body, self._icon, self._attach)
 
60
        self._notify.add_action("refresh", _("Switch Task"), switch_cb)
 
61
        self._notify.add_action("no", _("Stop Tracking"), stop_cb)
 
62
        self._notify.show()
 
63
 
 
64
 
36
65
class PanelButton(gtk.ToggleButton):
37
66
    def __init__(self):
38
67
        gtk.ToggleButton.__init__(self)
40
69
        self.set_border_width(0)
41
70
        
42
71
        self.label = gtk.Label()
 
72
        self.label.set_justify(gtk.JUSTIFY_CENTER)
 
73
 
 
74
        self.label.connect('style-set', self.on_label_style_set)
 
75
        self.connect('size_allocate', self.on_size_allocate)
 
76
 
43
77
        self.add(self.label)
44
 
 
45
 
    def set_text(self, text):
46
 
        self.label.set_text(text)
 
78
        
 
79
        self.activity, self.duration = None, None
 
80
        self.prev_size = 0
 
81
 
 
82
    def set_active(self, is_active):
 
83
        self.set_property('active', is_active)
 
84
 
 
85
    def set_text(self, activity, duration):
 
86
        self.activity = activity
 
87
        self.duration = duration
 
88
        self.reformat_label()
 
89
        
 
90
    def reformat_label(self):
 
91
        label = self.activity
 
92
        if self.duration:
 
93
            if self.use_two_line_format():
 
94
                label = "%s\n%s" % (self.activity, self.duration)
 
95
            else:
 
96
                label = "%s %s" % (self.activity, self.duration)
 
97
        
 
98
        label = '<span gravity=\"south\">' + label + '</span>'
 
99
        self.label.set_markup(label)
47
100
 
48
101
    def get_pos(self):
49
102
        return gtk.gdk.Window.get_origin(self.label.window)
50
103
 
51
 
    def set_active(self, is_active):
52
 
        self.set_property('active', is_active)
 
104
 
 
105
    def use_two_line_format(self):
 
106
        if not self.get_parent():
 
107
            return False
 
108
 
 
109
        popup_dir = self.get_parent().get_orient()
 
110
 
 
111
        orient_vertical = popup_dir in [gnomeapplet.ORIENT_LEFT] or \
 
112
                          popup_dir in [gnomeapplet.ORIENT_RIGHT]
 
113
 
 
114
 
 
115
        import pango
 
116
        context = self.label.get_pango_context()
 
117
        metrics = context.get_metrics(self.label.style.font_desc,
 
118
                                      pango.Context.get_language(context))
 
119
        ascent = pango.FontMetrics.get_ascent(metrics)
 
120
        descent = pango.FontMetrics.get_descent(metrics)
 
121
 
 
122
        if orient_vertical == False:
 
123
            thickness = self.style.ythickness;
 
124
        else:
 
125
            thickness = self.style.xthickness;
 
126
 
 
127
        focus_width = self.style_get_property("focus-line-width")
 
128
        focus_pad = self.style_get_property("focus-padding")
 
129
 
 
130
        required_size = 2 * ((pango.PIXELS(ascent + descent) ) + 2 * (focus_width + focus_pad + thickness))
 
131
 
 
132
        if orient_vertical:
 
133
            available_size = self.get_allocation().width
 
134
        else:
 
135
            available_size = self.get_allocation().height
 
136
        
 
137
        return required_size <= available_size
 
138
 
 
139
    def on_label_style_set(self, widget, something):
 
140
        self.reformat_label()
 
141
 
 
142
 
 
143
    def on_size_allocate(self, widget, allocation):
 
144
        if not self.get_parent():
 
145
            return
 
146
 
 
147
        self.popup_dir = self.get_parent().get_orient()
 
148
        
 
149
        orient_vertical = True
 
150
        new_size = allocation.width
 
151
        if self.popup_dir in [gnomeapplet.ORIENT_LEFT]:
 
152
            new_angle = 270
 
153
        elif self.popup_dir in [gnomeapplet.ORIENT_RIGHT]:
 
154
            new_angle = 90
 
155
        else:
 
156
            new_angle = 0
 
157
            orient_vertical = False
 
158
            new_size = allocation.height
 
159
        
 
160
        if new_angle != self.label.get_angle():
 
161
            self.label.set_angle(new_angle)
 
162
 
 
163
        if new_size != self.prev_size:
 
164
            self.reformat_label()
 
165
 
 
166
        self.prev_size = new_size
53
167
 
54
168
 
55
169
class HamsterApplet(object):
 
170
    def name_painter(self, column, cell, model, iter):
 
171
        activity_name = model.get_value(iter, 1)
 
172
        description = model.get_value(iter, 5)
 
173
 
 
174
        text = """%s""" % activity_name
 
175
        if description:
 
176
            text+= """\n<span style="italic" size="small">%s</span>""" % (description)
 
177
            
 
178
        cell.set_property('markup', text)
 
179
            
 
180
        return
 
181
 
 
182
 
56
183
    def __init__(self, applet):
57
184
        self.config = GconfStore.get_instance()
58
185
        
59
186
        self.applet = applet
60
187
        self.applet.set_applet_flags (gnomeapplet.EXPAND_MINOR);
61
188
 
 
189
        self.preferences_editor = None
 
190
        self.applet.about = None
 
191
 
62
192
        self.button = PanelButton()
63
193
        
64
194
        # load window of activity switcher and todays view
65
195
        self.glade = gtk.glade.XML(os.path.join(SHARED_DATA_DIR, "menu.glade"))
66
196
        self.window = self.glade.get_widget('hamster-window')
67
 
        
68
 
        # set up drop down menu
69
 
        self.activity_list = self.glade.get_widget('activity-list')
70
 
        self.activity_list.set_model(gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_INT))
71
 
        self.activity_list.set_text_column(0)
 
197
        self.window.set_keep_above(True)
72
198
 
73
 
        # set up autocompletition for the drop-down menu
74
 
        self.activities = gtk.ListStore(gobject.TYPE_STRING)
75
 
        completion = gtk.EntryCompletion()
76
 
        completion.set_model(self.activities)
77
 
        completion.set_text_column(0)
78
 
        completion.set_minimum_key_length(1)
79
 
        self.activity_list.child.set_completion(completion)
 
199
        self.set_dropdown()
80
200
 
81
201
        # init today's tree
82
202
        self.treeview = self.glade.get_widget('today')
83
203
        self.treeview.set_tooltip_column(1)
84
204
        
85
205
        self.treeview.append_column(gtk.TreeViewColumn("Time", gtk.CellRendererText(), text=2))
86
 
        self.treeview.append_column(ExpanderColumn("Name", text = 1))
87
 
        self.treeview.append_column(gtk.TreeViewColumn("", gtk.CellRendererText(), text=3))
 
206
 
 
207
        nameColumn = gtk.TreeViewColumn(_("Name"))
 
208
        nameColumn.set_expand(True)
 
209
        nameCell = gtk.CellRendererText()
 
210
        nameColumn.pack_start(nameCell, True)
 
211
        nameCell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
212
        nameColumn.set_cell_data_func(nameCell, self.name_painter)
 
213
        self.treeview.append_column(nameColumn)
 
214
 
88
215
        
89
216
        edit_cell = gtk.CellRendererPixbuf()
90
217
        edit_cell.set_property("stock_id", "gtk-edit")
91
218
        self.edit_column = gtk.TreeViewColumn("", edit_cell)
92
219
        self.treeview.append_column(self.edit_column)
93
220
 
 
221
        # DBus Setup
 
222
        try:
 
223
            dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
 
224
            name = dbus.service.BusName(HAMSTER_URI, dbus.SessionBus())
 
225
            self.dbusController = HamsterDbusController(bus_name = name)
94
226
 
 
227
            # let's also attach our listeners here
 
228
            bus = dbus.SessionBus()
 
229
            bus.add_signal_receiver(self.on_idle_changed,
 
230
                                    dbus_interface="org.gnome.ScreenSaver",
 
231
                                    signal_name="SessionIdleChanged")
 
232
        except dbus.DBusException, e:
 
233
            print "can't init dbus: %s" % e
 
234
    
95
235
        # Load today's data, activities and set label
96
236
        self.last_activity = None
97
237
        self.today = datetime.date.today()
99
239
        self.load_day()
100
240
        self.update_label()
101
241
 
 
242
        # Hamster DBusController current fact initialising
 
243
        self.__update_fact()
 
244
 
102
245
        # refresh hamster every 60 seconds to update duration
103
246
        gobject.timeout_add_seconds(60, self.refresh_hamster)
104
247
 
125
268
            SHARED_DATA_DIR, "Hamster_Applet.xml",
126
269
            None, [
127
270
            ("about", self.on_about),
 
271
            ("overview", self.show_overview),
128
272
            ("preferences", self.show_preferences),
129
273
            ])
130
274
 
132
276
        self.applet.set_background_widget(self.applet)
133
277
 
134
278
        self.button.connect('toggled', self.on_toggle)
135
 
        self.button.connect('size_allocate', self.on_applet_size_allocate)
136
279
 
137
280
        self.button.connect('button_press_event', self.on_button_press)
138
281
        self.glade.signal_autoconnect(self)
139
282
 
140
283
        # init hotkey
141
284
        dispatcher.add_handler('keybinding_activated', self.on_keybinding_activated)
142
 
        dispatcher.add_handler('gconf_timeout_changed', self.on_timeout_changed)
 
285
 
 
286
        # init idle check
143
287
        dispatcher.add_handler('gconf_timeout_enabled_changed', self.on_timeout_enabled_changed)
144
 
  
145
 
        # init idle check
146
 
        self.timeout_enabled = self.config.get_timeout_enabled()
 
288
        self.on_timeout_enabled_changed(None, self.config.get_timeout_enabled())
 
289
        
 
290
        # init nagging timeout
 
291
        if PYNOTIFY:
 
292
            self.notify = Notifier('HamsterApplet', gtk.STOCK_DIALOG_QUESTION, self.button)
 
293
            dispatcher.add_handler('gconf_notify_interval_changed', self.on_notify_interval_changed)
 
294
            self.on_notify_interval_changed(None, self.config.get_notify_interval())
 
295
 
 
296
 
 
297
    def on_idle_changed(self, state):
 
298
        print "Idle state changed. Idle: ", state
 
299
        # refresh when we are out of idle
 
300
        # (like, instantly after computer has been turned on!
 
301
        if state == 0:
 
302
            self.refresh_hamster() 
 
303
        
 
304
    def set_dropdown(self):
 
305
        # set up drop down menu
 
306
        self.activity_list = self.glade.get_widget('activity-list')
 
307
        self.activity_list.set_model(gtk.ListStore(gobject.TYPE_STRING,
 
308
                                                   gobject.TYPE_STRING,
 
309
                                                   gobject.TYPE_STRING))
 
310
 
 
311
        self.activity_list.clear()
 
312
        activity_cell = gtk.CellRendererText()
 
313
        self.activity_list.pack_start(activity_cell, True)
 
314
        self.activity_list.add_attribute(activity_cell, 'text', 0)
 
315
        category_cell = CategoryCell()  
 
316
        self.activity_list.pack_start(category_cell, False)
 
317
        self.activity_list.add_attribute(category_cell, 'text', 1)
 
318
        
 
319
        self.activity_list.set_property("text-column", 2)
 
320
 
 
321
 
 
322
        # set up autocompletition
 
323
        self.activities = gtk.ListStore(gobject.TYPE_STRING,
 
324
                                        gobject.TYPE_STRING,
 
325
                                        gobject.TYPE_STRING)
 
326
        completion = gtk.EntryCompletion()
 
327
        completion.set_model(self.activities)
 
328
 
 
329
        activity_cell = gtk.CellRendererText()
 
330
        completion.pack_start(activity_cell, True)
 
331
        completion.add_attribute(activity_cell, 'text', 0)
 
332
        completion.set_property("text-column", 2)
 
333
 
 
334
        category_cell = CategoryCell()  
 
335
        completion.pack_start(category_cell, False)
 
336
        completion.add_attribute(category_cell, 'text', 1)
 
337
 
 
338
 
 
339
        def match_func(completion, key, iter):
 
340
            model = completion.get_model()
 
341
            text = model.get_value(iter, 2)
 
342
            if text and text.startswith(key):
 
343
                return True
 
344
            return False
 
345
 
 
346
        completion.set_match_func(match_func)
 
347
        completion.set_minimum_key_length(1)
 
348
        completion.set_inline_completion(True)
 
349
 
 
350
        self.activity_list.child.set_completion(completion)
 
351
        
147
352
 
148
353
    def on_today_release_event(self, tree, event):
149
354
        pointer = event.window.get_pointer() # x, y, flags
178
383
            
179
384
 
180
385
        if self.last_activity and self.last_activity['end_time'] == None:
 
386
            # if we have running task and nagging is enabled
 
387
            # check if maybe it is time to nag
 
388
            if self.notify_interval:
 
389
                self.check_user()
 
390
 
181
391
            # if we have date change - let's finish previous task and start a new one
182
392
            if prev_date and prev_date != self.today: 
183
393
                storage.touch_fact(self.last_activity)
193
403
            delta = datetime.datetime.now() - self.last_activity['start_time']
194
404
            duration = delta.seconds /  60
195
405
            label = "%s %s" % (self.last_activity['name'], format_duration(duration))
 
406
            self.button.set_text(self.last_activity['name'], format_duration(duration))
196
407
            
197
408
            self.glade.get_widget('current_activity').set_text(self.last_activity['name'])
198
409
            self.glade.get_widget('stop_tracking').set_sensitive(1);
199
410
        else:
200
411
            label = "%s" % _(u"No activity")
 
412
            self.button.set_text(label, None)
201
413
            self.glade.get_widget('stop_tracking').set_sensitive(0);
202
 
        self.button.set_text(label)
203
 
        
 
414
        
 
415
        
 
416
        # Hamster DBusController current activity updating
 
417
        self.dbusController.update_activity(label)
 
418
 
 
419
    def check_user(self):
 
420
        delta = datetime.datetime.now() - self.last_activity['start_time']
 
421
        duration = delta.seconds /  60
 
422
                 
 
423
        if duration and duration % self.notify_interval == 0:
 
424
            # activity reminder
 
425
            msg = _(u"Are you still working on <b>%s</b>?") % self.last_activity['name']
 
426
            self.notify.msg(msg, self.switch_cb, self.stop_cb)
 
427
 
 
428
    def switch_cb(self, n, action):
 
429
        self.__show_toggle(None, not self.button.get_active())  
 
430
 
 
431
    def stop_cb(self, n, action):
 
432
        self.on_stop_tracking(None)
 
433
 
 
434
 
204
435
    def load_day(self):
205
436
        """sets up today's tree and fills it with records
206
437
           returns information about last activity"""
210
441
        if len(day.facts) == 0:
211
442
            self.last_activity = None
212
443
            self.glade.get_widget("todays_scroll").hide()
213
 
            self.glade.get_widget("no_facts_today").show()
 
444
            
 
445
            self.glade.get_widget("fact_totals").set_text(_("No records today"))
214
446
        else:
215
447
            self.last_activity = day.facts[len(day.facts) - 1]
216
448
            self.glade.get_widget("todays_scroll").show()
217
 
            self.glade.get_widget("no_facts_today").hide()
 
449
            
 
450
            total_string = ""
 
451
            for total in day.totals:
 
452
                total_string += _("%(category)s: %(duration)s, ") % ({'category': total,
 
453
                                                                      'duration': format_duration(day.totals[total])})
 
454
 
 
455
            total_string = total_string.rstrip(", ") # trailing slash
 
456
            self.glade.get_widget("fact_totals").set_text(total_string)
218
457
   
219
458
 
220
459
    def refresh_menu(self):
221
 
        #first populate the autocomplete - contains all entries
 
460
        #first populate the autocomplete - contains all entries in lowercase
222
461
        self.activities.clear()
223
 
        all_activities = storage.get_activities()
 
462
        all_activities = storage.get_autocomplete_activities()
224
463
        for activity in all_activities:
225
 
            self.activities.append([activity['name']])
 
464
            activity_category = "%s@%s" % (activity['name'], activity['category'])
 
465
            self.activities.append([activity['name'],
 
466
                                    activity['category'],
 
467
                                    activity_category])
226
468
 
227
469
 
228
470
        #now populate the menu - contains only categorized entries
233
475
        categorized_activities = storage.get_sorted_activities()
234
476
 
235
477
        for activity in categorized_activities:
236
 
            item = store.append([activity['name'], activity['id']])
 
478
            activity_category = "%s@%s" % (activity['name'], activity['category'])
 
479
            item = store.append([activity['name'],
 
480
                                 activity['category'],
 
481
                                 activity_category])
237
482
 
238
483
        # finally add TODO tasks from evolution to both lists
239
484
        tasks = hamster.eds.get_eds_tasks()
240
485
        for activity in tasks:
241
 
            self.activities.append([activity['name']])
242
 
            store.append([activity['name'], -1])
 
486
            activity_category = "%s@%s" % (activity['name'], activity['category'])
 
487
            self.activities.append([activity['name'],activity['category'],activity_category])
 
488
            store.append([activity['name'], activity['category'], activity_category])
243
489
 
244
490
        return True
245
491
 
257
503
        self.treeview.set_cursor(cur)
258
504
 
259
505
 
 
506
    def __update_fact(self):
 
507
        """dbus controller current fact updating"""
 
508
        if self.last_activity and self.last_activity['end_time'] == None:
 
509
            self.dbusController.update_fact(self.last_activity["name"])
 
510
        else:
 
511
            self.dbusController.update_fact(_(u'No activity'))
 
512
 
 
513
 
260
514
    def __show_toggle(self, event, is_active):
261
515
        """main window display and positioning"""
262
516
        self.button.set_active(is_active)
291
545
        else:
292
546
            self.activity_list.child.set_text('')
293
547
 
 
548
        # doing unstick / stick here, because sometimes while switching
 
549
        # between workplaces window still manages to dissappear
 
550
        self.window.unstick()
 
551
        self.window.stick() #show on all desktops
 
552
 
294
553
        gobject.idle_add(self._delayed_display)  
295
554
        
296
555
    def _delayed_display(self):
361
620
        stats_viewer = StatsViewer()
362
621
        stats_viewer.show()
363
622
 
 
623
    def show_overview(self, menu_item, verb):
 
624
        return self.on_overview(menu_item)
 
625
 
364
626
    def on_custom_fact(self, menu_item):
365
627
        from hamster.add_custom_fact import CustomFactController
366
628
        custom_fact = CustomFactController()
367
629
        custom_fact.show()
368
630
 
369
631
    def on_about (self, component, verb):
370
 
        from hamster.about import show_about
371
 
        show_about(self.applet)
 
632
        if self.applet.about:
 
633
            self.applet.about.present()
 
634
        else:
 
635
            from hamster.about import show_about
 
636
            show_about(self.applet)
372
637
 
373
638
    def show_preferences(self, menu_item, verb):
374
639
        from hamster.preferences import PreferencesEditor
375
640
 
376
641
        dispatcher.dispatch('panel_visible', False)
377
 
        activities_editor = PreferencesEditor()
378
 
        activities_editor.show()
 
642
        
 
643
        if self.preferences_editor and self.preferences_editor.window:
 
644
            self.preferences_editor.window.present()
 
645
        else:
 
646
            self.preferences_editor = PreferencesEditor()
 
647
            self.preferences_editor.show()
379
648
    
380
649
    """signals"""
381
650
    def after_activity_update(self, widget, renames):
387
656
        if date.date() == datetime.date.today():
388
657
            self.load_day()
389
658
            self.update_label()
 
659
    
 
660
        self.__update_fact()
390
661
 
391
662
    """global shortcuts"""
392
663
    def on_keybinding_activated(self, event, data):
393
664
        self.__show_toggle(None, not self.button.get_active())
394
665
        
395
 
    def on_timeout_changed(self, event, new_timeout):
396
 
        self.timeout = new_timeout
397
 
        
398
666
    def on_timeout_enabled_changed(self, event, enabled):
399
667
        # if enabled, set to value, otherwise set to zero, which means disable
400
668
        self.timeout_enabled = enabled
401
669
 
402
 
    def on_applet_size_allocate(self, widget, event):
403
 
        self.popup_dir = self.applet.get_orient()
404
 
        
405
 
        if self.popup_dir in [gnomeapplet.ORIENT_LEFT]:
406
 
            new_angle = 270
407
 
        elif self.popup_dir in [gnomeapplet.ORIENT_RIGHT]:
408
 
            new_angle = 90
 
670
    def on_notify_interval_changed(self, event, new_interval):
 
671
        if PYNOTIFY and 0 < new_interval < 121:
 
672
            self.notify_interval = new_interval
409
673
        else:
410
 
            new_angle = 0
411
 
 
412
 
        if new_angle != self.button.label.get_angle():
413
 
            self.button.label.set_angle(new_angle)
 
674
            self.notify_interval = None