#!/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.genre', '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) label = gtk.Label(contents[start:end]) label.set_line_wrap(True) vbox = gtk.VBox() vbox.pack_start(label, 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()