diff --git a/README b/README index 3b54860..3cfd0a2 100644 --- a/README +++ b/README @@ -1,7 +1,11 @@ Tracker-Search Extension for Gnome-Shell -Version: 1.3 +Version: 1.4 -TODO: + +Done: * Display File Location -* Display more than one row of results +* Display more than one row of results (as soon as bug is fixed) + +TODO: +* Fix CSS, no more transitions on mouse hover ... diff --git a/extension.js b/extension.js index 93dac40..9c37393 100644 --- a/extension.js +++ b/extension.js @@ -1,134 +1,202 @@ /* Tracker Search Provider for Gnome Shell * - * Copyright (c) 2012 Christian Weber, Felix Schultze + * 2012 Contributors Christian Weber, Felix Schultze, Martyn Russell * * This programm is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * - * Version 1.4 + * Version 1.7 * * https://github.com/cewee/tracker-search */ -const Main = imports.ui.main; -const Search = imports.ui.search; -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const Lang = imports.lang; -const Shell = imports.gi.Shell; -const Util = imports.misc.util; -const Tracker = imports.gi.Tracker; -const St = imports.gi.St; +const Main = imports.ui.main; +const Search = imports.ui.search; +const SearchDisplay = imports.ui.searchDisplay; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Shell = imports.gi.Shell; +const IconGrid = imports.ui.iconGrid; +const Util = imports.misc.util; +const Tracker = imports.gi.Tracker; +const St = imports.gi.St; +const Atk = imports.gi.Atk; /* let xdg-open pick the appropriate program to open/execute the file */ const DEFAULT_EXEC = 'xdg-open'; /* Limit search results, since number of displayed items is limited */ -const MAX_RESULTS = 12; +const MAX_RESULTS = 40; +const MAX_ROWS = 3; // this is currently ignored, but bug report is filed : https://bugzilla.gnome.org/show_bug.cgi?id=687474 +const ICON_SIZE = 25; -var trackerSearchProvider = null; +const CategoryType = { + FTS : 0, + FILES : 1, + FOLDERS : 2 +}; + +function TrackerResult(result) { + this._init(result); +} + +// Overwriting layout to display search results. +TrackerResult.prototype = { + _init: function(resultMeta) { + this.actor = new St.Bin({ style_class: 'result', + reactive: true, + can_focus: true, + track_hover: true, + accessible_role: Atk.Role.PUSH_BUTTON}); + var MainBox = new St.BoxLayout( { style_class: 'result-content', vertical: true }); + this.actor.set_child(MainBox); + var icon = resultMeta.createIcon(ICON_SIZE); + // View for regular files + if (resultMeta.contentType != "inode/directory" ) { + let title = new St.Label({ text: resultMeta.name, style_class: 'title' }); + MainBox.add(title, { x_fill: false, x_align: St.Align.START }); + let IconInfoFrame = new St.BoxLayout({ style_class: 'icon-info-frame', vertical: false }); + let IconBox = new St.BoxLayout({ vertical: false }); + MainBox.add(IconInfoFrame, { x_fill: false, x_align: St.Align.START, y_align: St.Align.MIDDLE }); + IconBox.add(icon, { x_fill: false, y_fill: false, x_align: St.Align.START, y_align: St.Align.MIDDLE }); + IconInfoFrame.add(IconBox, { x_fill: false, x_align: St.Align.START }); + let SideBox = new St.BoxLayout({ style_class: 'side-box', vertical: true }); + IconInfoFrame.add(SideBox, { x_fill: false, x_align: St.Align.START }); + let fileName = new St.Label({ text: resultMeta.filename, style_class: 'result-detail' }); + SideBox.add(fileName, { x_fill: false, x_align: St.Align.START }); + let lastMod = new St.Label({ text: resultMeta.lastMod, style_class: 'result-detail' }); + SideBox.add(lastMod, { x_fill: false, x_align: St.Align.START }); + let prettyPath = new St.Label({ text: resultMeta.prettyPath, style_class: 'result-path' }); + MainBox.add(prettyPath, { x_fill: false, x_align: St.Align.START }); + } else { // View for folder elements + let titleDir = new St.Label({ text: resultMeta.name, style_class: 'titleDir' }); + MainBox.add(titleDir, { x_fill: false, y_fill: true, x_align: St.Align.START, y_align: St.Align.MIDDLE }); + let prettyPath = new St.Label({ text: resultMeta.prettyPath, style_class: 'result-pathDir' }); + MainBox.add(prettyPath, { x_fill: false, x_align: St.Align.START }); + MainBox.add(icon, { x_fill: false, y_fill: false, x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE }); + } + } +}; + +var trackerSearchProviderFiles = null; +var trackerSearchProviderFolders = null; -function TrackerSearchProvider() { - this._init(); + +function TrackerSearchProvider(title, categoryType) { + this._init(title, categoryType); } TrackerSearchProvider.prototype = { __proto__ : Search.SearchProvider.prototype, + _categoryType : -1, - _init : function(name) { - Search.SearchProvider.prototype._init.call(this, "TRACKER SEARCH"); + _init : function(title, categoryType) { + this._categoryType = categoryType; + var grid = new IconGrid.IconGrid({ rowLimit: MAX_ROWS, columnLimit: 8, xAlign: St.Align.MIDDLE }); + var actor = new SearchDisplay.GridSearchResults(this, grid); + this._grid = grid; + Search.SearchProvider.prototype._init.call(this, title + " (from Tracker)"); }, - getResultMetas: function(resultIds) { + getResultMetas: function(resultIds,callback) { let metas = []; for (let i = 0; i < resultIds.length; i++) { metas.push(this.getResultMeta(resultIds[i])); } - return metas; + callback(metas); +// return metas; }, getResultMeta : function(resultId) { let type = resultId.contentType; - let name = resultId.filename; + let name = resultId.name; + let path = resultId.path; + let filename = resultId.filename; + let lastMod = resultId.lastMod; + let contentType = resultId.contentType; + let prettyPath = resultId.prettyPath; return { - 'id' : resultId, - 'name' : name, + 'id': resultId, + 'name': name, + 'path': path, + 'filename': filename, + 'lastMod': lastMod, + 'prettyPath': prettyPath, + 'contentType': contentType, 'createIcon' : function(size) { let icon = Gio.app_info_get_default_for_type(type,null).get_icon(); - return imports.gi.St.TextureCache.get_default().load_gicon(null, icon, size); } }; }, - activateResult : function(id) { + activateResult : function(result) { // Action executed when clicked on result - var target = id.fileAndPath; - Util.spawn([ DEFAULT_EXEC, target ]); + var uri = result.id; + var f = Gio.file_new_for_uri(uri); + var fileName = f.get_path(); + Util.spawn([DEFAULT_EXEC, fileName]); }, - getInitialResultSet : function(terms) { // terms holds array of search items - // check if 1st search term is >2 letters else drop the request - if(terms[0].length <= 2) { - return []; - } + getQuery : function (terms) { + var query = ""; - let results = []; - /* Build sparql query */ - var new_query = "SELECT nie:url(?f) WHERE {?f fts:match'"; - for ( var i = 0; i < terms.length; i++) { - new_query = new_query + " " + terms[i] + "*"; + if (this._categoryType == CategoryType.FTS) { + var terms_in_sparql = ""; + + for (var i = 0; i < terms.length; i++) { + if (terms_in_sparql.length > 0) terms_in_sparql += " "; + terms_in_sparql += terms[i] + "*"; } + // Technically, the tag should really be matched + // separately not as one phrase too. + query += "SELECT ?urn nie:url(?urn) tracker:coalesce(nie:title(?urn), nfo:fileName(?urn)) nie:url(?parent) nfo:fileLastModified(?urn) WHERE { { "; + query += " ?urn a nfo:FileDataObject ."; + query += " ?urn fts:match \"" + terms_in_sparql + "\" } UNION { ?urn nao:hasTag ?tag . FILTER (fn:contains (fn:lower-case (nao:prefLabel(?tag)), \"" + terms + "\")) }"; + query += " OPTIONAL { ?urn nfo:belongsToContainer ?parent . ?r2 a nfo:Folder . FILTER(?r2 = ?urn). } . FILTER(!BOUND(?r2)). } ORDER BY DESC(nfo:fileLastModified(?urn)) ASC(nie:title(?urn)) OFFSET 0 LIMIT " + String(MAX_RESULTS); + // ?r2 a nfo:Folder . FILTER(?r2 = ?urn). } . FILTER(!BOUND(?r2) is supposed to filter out folders, but this fails for 'root' folders in which is indexed (as 'Music', 'Documents' and so on ..) - WHY? - new_query = new_query + "' } ORDER BY DESC (fts:rank(?f)) LIMIT " + String(MAX_RESULTS); + } else if (this._categoryType == CategoryType.FILES) { + // TODO: Do we really want this? + } else if (this._categoryType == CategoryType.FOLDERS) { + query += "SELECT ?urn nie:url(?urn) tracker:coalesce(nie:title(?urn), nfo:fileName(?urn)) nie:url(?parent) nfo:fileLastModified(?urn) WHERE {"; + query += " ?urn a nfo:Folder ."; + query += " FILTER (fn:contains (fn:lower-case (nfo:fileName(?urn)), '" + terms + "')) ."; + query += " ?urn nfo:belongsToContainer ?parent ;"; + query += " tracker:available true ."; + query += "} ORDER BY DESC(nfo:fileLastModified(?urn)) DESC(nie:contentCreated(?urn)) ASC(nie:title(?urn)) OFFSET 0 LIMIT " + String(MAX_RESULTS); + } - let conn = Tracker.SparqlConnection.get(null); - let cursor = conn.query(new_query, null); + return query; + }, + filterResults : function(cursor) { + let results = []; try { while (cursor != null && cursor.next(null)) { - var result = cursor.get_string (null); - // filter our, bogus tracker responses - if (String(result) == ",0") { - continue; - } - - // cut of number of internal hits - var fileStr = String(result).split(','); - fileStr = decodeURI(fileStr[0]); - - // Extract filename from line - var splitted = String(fileStr).split('/'); - var filename = decodeURI(splitted[splitted.length - 1]); - let ft = String(filename).split('.'); - - // extract path and filename - splitted = String(fileStr).split('ile://'); - var fileAndPath = ""; - if (splitted.length == 2) { - fileAndPath = decodeURI(splitted[1]); - } - + var urn = cursor.get_string(0)[0]; + var uri = cursor.get_string(1)[0]; + var title = cursor.get_string(2)[0]; + var parentUri = cursor.get_string(3)[0]; + var lastMod = cursor.get_string(4)[0]; + var lastMod = lastMod.split('T')[0]; + var filename = decodeURI(uri.split('/').pop()); // if file does not exist, it won't be shown - if(!Gio.file_new_for_path(fileAndPath).query_exists(null)) - { - continue; - } - + var f = Gio.file_new_for_uri(uri); + if(!f.query_exists(null)) {continue;} + var path = f.get_path(); + // clean up path + var prettyPath = path.substr(0,path.length - filename.length).replace("/home/" + GLib.get_user_name() , "~"); // contentType is an array, the index "1" set true, // if function is uncertain if type is the right one - let contentType = Gio.content_type_guess(fileAndPath, null); + let contentType = Gio.content_type_guess(path, null); var newContentType = contentType[0]; if(contentType[1]){ if(newContentType == "application/octet-stream") { - - - let fileInfo = Gio.file_new_for_path(fileAndPath).query_info('standard::type', 0, null); - + let fileInfo = Gio.file_new_for_path(path).query_info('standard::type', 0, null); // for some reason 'content_type_guess' returns a wrong mime type for folders - if(fileInfo.get_file_type() == Gio.FileType.DIRECTORY) - { + if(fileInfo.get_file_type() == Gio.FileType.DIRECTORY) { newContentType = "inode/directory"; } else { // unrecognized mime-types are set to text, so that later an icon can be picked @@ -136,35 +204,77 @@ TrackerSearchProvider.prototype = { } } } - results.push({ - 'filename' : filename, - 'fileAndPath' : fileAndPath, + 'id' : uri, + 'name' : title, + 'path' : path, + 'filename': filename, + 'lastMod' : lastMod, + 'prettyPath' : prettyPath, 'contentType' : newContentType }); } } catch (error) { - global.log("cursor " + error.message); // + global.log(this.title + ": Could not traverse results cursor: " + error.message); return []; } - return (results.length > 0) ? results : []; }, + getInitialResultSet : function(terms) { + // terms holds array of search items + // check if 1st search term is >2 letters else drop the request + if(terms[0].length < 3) { + return []; + } + + try { + var conn = Tracker.SparqlConnection.get(null); + var query = this.getQuery(terms); + var cursor = conn.query(query, null); + } catch (error) { + global.log("Querying Tracker failed. Please make sure you have the --GObject Introspection-- package for Tracker installed."); + global.log(error.message); + return []; + } + +// return this.filterResults(cursor); +return this.searchSystem.pushResults(this, this.filterResults(cursor) ); + }, + getSubsearchResultSet : function(previousResults, terms) { + // check if 1st search term is >2 letters else drop the request + if(terms[0].length < 3) { + return []; + } return this.getInitialResultSet(terms); + }, + + // Display overwrites + createResultActor: function(resultMeta, terms) { + let result = new TrackerResult(resultMeta); + return result.actor; }, + + }; function init(meta) { } function enable() { - trackerSearchProvider = new TrackerSearchProvider(); - Main.overview.addSearchProvider(trackerSearchProvider); + trackerSearchProviderFolders = new TrackerSearchProvider("Folders", CategoryType.FOLDERS); + Main.overview.addSearchProvider(trackerSearchProviderFolders); + + trackerSearchProviderFiles = new TrackerSearchProvider("Files", CategoryType.FTS); + Main.overview.addSearchProvider(trackerSearchProviderFiles); } function disable() { - Main.overview.removeSearchProvider(trackerSearchProvider); - trackerSearchProvider = null; + Main.overview.removeSearchProvider(trackerSearchProviderFiles); + trackerSearchProviderFiles = null; + + Main.overview.removeSearchProvider(trackerSearchProviderFolders); + trackerSearchProviderFolders = null; } + diff --git a/metadata.json b/metadata.json index 52bf189..757b6d7 100644 --- a/metadata.json +++ b/metadata.json @@ -1,13 +1,11 @@ { - "description": "Search Provider for Tracker", + "description": "Integrates Tracker-Search into the Gnome-Shell desktop.\r\nDepends on Tracker beeing installed and running (http://projects.gnome.org/tracker/) , as well as the GObject Introspection package for Tracker (for Ubuntu that is gir1.2-tracker).", "name": "Tracker Search", "shell-version": [ - "3.2", - "3.4" - ], - "url": "https://github.com/cewee/tracker-search", - "uuid": "tracker-search@cewee", - "version": 3 + "3.6", + "3.6.1" + ], + "url": "https://github.com/cewee/tracker-search", + "uuid": "tracker-search@cewee", + "version": 8 } - - diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..131b248 --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,77 @@ +.title { + font-size: .8em; + +} +.titleDir { + font-size: .9em; + padding-left: 7px; +} +.result-pathDir { + padding-left: 7px; + font-size: .7em; +} +.result-path { + font-size: .6em; +} +.result-detail { + padding-left: 7px; + padding-bottom: 4px; + font-size: .8em; + color:rgb(200,200,200); +} +.icon-info-frame{ + padding-top: 5px; +} + +.directory-content { + border-radius: 7px; + padding: 10px; + width: 100px; + height: 50px; + background-color: rgba(0.0, 0.0, 0.0, 0.55); + color: white; +} + +.result { + width: 272px; /* Same width as two normal results + spacing */ + height: 118px; /* Aspect ratio = 1.75. Normal US business card ratio */ + border-radius: 4px; + padding: 3px; + border: 1px rgba(0,0,0,0); + transition-duration: 100; +} + +.result-content { + border-radius: 7px; + padding: 4px; + width: 200px; + height: 70px; + background-color: rgba(0.0, 0.0, 0.0, 0.3); + color: white; +} + +.result:hover { + background-color: rgba(255,255,255,0.1); + transition-duration: 100; +} + +.result:focus, +.app-well-app:focus > .overview-icon, +.search-result-content:focus > .overview-icon, +.result:selected, +.app-well-app:selected > .overview-icon, +.search-result-content:selected > .overview-icon { + background-color: rgba(255,255,255,0.33); +} + +.tracker-grid { + border-radius: 7px; + padding: 4px; + width: 300px; + height: 70px; + +} + + + +