1
# -.- coding: utf-8 -.-
3
# GNOME Activity Journal
5
# Copyright © 2009-2010 Seif Lotfy <seif@lotfy.com>
6
# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
7
# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
9
# This program 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
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
31
from datetime import date
33
from zeitgeist.client import ZeitgeistClient
34
from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation, \
37
from common import shade_gdk_color, combine_gdk_color, get_gtk_rgba
39
from thumb import ThumbBox
40
from timeline import TimelineView, TimelineHeader
41
from eventgatherer import get_dayevents, get_file_events
43
CLIENT = ZeitgeistClient()
46
class GenericViewWidget(gtk.VBox):
48
"unfocus-day" : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
49
# Sends a list zeitgeist events
53
gtk.VBox.__init__(self)
55
self.connect("style-set", self.change_style)
57
def _set_date_strings(self):
58
self.date_string = date.fromtimestamp(self.day_start).strftime("%d %B")
59
self.year_string = date.fromtimestamp(self.day_start).strftime("%Y")
60
if time.time() < self.day_end and time.time() > self.day_start:
61
self.week_day_string = _("Today")
62
elif time.time() - 86400 < self.day_end and time.time() - 86400> self.day_start:
63
self.week_day_string = _("Yesterday")
65
self.week_day_string = date.fromtimestamp(self.day_start).strftime("%A")
66
self.emit("style-set", None)
68
def click(self, widget, event):
69
if event.button in (1, 3):
70
self.emit("unfocus-day")
72
def change_style(self, widget, style):
74
color = rc_style.bg[gtk.STATE_NORMAL]
75
color = shade_gdk_color(color, 102/100.0)
76
self.view.modify_bg(gtk.STATE_NORMAL, color)
77
self.view.modify_base(gtk.STATE_NORMAL, color)
80
class ThumbnailDayWidget(GenericViewWidget):
83
GenericViewWidget.__init__(self)
85
self.scrolledwindow = gtk.ScrolledWindow()
86
self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
87
self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
88
self.view = ThumbBox()
89
self.scrolledwindow.add_with_viewport(self.view)
90
self.scrolledwindow.get_children()[0].set_shadow_type(gtk.SHADOW_NONE)
91
self.pack_end(self.scrolledwindow)
93
def set_day(self, start, end):
95
self.day_start = start
98
if self.scrolledwindow != widget:
100
self._set_date_strings()
101
today = int(time.time() ) - 7*86400
105
if self.day_start < today:
106
self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string)
108
self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string)
109
self.daylabel.set_size_request(100, 60)
110
self.daylabel.connect("button-press-event", self.click)
111
self.daylabel.set_tooltip_text(_("Click to return to multiday view"))
112
self.pack_start(self.daylabel, False, False)
115
self.daylabel.show_all()
119
get_file_events(start*1000, (start + 12*hour -1) * 1000, self.set_morning_events)
120
get_file_events((start + 12*hour)*1000, (start + 18*hour - 1)*1000, self.set_afternoon_events)
121
get_file_events((start + 18*hour)*1000, end*1000, self.set_evening_events)
123
def set_morning_events(self, events):
125
timestamp = int(events[0].timestamp)
126
if self.day_start*1000 <= timestamp and timestamp < (self.day_start + 12*60*60)*1000:
127
self.view.set_morning_events(events)
128
self.view.views[0].show_all()
129
self.view.labels[0].show_all()
131
self.view.set_morning_events(events)
132
self.view.views[0].hide_all()
133
self.view.labels[0].hide_all()
135
def set_afternoon_events(self, events):
137
timestamp = int(events[0].timestamp)
138
if (self.day_start + 12*60*60)*1000 <= timestamp and timestamp < (self.day_start + 18*60*60)*1000:
139
self.view.set_afternoon_events(events)
140
self.view.views[1].show_all()
141
self.view.labels[1].show_all()
143
self.view.set_afternoon_events(events)
144
self.view.views[1].hide_all()
145
self.view.labels[1].hide_all()
147
def set_evening_events(self, events):
149
timestamp = int(events[0].timestamp)
150
if (self.day_start + 18*60*60)*1000 <= timestamp and timestamp < self.day_end*1000:
151
self.view.set_evening_events(events)
152
self.view.views[2].show_all()
153
self.view.labels[2].show_all()
155
self.view.set_evening_events(events)
156
self.view.views[2].hide_all()
157
self.view.labels[2].hide_all()
160
class SingleDayWidget(GenericViewWidget):
163
GenericViewWidget.__init__(self)
164
self.ruler = TimelineHeader()
165
self.scrolledwindow = gtk.ScrolledWindow()
166
self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE)
167
self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
168
self.view = TimelineView()
169
self.scrolledwindow.add(self.view)
170
self.pack_end(self.scrolledwindow)
171
self.pack_end(self.ruler, False, False)
172
self.view.set_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK)
174
def set_day(self, start, end):
175
self.day_start = start
178
if widget not in (self.ruler, self.scrolledwindow):
180
self._set_date_strings()
181
today = int(time.time() ) - 7*86400
185
if self.day_start < today:
186
self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string)
188
self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string)
189
self.daylabel.set_size_request(100, 60)
190
self.daylabel.connect("button-press-event", self.click)
191
self.daylabel.set_tooltip_text(_("Click to return to multiday view"))
193
self.pack_start(self.daylabel, False, False)
194
get_dayevents(start*1000, end*1000, 1, self.view.set_model_from_list)
197
def change_style(self, widget, style):
198
GenericViewWidget.change_style(self, widget, style)
199
rc_style = self.style
200
color = rc_style.bg[gtk.STATE_NORMAL]
201
color = shade_gdk_color(color, 102/100.0)
202
self.ruler.modify_bg(gtk.STATE_NORMAL, color)
205
class DayWidget(gtk.VBox):
208
"focus-day" : (gobject.SIGNAL_RUN_FIRST,
213
def __init__(self, start, end):
214
super(DayWidget, self).__init__()
216
self.day_start = start
219
self._set_date_strings()
221
(_("Morning"), start, start + 12*hour - 1),
222
(_("Afternoon"), start + 12*hour, start + 18*hour - 1),
223
(_("Evening"), start + 18*hour, end),
228
gobject.timeout_add_seconds(
229
86400 - (int(time.time() - time.timezone) % 86400), self._refresh)
238
def _set_date_strings(self):
239
self.date_string = date.fromtimestamp(self.day_start).strftime("%d %B")
240
self.year_string = date.fromtimestamp(self.day_start).strftime("%Y")
241
if time.time() < self.day_end and time.time() > self.day_start:
242
self.week_day_string = _("Today")
243
elif time.time() - 86400 < self.day_end and time.time() - 86400> self.day_start:
244
self.week_day_string = _("Yesterday")
246
self.week_day_string = date.fromtimestamp(self.day_start).strftime("%A")
247
self.emit("style-set", None)
250
self._init_date_label()
254
def _init_pinbox(self):
255
if self.day_start <= time.time() < self.day_end:
256
self.view.pack_start(pinbox, False, False)
258
def _init_widgets(self):
259
self.vbox = gtk.VBox()
260
self.pack_start(self.vbox)
264
self._init_date_label()
266
#label.modify_bg(gtk.STATE_SELECTED, style.bg[gtk.STATE_SELECTED])
268
self.view = gtk.VBox()
269
scroll = gtk.ScrolledWindow()
270
scroll.set_shadow_type(gtk.SHADOW_NONE)
272
evbox2 = gtk.EventBox()
273
evbox2.add(self.view)
274
self.view.set_border_width(6)
276
scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
277
scroll.add_with_viewport(evbox2)
278
for w in scroll.get_children():
279
w.set_shadow_type(gtk.SHADOW_NONE)
280
self.vbox.pack_start(scroll)
283
def change_style(widget, style):
284
rc_style = self.style
285
color = rc_style.bg[gtk.STATE_NORMAL]
286
color = shade_gdk_color(color, 102/100.0)
287
evbox2.modify_bg(gtk.STATE_NORMAL, color)
289
self.connect("style-set", change_style)
291
def _init_date_label(self):
292
self._set_date_strings()
294
today = int(time.time() ) - 7*86400
298
if self.day_start < today:
299
self.daylabel = DayLabel(self.date_string, self.week_day_string+", "+ self.year_string)
301
self.daylabel = DayLabel(self.week_day_string, self.date_string+", "+ self.year_string)
302
self.daylabel.connect("button-press-event", self.click)
303
self.daylabel.set_tooltip_text(
304
_("Left click for a detailed timeline view")
306
_("Right click for a thumbnail view"))
307
self.daylabel.set_size_request(100, 60)
308
evbox = gtk.EventBox()
309
evbox.add(self.daylabel)
310
evbox.set_size_request(100, 60)
311
self.vbox.pack_start(evbox, False, False)
312
def cursor_func(x, y):
314
evbox.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
315
self.connect("motion-notify-event", cursor_func)
317
def change_style(widget, style):
318
rc_style = self.style
319
color = rc_style.bg[gtk.STATE_NORMAL]
320
evbox.modify_bg(gtk.STATE_NORMAL, color)
321
self.daylabel.modify_bg(gtk.STATE_NORMAL, color)
323
self.connect("style-set", change_style)
324
#self.connect("leave-notify-event", lambda x, y: evbox.window.set_cursor(None))
326
self.vbox.reorder_child(self.daylabel, 0)
328
def click(self, widget, event):
329
if event.button == 1:
330
self.emit("focus-day", 1)
331
elif event.button == 3:
332
self.emit("focus-day", 2)
334
def _init_events(self):
338
for period in self._periods:
339
part = DayPartWidget(period[0], period[1], period[2])
340
self.view.pack_start(part, False, False)
343
class CategoryBox(gtk.HBox):
345
def __init__(self, category, events, pinnable = False):
346
super(CategoryBox, self).__init__()
347
self.view = gtk.VBox(True)
348
self.vbox = gtk.VBox()
350
item = Item(event, pinnable)
352
#label = gtk.Label("")
353
#hbox.pack_start(label, False, False, 7)
354
hbox.pack_start(item, True, True, 0)
355
self.view.pack_start(hbox, False, False, 0)
359
# If this isn't a set of ungrouped events, give it a label
361
# Place the items into a box and simulate left padding
362
self.box = gtk.HBox()
363
#label = gtk.Label("")
364
self.box.pack_start(self.view)
367
# Add the title button
368
if category in SUPPORTED_SOURCES:
369
text = SUPPORTED_SOURCES[category].group_label(len(events))
374
label.set_markup("<span>%s</span>" % text)
375
#label.set_ellipsize(pango.ELLIPSIZE_END)
377
hbox.pack_start(label, True, True, 0)
380
label.set_markup("<span>(%d)</span>" % len(events))
381
label.set_alignment(1.0,0.5)
382
label.set_alignment(1.0,0.5)
383
hbox.pack_end(label, False, False, 2)
385
hbox.set_border_width(3)
387
self.expander = gtk.Expander()
388
self.expander.set_label_widget(hbox)
390
self.vbox.pack_start(self.expander, False, False)
391
self.expander.add(self.box)#
393
self.pack_start(self.vbox, True, True, 24)
395
self.expander.show_all()
403
self.vbox.pack_end(self.box)
407
self.pack_start(self.vbox, True, True, 16)
411
def on_toggle(self, view, bool):
419
class DayLabel(gtk.DrawingArea):
422
gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK |
423
gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_MOTION_MASK |
424
gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK |
425
gtk.gdk.BUTTON_PRESS_MASK
428
def __init__(self, day, date):
429
if day == _("Today"):
433
super(DayLabel, self).__init__()
436
self.set_events(self._events)
437
self.connect("expose_event", self.expose)
438
self.connect("enter-notify-event", self._on_enter)
439
self.connect("leave-notify-event", self._on_leave)
441
def _on_enter(self, widget, event):
442
widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
444
def _on_leave(self, widget, event):
445
widget.window.set_cursor(None)
447
def expose(self, widget, event):
448
context = widget.window.cairo_create()
449
self.context = context
451
bg = self.style.bg[0]
452
red, green, blue = bg.red/65535.0, bg.green/65535.0, bg.blue/65535.0
453
self.font_name = self.style.font_desc.get_family()
455
widget.style.set_background(widget.window, gtk.STATE_NORMAL)
457
# set a clip region for the expose event
458
context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
460
self.draw(widget, event, context)
461
self.day_text(widget, event, context)
464
def day_text(self, widget, event, context):
465
actual_y = self.get_size_request()[1]
466
if actual_y > event.area.height:
469
y = event.area.height
471
gc = self.style.fg_gc[gtk.STATE_SELECTED if self.leading else gtk.STATE_NORMAL]
472
layout = widget.create_pango_layout(self.day)
473
layout.set_font_description(pango.FontDescription(self.font_name + " Bold 15"))
474
w, h = layout.get_pixel_size()
475
widget.window.draw_layout(gc, (x-w)/2, (y)/2 - h + 5, layout)
476
self.date_text(widget, event, context, (y)/2 + 5)
478
def date_text(self, widget, event, context, lastfontheight):
479
gc = self.style.fg_gc[gtk.STATE_SELECTED if self.leading else gtk.STATE_INSENSITIVE]
480
layout = widget.create_pango_layout(self.date)
481
layout.set_font_description(pango.FontDescription(self.font_name + " 10"))
482
w, h = layout.get_pixel_size()
483
widget.window.draw_layout(gc, (event.area.width-w)/2, lastfontheight, layout)
485
def draw(self, widget, event, context):
487
bg = self.style.bg[gtk.STATE_SELECTED]
488
red, green, blue = bg.red/65535.0, bg.green/65535.0, bg.blue/65535.0
490
bg = self.style.bg[gtk.STATE_NORMAL]
491
red = (bg.red * 125 / 100)/65535.0
492
green = (bg.green * 125 / 100)/65535.0
493
blue = (bg.blue * 125 / 100)/65535.0
496
w, h = event.area.width, event.area.height
497
context.set_source_rgba(red, green, blue, 1)
498
context.new_sub_path()
499
context.arc(r+x, r+y, r, math.pi, 3 * math.pi /2)
500
context.arc(w-r, r+y, r, 3 * math.pi / 2, 0)
502
context.rectangle(0, r, w, h)
503
context.fill_preserve()
506
class DayButton(gtk.DrawingArea):
512
bg_color = (0, 0, 0, 0)
513
header_color = (1, 1, 1, 1)
514
leading_header_color = (1, 1, 1, 1)
515
internal_color = (0, 1, 0, 1)
516
arrow_color = (1,1,1,1)
517
arrow_color_selected = (1, 1, 1, 1)
520
"clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()),
523
gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK |
524
gtk.gdk.KEY_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.BUTTON_PRESS_MASK |
525
gtk.gdk.MOTION_NOTIFY | gtk.gdk.POINTER_MOTION_MASK
527
def __init__(self, side = 0, leading = False):
528
super(DayButton, self).__init__()
529
self.set_events(self._events)
530
self.set_flags(gtk.CAN_FOCUS)
531
self.leading = leading
533
self.connect("button_press_event", self.on_press)
534
self.connect("button_release_event", self.clicked_sender)
535
self.connect("key_press_event", self.keyboard_clicked_sender)
536
self.connect("motion_notify_event", self.on_hover)
537
self.connect("leave_notify_event", self._enter_leave_notify, False)
538
self.connect("expose_event", self.expose)
539
self.connect("style-set", self.change_style)
540
self.set_size_request(20, -1)
542
def set_sensitive(self, case):
543
self.sensitive = case
546
def _enter_leave_notify(self, widget, event, bol):
550
def on_hover(self, widget, event):
551
if event.y > self.header_size:
561
def on_press(self, widget, event):
562
if event.y > self.header_size:
566
def keyboard_clicked_sender(self, widget, event):
567
if event.keyval in (gtk.keysyms.Return, gtk.keysyms.space):
575
def clicked_sender(self, widget, event):
576
if event.y > self.header_size:
583
def change_style(self, *args, **kwargs):
584
self.bg_color = get_gtk_rgba(self.style, "bg", 0)
585
self.header_color = get_gtk_rgba(self.style, "bg", 0, 1.25)
586
self.leading_header_color = get_gtk_rgba(self.style, "bg", 3)
587
self.internal_color = get_gtk_rgba(self.style, "bg", 0, 1.02)
588
self.arrow_color = get_gtk_rgba(self.style, "text", 0, 0.6)
589
self.arrow_color_selected = get_gtk_rgba(self.style, "bg", 3)
590
self.arrow_color_insensitive = get_gtk_rgba(self.style, "text", 4)
592
def expose(self, widget, event):
593
context = widget.window.cairo_create()
595
context.set_source_rgba(*self.bg_color)
596
context.set_operator(cairo.OPERATOR_SOURCE)
598
context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
603
w, h = event.area.width, event.area.height
606
context.set_source_rgba(*(self.leading_header_color if self.leading else self.header_color))
607
context.new_sub_path()
608
context.move_to(x+r,y)
609
context.line_to(x+w-r,y)
610
context.curve_to(x+w,y,x+w,y,x+w,y+r)
611
context.line_to(x+w,y+h-r)
612
context.curve_to(x+w,y+h,x+w,y+h,x+w-r,y+h)
613
context.line_to(x+r,y+h)
614
context.curve_to(x,y+h,x,y+h,x,y+h-r)
615
context.line_to(x,y+r)
616
context.curve_to(x,y,x,y,x+r,y)
617
context.set_source_rgba(*(self.leading_header_color if self.leading else self.header_color))
619
context.rectangle(0, r, w, self.header_size)
621
context.set_source_rgba(*self.internal_color)
622
context.rectangle(0, self.header_size, w, h)
625
widget.style.paint_box(widget.window, gtk.STATE_PRELIGHT, gtk.SHADOW_OUT,
626
event.area, widget, "button",
627
event.area.x, self.header_size,
628
w, h-self.header_size)
630
if not self.sensitive:
631
state = gtk.STATE_INSENSITIVE
632
elif self.is_focus() or self.pressed:
633
widget.style.paint_focus(widget.window, gtk.STATE_ACTIVE, event.area,
634
widget, None, event.area.x, self.header_size,
635
w, h-self.header_size)
636
state = gtk.STATE_SELECTED
638
state = gtk.STATE_NORMAL
639
arrow = gtk.ARROW_RIGHT if self.side else gtk.ARROW_LEFT
640
self.style.paint_arrow(widget.window, state, gtk.SHADOW_NONE, None,
641
self, "arrow", arrow, True,
642
w/2-size/2, h/2 + size/2, size, size)
645
class EventGroup(gtk.VBox):
647
def __init__(self, title):
648
super(EventGroup, self).__init__()
650
# Create the title label
651
self.label = gtk.Label(title)
652
self.label.set_alignment(0.03, 0.5)
653
self.pack_start(self.label, False, False, 6)
655
# Create the main container
656
self.view = gtk.VBox()
657
self.pack_start(self.view)
659
# Connect to relevant signals
660
self.connect("style-set", self.on_style_change)
662
# Populate the widget with content
665
def on_style_change(self, widget, style):
666
""" Update used colors according to the system theme. """
667
color = self.style.bg[gtk.STATE_NORMAL]
668
fcolor = self.style.fg[gtk.STATE_NORMAL]
669
color = combine_gdk_color(color, fcolor)
670
self.label.modify_fg(gtk.STATE_NORMAL, color)
673
def event_exists(uri):
674
# TODO: Move this into Zeitgeist's datamodel.py
675
return not uri.startswith("file://") or os.path.exists(
676
urllib.unquote(str(uri[7:])))
678
def set_events(self, events):
680
for widget in self.view:
681
self.view.remove(widget)
684
box = CategoryBox(None, events, True)
685
self.view.pack_start(box)
689
subject = event.subjects[0]
690
if self.event_exists(subject.uri):
691
if not categories.has_key(subject.interpretation):
692
categories[subject.interpretation] = []
693
categories[subject.interpretation].append(event)
694
self.events.append(event)
699
# Make the group title, etc. visible
702
ungrouped_events = []
703
for key in sorted(categories.iterkeys()):
704
events = categories[key]
706
box = CategoryBox(key, list(reversed(events)))
707
self.view.pack_start(box)
709
ungrouped_events += events
711
ungrouped_events.sort(key=lambda x: x.timestamp)
712
box = CategoryBox(None, ungrouped_events)
713
self.view.pack_start(box)
715
# Make the group's contents visible
719
if len(self.events) == 0:
724
def get_events(self, *discard):
725
if self.event_templates and len(self.event_templates) > 0:
726
CLIENT.find_events_for_templates(self.event_templates,
727
self.set_events, self.event_timerange, num_events=50000,
728
result_type=ResultType.MostRecentSubjects)
733
class DayPartWidget(EventGroup):
735
def __init__(self, title, start, end):
736
# Setup event criteria for querying
737
self.event_timerange = [start * 1000, end * 1000]
738
self.event_templates = (
739
Event.new_for_values(interpretation=Interpretation.VISIT_EVENT.uri),
740
Event.new_for_values(interpretation=Interpretation.MODIFY_EVENT.uri),
741
Event.new_for_values(interpretation=Interpretation.CREATE_EVENT.uri),
742
Event.new_for_values(interpretation=Interpretation.OPEN_EVENT.uri),
745
# Initialize the widget
746
super(DayPartWidget, self).__init__(title)
748
# FIXME: Move this into EventGroup
749
CLIENT.install_monitor(self.event_timerange, self.event_templates,
750
self.notify_insert_handler, self.notify_delete_handler)
752
def notify_insert_handler(self, time_range, events):
753
# FIXME: Don't regenerate everything, we already get the
754
# information we need
757
def notify_delete_handler(self, time_range, event_ids):
758
# FIXME: Same as above
761
class PinBox(EventGroup):
764
# Setup event criteria for querying
765
self.event_timerange = TimeRange.until_now()
767
# Initialize the widget
768
super(PinBox, self).__init__(_("Pinned items"))
770
# Connect to relevant signals
771
bookmarker.connect("reload", self.get_events)
774
def event_templates(self):
775
if not bookmarker.bookmarks:
776
# Abort, or we will query with no templates and get lots of
781
for bookmark in bookmarker.bookmarks:
782
templates.append(Event.new_for_values(subject_uri=bookmark))
785
def set_events(self, *args, **kwargs):
786
super(PinBox, self).set_events(*args, **kwargs)
787
# Make the pin icons visible