1
# -.- coding: utf-8 -.-
3
# GNOME Activity Journal
5
# Copyright © 2009-2010 Seif Lotfy <seif@lotfy.com>
6
# Copyright © 2010 Siegfried Gevatter <siegfried@gevatter.com>
7
# Copyright © 2010 Markus Korn <thekorn@gmx.de>
8
# Copyright © 2010 Randal Barlow <email.tehk@gmail.com>
10
# This program is free software: you can redistribute it and/or modify
11
# it under the terms of the GNU General Public License as published by
12
# the Free Software Foundation, either version 3 of the License, or
13
# (at your option) any later version.
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
# GNU General Public License for more details.
20
# You should have received a copy of the GNU General Public License
21
# along with this program. If not, see <http://www.gnu.org/licenses/>.
23
from __future__ import with_statement
32
from dbus.exceptions import DBusException
38
from zeitgeist.client import ZeitgeistClient
39
from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation, \
42
from common import shade_gdk_color, combine_gdk_color, is_command_available, \
44
from config import BASE_PATH, VERSION, settings, get_icon_path
45
from sources import Source, SUPPORTED_SOURCES
46
from gio_file import GioFile, SIZE_NORMAL, SIZE_LARGE
47
from bookmarker import bookmarker
49
from tracker_wrapper import tracker
51
print "Tracker disabled."
53
import content_objects
56
CLIENT = ZeitgeistClient()
59
class SearchBox(gtk.EventBox):
62
"clear" : (gobject.SIGNAL_RUN_FIRST,
65
"search" : (gobject.SIGNAL_RUN_FIRST,
67
(gobject.TYPE_PYOBJECT,))
71
gtk.EventBox.__init__(self)
75
self.set_border_width(3)
76
self.hbox = gtk.HBox()
81
self.search = SearchEntry()
83
self.hbox.pack_start(self.search)
84
self.hbox.set_border_width(6)
88
for source in SUPPORTED_SOURCES.keys():
89
s = SUPPORTED_SOURCES[source]._desc_pl
90
self.category[s] = source
95
def change_style(widget, style):
98
color = rc_style.bg[gtk.STATE_NORMAL]
99
color = shade_gdk_color(color, 102/100.0)
100
self.modify_bg(gtk.STATE_NORMAL, color)
102
color = rc_style.bg[gtk.STATE_NORMAL]
103
fcolor = rc_style.fg[gtk.STATE_NORMAL]
104
color = combine_gdk_color(color, fcolor)
106
self.search.modify_text(gtk.STATE_NORMAL, color)
108
self.hbox.connect("style-set", change_style)
109
self.search.connect("search", self.set_search)
110
self.search.connect("clear", self.clear)
112
def clear(self, widget):
113
if self.text.strip() != "" and self.text.strip() != self.search.default_text:
118
def _init_combobox(self):
120
self.clearbtn = gtk.Button()
122
#label.set_markup("<span><b>X</b></span>")
124
img = gtk.image_new_from_stock("gtk-close", gtk.ICON_SIZE_MENU)
125
self.clearbtn.add(img)
126
self.clearbtn.set_focus_on_click(False)
127
self.clearbtn.set_relief(gtk.RELIEF_NONE)
128
self.hbox.pack_end(self.clearbtn, False, False)
129
self.clearbtn.connect("clicked", lambda button: self.hide())
130
self.clearbtn.connect("clicked", lambda button: self.search.set_text(""))
132
self.combobox = gtk.combo_box_new_text()
133
self.combobox.set_focus_on_click(False)
134
self.hbox.pack_end(self.combobox, False, False, 6)
135
self.combobox.append_text("All activities")
136
self.combobox.set_active(0)
137
for cat in self.category.keys():
138
self.combobox.append_text(cat)
140
def set_search(self, widget, text=None):
141
if not self.text.strip() == text.strip():
143
def callback(results):
144
self.results = [s[1] for s in results]
145
self.emit("search", results)
148
text = self.search.get_text()
149
if text == self.search.default_text or text.strip() == "":
152
cat = self.combobox.get_active()
154
interpretation = None
156
cat = self.category[self.combobox.get_active_text()]
157
interpretation = self.category[self.combobox.get_active_text()]
158
if "tracker" in globals().keys():
159
tracker.search(text, interpretation, callback)
161
class SearchEntry(gtk.Entry):
164
"clear" : (gobject.SIGNAL_RUN_FIRST,
167
"search" : (gobject.SIGNAL_RUN_FIRST,
169
(gobject.TYPE_STRING,))
172
default_text = _("Type here to search...")
174
# The font style of the text in the entry.
177
# TODO: What is this?
180
def __init__(self, accel_group = None):
181
gtk.Entry.__init__(self)
183
self.set_width_chars(30)
184
self.set_text(self.default_text)
185
self.set_size_request(-1, 32)
186
self.connect("changed", lambda w: self._queue_search())
187
self.connect("focus-in-event", self._entry_focus_in)
188
self.connect("focus-out-event", self._entry_focus_out)
189
#self.connect("icon-press", self._icon_press)
192
def _icon_press(self, widget, pos, event):
193
# Note: GTK_ENTRY_ICON_SECONDARY does not seem to be bound in PyGTK.
194
if int(pos) == 1 and not self.get_text() == self.default_text:
195
self._entry_clear_no_change_handler()
197
def _entry_focus_in(self, widget, x):
198
if self.get_text() == self.default_text:
200
#self.modify_font(self.font_style)
202
def _entry_focus_out(self, widget, x):
203
if self.get_text() == "":
204
self.set_text(self.default_text)
205
#self.modify_font(self.font_style)
207
def _entry_clear_no_change_handler(self):
208
if not self.get_text() == self.default_text:
211
def _queue_search(self):
212
if self.search_timeout != 0:
213
gobject.source_remove(self.search_timeout)
214
self.search_timeout = 0
216
if self.get_text() == self.default_text or len(self.get_text()) == 0:
219
self.search_timeout = gobject.timeout_add(200, self._typing_timeout)
221
def _typing_timeout(self):
222
if len(self.get_text()) > 0:
223
self.emit("search", self.get_text())
225
self.search_timeout = 0
229
class PreviewTooltip(gtk.Window):
231
# per default we are using thumbs at a size of 128 * 128 px
232
# in tooltips. For preview of text files we are using 256 * 256 px
233
# which is dynamically defined in StaticPreviewTooltip.preview()
234
TOOLTIP_SIZE = SIZE_NORMAL
237
gtk.Window.__init__(self, type=gtk.WINDOW_POPUP)
239
def preview(self, gio_file):
242
class StaticPreviewTooltip(PreviewTooltip):
245
super(StaticPreviewTooltip, self).__init__()
246
self.__current = None
247
self.__monitor = None
249
def replace_content(self, content):
250
children = self.get_children()
252
self.remove(children[0])
253
# hack to force the tooltip to have the exact same size
258
def preview(self, gio_file):
259
if gio_file == self.__current:
260
return bool(self.__current)
261
if self.__monitor is not None:
262
self.__monitor.cancel()
263
self.__current = gio_file
264
self.__monitor = gio_file.get_monitor()
265
self.__monitor.connect("changed", self._do_update_preview)
266
# for text previews we are always using SIZE_LARGE
267
if "text-x-generic" in gio_file.icon_names or "text-x-script" in gio_file.icon_names:
270
size = self.TOOLTIP_SIZE
271
if not isinstance(gio_file, GioFile): return False
272
pixbuf = gio_file.get_thumbnail(size=size, border=1)
274
self.__current = None
276
img = gtk.image_new_from_pixbuf(pixbuf)
277
img.set_alignment(0.5, 0.5)
279
self.replace_content(img)
283
def _do_update_preview(self, monitor, file, other_file, event_type):
284
if event_type == gio.FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
285
if self.__current is not None:
286
self.__current.refresh()
287
self.__current = None
288
gtk.tooltip_trigger_tooltip_query(gtk.gdk.display_get_default())
290
class VideoPreviewTooltip(PreviewTooltip):
293
PreviewTooltip.__init__(self)
295
self.movie_window = gtk.DrawingArea()
296
hbox.pack_start(self.movie_window)
298
self.player = gst.element_factory_make("playbin", "player")
299
bus = self.player.get_bus()
300
bus.add_signal_watch()
301
bus.enable_sync_message_emission()
302
bus.connect("message", self.on_message)
303
bus.connect("sync-message::element", self.on_sync_message)
304
self.connect("hide", self._handle_hide)
305
self.connect("show", self._handle_show)
306
self.set_default_size(*self.TOOLTIP_SIZE)
308
def _handle_hide(self, widget):
309
self.player.set_state(gst.STATE_NULL)
311
def _handle_show(self, widget):
312
self.player.set_state(gst.STATE_PLAYING)
314
def preview(self, gio_file):
315
if gio_file.uri == self.player.get_property("uri"):
317
self.player.set_property("uri", gio_file.uri)
320
def on_message(self, bus, message):
322
if t == gst.MESSAGE_EOS:
323
self.player.set_state(gst.STATE_NULL)
325
elif t == gst.MESSAGE_ERROR:
326
self.player.set_state(gst.STATE_NULL)
327
err, debug = message.parse_error()
328
print "Error: %s" % err, debug
330
def on_sync_message(self, bus, message):
331
if message.structure is None:
333
message_name = message.structure.get_name()
334
if message_name == "prepare-xwindow-id":
335
imagesink = message.src
336
imagesink.set_property("force-aspect-ratio", True)
337
gtk.gdk.threads_enter()
340
imagesink.set_xwindow_id(self.movie_window.window.xid)
342
gtk.gdk.threads_leave()
344
class Item(gtk.HBox):
346
def __init__(self, event, allow_pin = False):
348
gtk.HBox.__init__(self)
349
self.set_border_width(2)
350
self.allow_pin = allow_pin
351
self.btn = gtk.Button()
352
self.search_results = []
353
self.in_search = False
354
self.subject = event.subjects[0]
355
self.content_obj = content_objects.choose_content_object(event)
356
# self.content_obj = GioFile.create(self.subject.uri)
357
self.time = float(event.timestamp) / 1000
358
self.time = time.strftime("%H:%M", time.localtime(self.time))
360
if self.content_obj is not None:
361
self.icon = self.content_obj.get_icon(
362
can_thumb=settings.get('small_thumbnails', False), border=0)
365
self.btn.set_relief(gtk.RELIEF_NONE)
366
self.btn.set_focus_on_click(False)
374
if self.search_results != searchbox.results:
375
self.search_results = searchbox.results
376
rc_style = self.style
377
text = self.content_obj.text.replace("&", "&")
378
if self.subject.uri in searchbox.results:
379
self.label.set_markup("<span><b>"+text+"</b></span>")
380
self.in_search = True
381
color = rc_style.base[gtk.STATE_SELECTED]
382
self.label.modify_fg(gtk.STATE_NORMAL, color)
384
self.label.set_markup("<span>"+text+"</span>")
385
self.in_search = False
386
color = rc_style.text[gtk.STATE_NORMAL]
387
self.label.modify_fg(gtk.STATE_NORMAL, color)
389
def __init_widget(self):
390
self.label = gtk.Label()
391
text = self.content_obj.text.replace("&", "&")
392
self.label.set_markup(text)
393
self.label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
394
self.label.set_alignment(0.0, 0.5)
396
if self.icon: img = gtk.image_new_from_pixbuf(self.icon)
399
if img: hbox.pack_start(img, False, False, 1)
400
hbox.pack_start(self.label, True, True, 4)
403
# TODO: get the name "pin" from theme when icons are properly installed
404
img = gtk.image_new_from_file(get_icon_path("hicolor/24x24/status/pin.png"))
405
self.pin = gtk.Button()
407
self.pin.set_tooltip_text(_("Remove Pin"))
408
self.pin.set_focus_on_click(False)
409
self.pin.set_relief(gtk.RELIEF_NONE)
410
self.pack_end(self.pin, False, False)
411
self.pin.connect("clicked", lambda x: self.set_bookmarked(False))
412
#hbox.pack_end(img, False, False)
413
evbox = gtk.EventBox()
416
self.pack_start(evbox)
418
self.btn.connect("clicked", self.launch)
419
self.btn.connect("button_press_event", self._show_item_popup)
421
def realize_cb(widget):
422
evbox.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
424
self.btn.connect("realize", realize_cb)
427
def change_style(widget, style):
428
rc_style = self.style
429
color = rc_style.bg[gtk.STATE_NORMAL]
430
fcolor = rc_style.fg[gtk.STATE_NORMAL]
431
color = combine_gdk_color(color, fcolor)
434
color = rc_style.bg[gtk.STATE_SELECTED]
435
self.label.modify_text(gtk.STATE_NORMAL, color)
437
color = rc_style.text[gtk.STATE_NORMAL]
438
self.label.modify_text(gtk.STATE_NORMAL, color)
441
color = rc_style.bg[gtk.STATE_NORMAL]
442
color = shade_gdk_color(color, 102/100.0)
443
evbox.modify_bg(gtk.STATE_NORMAL, color)
445
self.connect("style-set", change_style)
447
self.init_multimedia_tooltip()
449
def init_multimedia_tooltip(self):
450
"""add multimedia tooltip to multimedia files
451
multimedia tooltip is shown for all images, all videos and pdfs
453
TODO: make loading of multimedia thumbs async
455
if isinstance(self.content_obj, GioFile) and self.content_obj.has_preview():
456
icon_names = self.content_obj.icon_names
457
self.set_property("has-tooltip", True)
458
self.connect("query-tooltip", self._handle_tooltip)
459
if "video-x-generic" in icon_names and gst is not None:
460
self.set_tooltip_window(VideoPreviewTooltip)
462
self.set_tooltip_window(StaticPreviewTooltip)
464
def _handle_tooltip(self, widget, x, y, keyboard_mode, tooltip):
465
# nothing to do here, we always show the multimedia tooltip
466
# if we like video/sound preview later on we can start them here
467
tooltip_window = self.get_tooltip_window()
468
return tooltip_window.preview(self.content_obj)
470
def _show_item_popup(self, widget, ev):
472
items = [self.content_obj]
473
ContextMenu.do_popup(ev.time, items)
475
def set_bookmarked(self, bool_):
476
uri = unicode(self.subject.uri)
478
bookmarker.bookmark(uri)
480
bookmarker.unbookmark(uri)
483
def launch(self, *discard):
484
if self.content_obj is not None:
485
self.content_obj.launch()
488
class AnimatedImage(gtk.Image):
493
def __init__(self, uri, speed = 0):
494
super(AnimatedImage, self).__init__()
495
if speed: self.speed = speed
497
for i in (6, 5, 4, 3, 2, 1, 0):
498
self.frames.append(gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path(uri % i), 16, 16))
499
self.set_from_pixbuf(self.frames[0])
505
self.set_from_pixbuf(self.frames[self.i % self.mod])
511
start the image's animation
513
if self.animating: gobject.source_remove(self.animating)
514
self.animating = gobject.timeout_add(self.speed, self.next)
518
stop the image's animation
520
if self.animating: gobject.source_remove(self.animating)
521
self.animating = None
524
def animate_for_seconds(self, seconds):
526
:param seconds: int seconds for the amount of time when you want
530
gobject.timeout_add_seconds(seconds, self.stop)
533
class AboutDialog(gtk.AboutDialog):
534
name = "Activity Journal"
536
"Seif Lotfy <seif@lotfy.com>",
537
"Randal Barlow <email.tehk@gmail.com>",
538
"Siegfried-Angel Gevatter <siegfried@gevatter.com>",
539
"Peter Lund <peterfirefly@gmail.com>",
540
"Hylke Bons <hylkebons@gmail.com>",
541
"Markus Korn <thekorn@gmx.de>",
542
"Mikkel Kamstrup <mikkel.kamstrup@gmail.com>"
545
"Hylke Bons <hylkebons@gmail.com>",
546
"Thorsten Prante <thorsten@prante.eu>"
548
copyright_ = "Copyright © 2009-2010 Activity Journal authors"
549
comment = "A viewport into the past powered by Zeitgeist"
552
super(AboutDialog, self).__init__()
553
self.set_name(self.name)
554
self.set_version(self.version)
555
self.set_comments(self.comment)
556
self.set_copyright(self.copyright_)
557
self.set_authors(self.authors)
558
self.set_artists(self.artists)
561
for name in ("/usr/share/common-licenses/GPL",
562
os.path.join(BASE_PATH, "COPYING")):
563
if os.path.isfile(name):
564
with open(name) as licensefile:
565
license = licensefile.read()
568
license = "GNU General Public License, version 3 or later."
570
self.set_license(license)
571
#self.set_logo_icon_name("gnome-activity-journal")
572
self.set_logo(gtk.gdk.pixbuf_new_from_file_at_size(get_icon_path(
573
"hicolor/scalable/apps/gnome-activity-journal.svg"), 48, 48))
576
class ContextMenu(gtk.Menu):
577
subjects = []# A list of Zeitgeist event uris
578
informationwindow = None
580
super(ContextMenu, self).__init__()
582
"open" : gtk.ImageMenuItem(gtk.STOCK_OPEN),
583
"unpin" : gtk.MenuItem(_("Remove Pin")),
584
"pin" : gtk.MenuItem(_("Pin to Today")),
585
"delete" : gtk.MenuItem(_("Delete item from Journal")),
586
"delete_uri" : gtk.MenuItem(_("Delete all events with this URI")),
587
"info" : gtk.MenuItem(_("More Information")),
590
"open" : self.do_open,
591
"unpin" : self.do_unbookmark,
592
"pin" : self.do_bookmark,
593
"delete" : self.do_delete,
594
"delete_uri" : self.do_delete_events_with_shared_uri,
595
"info" : self.do_show_info,
597
names = ["open", "unpin", "pin", "delete", "delete_uri", "info"]
598
if is_command_available("nautilus-sendto"):
599
self.menuitems["sendto"] = gtk.MenuItem(_("Send To..."))
600
callbacks["sendto"] = self.do_send_to
601
names.append("sendto")
603
item = self.menuitems[name]
605
item.connect("activate", callbacks[name])
608
def do_popup(self, time, subjects):
610
Call this method to popup the context menu
612
:param time: the event time from the button press event
613
:param subjects: a list of uris
615
self.subjects = subjects
616
if len(subjects) == 1:
618
if bookmarker.is_bookmarked(uri):
619
self.menuitems["pin"].hide()
620
self.menuitems["unpin"].show()
622
self.menuitems["pin"].show()
623
self.menuitems["unpin"].hide()
625
self.popup(None, None, None, 3, time)
627
def do_open(self, menuitem):
628
for obj in self.subjects:
631
def do_show_info(self, menuitem):
632
if self.subjects and self.informationwindow:
633
self.informationwindow.set_content_object(self.subjects[0])
635
def do_bookmark(self, menuitem):
636
for obj in self.subjects:
639
isbookmarked = bookmarker.is_bookmarked(uri)
641
bookmarker.bookmark(uri)
643
def do_unbookmark(self, menuitem):
644
for obj in self.subjects:
647
isbookmarked = bookmarker.is_bookmarked(uri)
649
bookmarker.unbookmark(uri)
651
def do_delete(self, menuitem):
652
for obj in self.subjects:
653
CLIENT.delete_events([obj.event.id])
655
def do_delete_events_with_shared_uri(self, menuitem):
656
for uri in map(lambda obj: obj.uri, self.subjects):
657
CLIENT.find_event_ids_for_template(
658
Event.new_for_values(subject_uri=uri),
659
lambda ids: CLIENT.delete_events(map(int, ids)))
662
def do_send_to(self, menuitem):
663
launch_command("nautilus-sendto", map(lambda obj: obj.uri, self.subjects))
666
searchbox = SearchBox()
668
VideoPreviewTooltip = VideoPreviewTooltip()
670
VideoPreviewTooltip = None
671
StaticPreviewTooltip = StaticPreviewTooltip()
672
ContextMenu = ContextMenu()