#!/usr/bin/env python
import os
import os.path
import sys
import gtk
import pango
import gconf
import dbus
import md5
import urllib
from getopt import getopt
SERVICES = [
( 'Files', 'system-file-manager' ),
( 'Folders', 'folder' ),
( 'Applications', 'application-x-executable' ),
( 'Conversations', 'face-smile-big' ),
( 'Contacts', 'x-office-address-book' ),
( 'Development Files', 'applications-development' ),
( 'Documents', 'x-office-document' ),
( 'Emails', 'email' ),
( 'Images', 'image-x-generic' ),
( 'Music', 'audio-x-generic' ),
( 'Text Files', 'text-x-generic' ),
( 'Videos', 'video-x-generic' ),
]
DISPLAY_METADATA = {
'Files' : [ 'file.name', 'file.format', 'file.path', 'file.size', 'file.rank', ],
'Music' : [ 'audio.artist', 'audio.album', 'audio.trackno', 'audio.title', 'audio.codec' ],
'Documents' : [ 'doc.title', 'doc.author', 'doc.subject' ],
'Images' : [ 'image.title', 'image.height', 'image.width' ],
}
def FileSizeFormat(hbox, size):
label = gtk.Label()
size = int(size)
if size < 2**10:
label.set_text('%d bytes' % size)
elif size < 2**20:
label.set_text('%3.1f KB' % (size / 2.**10))
elif size < 2**30:
label.set_text('%3.1f MB' % (size / 2.**20))
elif size < 2**40:
label.set_text('%3.1f GB' % (size / 2.**30))
hbox.pack_start(label, expand=False, fill=False)
def FileRankFormat(hbox, rank):
if not rank:
rank='7'
i = 0
rank = float(rank) / 2.
while rank > 0.5:
hbox.pack_start(gtk.image_new_from_icon_name('weather-clear', gtk.ICON_SIZE_MENU), expand=False, fill=False)
i += 1
rank -= 1
if rank == 0.5:
hbox.pack_start(gtk.image_new_from_icon_name('weather-few-clouds', gtk.ICON_SIZE_MENU), expand=False, fill=False)
i += 1
while i < 5:
hbox.pack_start(gtk.image_new_from_icon_name('weather-overcast', gtk.ICON_SIZE_MENU), expand=False, fill=False)
i += 1
def ImageSizeFormat(hbox, size):
hbox.pack_start(gtk.Label(size + ' px'), expand=False, fill=False)
METADATA_LABELS = {
'audio.trackno':'Track #'
}
METADATA_FORMATTERS = {
'file.size':FileSizeFormat,
'file.rank':FileRankFormat,
'image.height':ImageSizeFormat,
'image.width':ImageSizeFormat,
}
class Tracker:
def __init__(self):
bus = dbus.SessionBus()
obj = bus.get_object('org.freedesktop.Tracker',
'/org/freedesktop/tracker')
self.tracker = dbus.Interface(obj, 'org.freedesktop.Tracker')
self.files = dbus.Interface(obj, 'org.freedesktop.Tracker.Files')
self.search = dbus.Interface(obj, 'org.freedesktop.Tracker.Search')
self.keywords = dbus.Interface(obj, 'org.freedesktop.Tracker.Keywords')
self.metadata = dbus.Interface(obj, 'org.freedesktop.Tracker.Metadata')
def __getattr__(self, attr):
return getattr(self.tracker, attr)
class TrackerGUI:
def __init__(self):
self.gconf = gconf.client_get_default()
self.icon_theme = gtk.icon_theme_get_default()
self.tracker = Tracker()
self.layout = 1
self.max_results = 20
opts,args = getopt(sys.argv[1:], '', ['layout=', 'max-hits='])
for a,v in opts:
if a == '--layout':
self.layout = int(v)
elif a == '--max-hits':
self.max_results = int(v)
self.window = gtk.Window()
self.window.set_title('Tracker')
main_vbox = gtk.VBox()
#main_vbox.set_border_width(12)
main_vbox.set_spacing(6)
hbox = gtk.HBox()
hbox.set_spacing(6)
hbox.set_border_width(12)
label = gtk.Label('_Search:')
hbox.pack_start(label, expand=False, fill=False)
self.search_entry = gtk.Entry()
hbox.pack_start(self.search_entry, expand=True, fill=True)
label.props.use_underline = True
label.set_mnemonic_widget(self.search_entry)
if self.layout == 1:
self.service_combobox = gtk.ComboBox()
store = gtk.ListStore(gtk.gdk.Pixbuf, str)
self.service_combobox.set_model(store)
cell = gtk.CellRendererPixbuf()
self.service_combobox.pack_start(cell, False)
self.service_combobox.add_attribute(cell, 'pixbuf', 0)
cell = gtk.CellRendererText()
self.service_combobox.pack_start(cell, False)
self.service_combobox.add_attribute(cell, 'text', 1)
for label, icon_name in SERVICES:
pixbuf = self.icon_theme.load_icon(icon_name,
gtk.ICON_SIZE_MENU, gtk.ICON_LOOKUP_USE_BUILTIN)
store.append([pixbuf, label])
self.service_combobox.set_active(0)
hbox.pack_start(self.service_combobox, expand=False, fill=True)
self.search_button = gtk.Button(stock='gtk-find')
self.search_button.set_data('offset', 0)
hbox.pack_start(self.search_button, expand=False, fill=False)
main_vbox.pack_start(hbox, expand=False, fill=False)
hbox = gtk.HBox()
if self.layout == 2:
self.service_treeview = gtk.TreeView()
store = gtk.ListStore(gtk.gdk.Pixbuf, str)
self.service_treeview.set_model(store)
#self.service_treeview.set_sensitive(False)
self.service_treeview.set_headers_visible(False)
self.service_treeview.set_row_separator_func(lambda model,iter: model[iter][1] == '')
cell = gtk.CellRendererPixbuf()
col = gtk.TreeViewColumn('', cell, pixbuf=0)
self.service_treeview.append_column(col)
cell = gtk.CellRendererText()
col = gtk.TreeViewColumn('', cell, text=1)
self.service_treeview.append_column(col)
for label, icon_name in SERVICES:
pixbuf = self.icon_theme.load_icon(icon_name,
gtk.ICON_SIZE_MENU, gtk.ICON_LOOKUP_USE_BUILTIN)
store.append([pixbuf, label])
store.append([None,''])
tags = self.tracker.keywords.GetList('Files')
for tag,count in tags.items():
store.append([None, tag])
selection = self.service_treeview.get_selection()
selection.select_iter(store.get_iter_first())
selection.connect('changed', self.on_service_selection_changed)
sw = gtk.ScrolledWindow()
sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
sw.set_shadow_type(gtk.SHADOW_IN)
sw.add(self.service_treeview)
hbox.pack_start(sw, expand=False, fill=True)
sw = gtk.ScrolledWindow()
sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
sw.set_shadow_type(gtk.SHADOW_IN)
if self.layout in (1,2,3,):
self.icon_view = gtk.IconView()
store = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
self.icon_view.set_model(store)
self.icon_view.set_pixbuf_column(0)
self.icon_view.set_text_column(1)
sw.add(self.icon_view)
elif self.layout == 4:
self.results_tree = gtk.TreeView()
store = gtk.TreeStore(gtk.gdk.Pixbuf, str, str)
self.results_tree.set_model(store)
self.results_tree.set_headers_visible(False)
cell = gtk.CellRendererPixbuf()
col = gtk.TreeViewColumn('', cell, pixbuf=0)
self.results_tree.append_column(col)
cell = gtk.CellRendererText()
col = gtk.TreeViewColumn('', cell, text=1)
self.results_tree.append_column(col)
self._fill_in_services(store)
sw.add(self.results_tree)
sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
if self.layout == 3:
self.notebook = gtk.Notebook()
label = gtk.Label('Files')
self.notebook.append_page(sw, label)
main_vbox.pack_start(self.notebook, expand=True, fill=True)
else:
self.result_label = gtk.Label('')
hb = gtk.HBox()
hb.set_border_width(6)
hb.pack_start(self.result_label, expand=False, fill=False)
vb = gtk.VBox()
vb.pack_start(hb, expand=False, fill=False)
vb.pack_start(sw, expand=True, fill=True)
hbox.pack_start(vb, expand=True, fill=True)
main_vbox.pack_start(hbox, expand=True, fill=True)
if self.layout != 4:
hbox = gtk.HBox()
hbox.set_spacing(6)
self.last_button = gtk.Button(stock='gtk-goto-last')
self.last_button.set_sensitive(False)
hbox.pack_end(self.last_button, expand=False, fill=False)
self.next_button = gtk.Button(stock='gtk-go-forward')
self.next_button.get_children()[0].get_children()[0].get_children()[1].set_text('Next')
self.next_button.set_sensitive(False)
hbox.pack_end(self.next_button, expand=False, fill=False)
image = gtk.image_new_from_stock('gtk-go-back', gtk.ICON_SIZE_BUTTON)
self.previous_button = gtk.Button()
self.previous_button.add(image)
self.previous_button.set_sensitive(False)
hbox.pack_end(self.previous_button, expand=False, fill=False)
image = gtk.image_new_from_stock('gtk-goto-first', gtk.ICON_SIZE_BUTTON)
self.first_button = gtk.Button()
self.first_button.add(image)
self.first_button.set_sensitive(False)
self.first_button.set_data('offset', 0)
hbox.pack_end(self.first_button, expand=False, fill=False)
main_vbox.pack_start(hbox, expand=False, fill=False)
self.bottom_pane = gtk.HBox()
self.bottom_pane_content = gtk.HBox()
self.bottom_pane.pack_start(self.bottom_pane_content, expand=True, fill=True)
self.hide_bottom_pane_button = gtk.Button()
image = gtk.image_new_from_stock('gtk-close', gtk.ICON_SIZE_MENU)
self.hide_bottom_pane_button.add(image)
self.hide_bottom_pane_button.set_relief(gtk.RELIEF_NONE)
vbox = gtk.VBox()
vbox.pack_start(self.hide_bottom_pane_button, expand=False, fill=False)
self.bottom_pane.pack_start(vbox, expand=False, fill=False)
vbox = gtk.VBox()
vbox.pack_start(main_vbox, expand=True, fill=True)
vbox.pack_start(self.bottom_pane, expand=False, fill=True)
self.statusbar = gtk.Statusbar()
vbox.pack_start(self.statusbar, expand=False, fill=False)
self.window.add(vbox)
self.search_entry.connect('activate', lambda e: self.search_button.emit('clicked'))
self.search_button.connect('clicked', self.get_search_stats)
if self.layout == 3:
self.search_button.connect('clicked', self.build_service_tabs)
if self.layout == 4:
self.search_button.connect('clicked', self.on_complete_search)
self.results_tree.get_selection().connect('changed', self.on_result_selected)
else:
self.icon_view.connect('selection-changed', self.on_result_selected)
self.search_button.connect('clicked', self.on_search)
self.first_button.connect('clicked', self.on_search)
self.next_button.connect('clicked', self.on_search)
self.previous_button.connect('clicked', self.on_search)
self.last_button.connect('clicked', self.on_search)
self.hide_bottom_pane_button.connect('clicked', lambda *e: self.bottom_pane.hide())
self.window.connect('destroy', gtk.main_quit)
self.window.resize(640, 480)
self.window.show_all()
self.result_label.hide()
def get_search_stats(self, button):
self.search_stats = { }
done = False
offset = 0
query = self.search_entry.get_text()
while not done:
if query.startswith('tag:'):
results = self.tracker.keywords.Search(-1, 'Files', [query[4:]], offset, 512)
else:
results = self.tracker.search.Text(-1, 'Files', query, offset, 512, False)
for r in results:
service = self.tracker.files.GetServiceType(r)
self.search_stats.setdefault(service, 0)
self.search_stats[service] += 1
offset += len(results)
done = (len(results) < 512)
self.search_stats['Files'] = offset
self.last_button.set_data('offset', offset - offset % self.max_results)
self.last_button.set_sensitive(True)
print self.search_stats
def on_search(self, button):
query = self.search_entry.get_text()
offset = button.get_data('offset')
if self.layout == 1:
service = SERVICES[self.service_combobox.get_active()][0]
elif self.layout == 2:
store, iter = self.service_treeview.get_selection().get_selected()
service = store[iter][1]
elif self.layout == 3:
service = self.notebook.get_tab_label(self.icon_view.get_parent()).get_text()
if query.startswith('tag:'):
results = self.tracker.keywords.Search(-1, service,
[ query[4:] ], offset, self.max_results + 1)
else:
results = self.tracker.search.Text(-1, service,
query, offset, self.max_results + 1, False)
self.result_label.set_markup('Showing results %d - %d of %d.'
% (offset + 1,
offset + min(len(results), self.max_results),
self.search_stats[service]))
self.result_label.show()
if self.layout in (1,2,3,):
store = self.icon_view.get_model()
store.clear()
for r in results[:self.max_results]:
basename = os.path.basename(r)
pixbuf = self._get_file_thumbnail_pixbuf(r) or self._get_mime_thumbnail(r)
store.append([ pixbuf, basename, r ])
self.icon_view.get_parent().get_vscrollbar().get_adjustment().value = 0
self.first_button.set_sensitive(True)
if offset > 0:
self.previous_button.set_sensitive(True)
self.previous_button.set_data('offset', offset - self.max_results)
else:
self.previous_button.set_sensitive(False)
if len(results) == self.max_results + 1:
self.next_button.set_sensitive(True)
self.next_button.set_data('offset', offset + self.max_results)
else:
self.next_button.set_sensitive(False)
def on_complete_search(self, button):
query = self.search_entry.get_text()
store = self.results_tree.get_model()
store.clear()
self._fill_in_services(store)
iter = store.get_iter_first()
while iter:
service = store[iter][1]
done = False
offset = 0
while not done:
if query.startswith('tag:'):
results = self.tracker.keywords.Search(-1, service,
[ query[4:] ], offset, self.max_results + 1)
else:
results = self.tracker.search.Text(-1, service, query, offset, 256, False)
offset += len(results)
for r in results:
pixbuf = self._get_file_thumbnail_pixbuf(r) or self._get_mime_thumbnail(r)
h = pixbuf.get_height()
w = pixbuf.get_width()
scale = max(h,w)
h = int(h * 24. / scale)
w = int(w * 24. / scale)
pixbuf = pixbuf.scale_simple(w, h, gtk.gdk.INTERP_BILINEAR)
store.append(iter, [pixbuf, r, r])
done = (len(results) < 256)
iter = store.iter_next(iter)
def on_service_selection_changed(self, selection):
self.on_search(self.search_button)
def _get_file_thumbnail_pixbuf(self, f):
uri = 'file://' + urllib.pathname2url(f)
thumb = md5.new()
thumb.update(uri)
thumb = os.path.expanduser('~/.thumbnails/normal/%s.png' % thumb.hexdigest())
try:
return gtk.gdk.pixbuf_new_from_file(thumb)
except:
return None
def _get_mime_thumbnail(self, f):
return self.icon_theme.load_icon('image-loading', 32, gtk.ICON_LOOKUP_USE_BUILTIN)
def _fill_in_services(self, store):
for label, icon_name in SERVICES:
pixbuf = self.icon_theme.load_icon(icon_name,
gtk.ICON_SIZE_MENU, gtk.ICON_LOOKUP_USE_BUILTIN)
store.append(None, [pixbuf, label, ''])
def build_service_tabs(self, button):
query = self.search_entry.get_text()
services = set()
done = False
offset = 0
while not done:
results = self.tracker.search.Text(-1, 'Files', query, offset, 256, False)
print len(results),'results.',results
for r in results:
print r, self.tracker.files.GetServiceType(r)
#services.add(self.tracker.files.GetServiceType(r))
offset += len(results)
done = (len(results) < 256)
print 'done:',done
print services
def on_result_selected(self, arg):
self._clear_container(self.bottom_pane_content)
if isinstance(arg, gtk.IconView):
store = arg.get_model()
selected = arg.get_selected_items()[0]
path = store[selected][2]
else:
model, iter = arg.get_selected()
path = model[iter][2]
if not path:
return
service = self.tracker.files.GetServiceType(path)
self.statusbar.push(0, '%s [%s]' % (path,service))
main_hbox = gtk.HBox() # 4 columns
main_hbox.set_homogeneous(True)
# Column #1: Thumbnail
# TODO
# Column #2: General File Properties
keys = DISPLAY_METADATA['Files']
metadata = self.tracker.metadata.Get('Files', path, keys)
table = gtk.Table(len(keys) + 2, 2, False)
table.set_col_spacings(6)
table.set_row_spacings(6)
label = gtk.Label('')
label.set_markup('%s' % metadata[0])
label.set_selectable(True)
label.set_ellipsize(pango.ELLIPSIZE_END)
hbox = gtk.HBox()
hbox.pack_start(label, expand=True, fill=True)
table.attach(hbox, 0, 2, 0, 1, gtk.FILL | gtk.EXPAND, gtk.FILL)
for n,(k,m) in enumerate(zip(keys,metadata)[1:]):
# Build Key
label = gtk.Label('')
if k in METADATA_LABELS:
label.set_markup('%s:' % METADATA_LABELS[k])
else:
label.set_markup('%s:' % k.split('.')[1].title())
hbox = gtk.HBox()
hbox.pack_end(label, expand=False, fill=False)
table.attach(hbox, 0, 1, n+1, n+2, gtk.FILL, gtk.FILL)
# Build Value
hbox = gtk.HBox()
if k in METADATA_FORMATTERS:
METADATA_FORMATTERS[k](hbox, m)
else:
label = gtk.Label(m)
label.set_selectable(True)
label.set_ellipsize(pango.ELLIPSIZE_END)
hbox.pack_start(label, fill=True, expand=True)
table.attach(hbox, 1, 2, n+1, n+2, gtk.FILL | gtk.EXPAND, gtk.FILL)
# Tag row widgets
label = gtk.Label('')
label.set_markup('Tags:')
hbox = gtk.HBox()
hbox.pack_end(label, expand=False, fill=False)
table.attach(hbox, 0, 1, n+2, n+3, gtk.FILL, gtk.FILL)
hbox1 = gtk.HBox()
add_button = gtk.Button()
image = gtk.image_new_from_stock('gtk-add', gtk.ICON_SIZE_BUTTON)
add_button.add(image)
add_button.set_relief(gtk.RELIEF_NONE)
hbox1.pack_start(add_button, expand=False, fill=False)
tags = self.tracker.keywords.Get('Files', path)
first = True
for tag in tags:
if not first:
hbox1.pack_start(gtk.Label(','), expand=False, fill=False)
label = gtk.Label()
label.set_markup('%s' % tag)
btn = gtk.Button()
btn.add(label)
btn.set_relief(gtk.RELIEF_NONE)
btn.connect('clicked', self.on_tag_selected)
hbox1.pack_start(btn, expand=False, fill=False)
first = False
hbox2 = gtk.HBox()
completion = gtk.EntryCompletion()
completion.set_model(self._create_tag_model())
completion.set_text_column(0)
tag_entry = gtk.Entry()
tag_entry.set_completion(completion)
hbox2.pack_start(tag_entry, expand=False, fill=False)
accept_button = gtk.Button()
image = gtk.image_new_from_stock('gtk-apply', gtk.ICON_SIZE_BUTTON)
accept_button.add(image)
accept_button.set_relief(gtk.RELIEF_NONE)
hbox2.pack_start(accept_button, expand=False, fill=False)
cancel_button = gtk.Button()
image = gtk.image_new_from_stock('gtk-cancel', gtk.ICON_SIZE_BUTTON)
cancel_button.add(image)
cancel_button.set_relief(gtk.RELIEF_NONE)
hbox2.pack_start(cancel_button, expand=False, fill=False)
hbox = gtk.HBox()
hbox.pack_start(hbox1, expand=True, fill=True)
hbox.pack_start(hbox2, expand=True, fill=True)
table.attach(hbox, 1, 2, n+2, n+3, gtk.FILL, gtk.FILL)
main_hbox.pack_start(table, expand=True, fill=True)
# Tag widget signals
tag_entry.connect('activate', lambda e: accept_button.emit('clicked'))
add_button.connect('clicked', lambda b: (hbox1.hide(), hbox2.show(), tag_entry.grab_focus()))
accept_button.connect('clicked', self.on_accept_tag_add, hbox1, hbox2, path, arg)
cancel_button.connect('clicked', lambda b: (hbox1.show(), hbox2.hide(), tag_entry.set_text('')))
# Column #3: Type specific metadata
if service in DISPLAY_METADATA and DISPLAY_METADATA[service]:
keys = DISPLAY_METADATA[service]
metadata = self.tracker.metadata.Get('Files', path, keys)
table = gtk.Table(len(keys), 2, False)
table.set_col_spacings(6)
table.set_row_spacings(6)
for n,(k,m) in enumerate(zip(keys,metadata)):
# Build Key
label = gtk.Label('')
if k in METADATA_LABELS:
label.set_markup('%s:' % METADATA_LABELS[k])
else:
label.set_markup('%s:' % k.split('.')[1].title())
hbox = gtk.HBox()
hbox.pack_end(label, expand=False, fill=False)
table.attach(hbox, 0, 1, n, n+1, gtk.FILL, gtk.FILL)
# Build Value
hbox = gtk.HBox()
if k in METADATA_FORMATTERS:
METADATA_FORMATTERS[k](hbox, m)
else:
label = gtk.Label(m)
label.set_selectable(True)
label.set_ellipsize(pango.ELLIPSIZE_END)
hbox.pack_start(label, fill=True, expand=True)
table.attach(hbox, 1, 2, n, n+1, gtk.FILL | gtk.EXPAND, gtk.FILL)
main_hbox.pack_start(table, expand=True, fill=True)
# Column #4: Sample contents
query = self.search_entry.get_text()
if not query.startswith('tag:'):
contents = metadata = self.tracker.metadata.Get('Files', path, ['file.content'])
if contents:
contents = contents[0].replace('\n', '\\n')
idx = contents.lower().find(query.lower())
if idx > -1:
start = max(0, idx - 75)
end = min(len(contents), start + 150)
text = gtk.TextView()
text.get_buffer().set_text(contents[start:end])
# .replace('<', '<')
# .replace('>', '>')
# .replace(query, '%s' % query))
text.set_wrap_mode(gtk.WRAP_WORD)
vbox = gtk.VBox()
vbox.pack_start(text, expand=False, fill=False)
frame = gtk.Frame('Contents')
frame.add(vbox)
main_hbox.pack_start(frame, expand=True, fill=True)
main_hbox.set_spacing(12)
main_hbox.show_all()
hbox2.hide()
self.bottom_pane_content.pack_start(main_hbox, expand=True, fill=True)
def _clear_container(self, widget, parent=None):
if isinstance(widget, gtk.Container):
widget.foreach(self._clear_container, widget)
if parent:
parent.remove(widget)
def _create_tag_model(self):
store = gtk.ListStore(str)
tags = self.tracker.keywords.GetList('Files')
for tag,count in tags.items():
store.append([tag])
return store
def on_tag_selected(self, button):
tag = button.get_children()[0].get_text()
self.search_entry.set_text('tag:%s' % tag)
self.search_button.emit('clicked')
def on_accept_tag_add(self, accept_button, hbox1, hbox2, path, selection):
tag = hbox2.get_children()[0].get_text()
self.tracker.keywords.Add('Files', path, [ tag ])
self.on_result_selected(selection)
gui = TrackerGUI()
gtk.main()