a9c0dcbd6b
The current search system uses the OR operator to concatenate search terms. While results which are matched multiple times sort before other matches, it is almost guaranteed that adding an additional term to the search increments the number of results, which is rather surprising. https://bugzilla.gnome.org/show_bug.cgi?id=610955
614 lines
21 KiB
JavaScript
614 lines
21 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
const GConf = imports.gi.GConf;
|
|
const GLib = imports.gi.GLib;
|
|
const Gio = imports.gi.Gio;
|
|
const Shell = imports.gi.Shell;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const Signals = imports.signals;
|
|
const St = imports.gi.St;
|
|
const Gettext = imports.gettext.domain('gnome-shell');
|
|
const _ = Gettext.gettext;
|
|
|
|
const DND = imports.ui.dnd;
|
|
const Main = imports.ui.main;
|
|
const Search = imports.ui.search;
|
|
|
|
const NAUTILUS_PREFS_DIR = '/apps/nautilus/preferences';
|
|
const DESKTOP_IS_HOME_KEY = NAUTILUS_PREFS_DIR + '/desktop_is_home_dir';
|
|
|
|
const PLACES_ICON_SIZE = 16;
|
|
|
|
/**
|
|
* Represents a place object, which is most normally a bookmark entry,
|
|
* a mount/volume, or a special place like the Home Folder, Computer, and Network.
|
|
*
|
|
* @name: String title
|
|
* @iconFactory: A JavaScript callback which will create an icon texture given a size parameter
|
|
* @launch: A JavaScript callback to launch the entry
|
|
*/
|
|
function PlaceInfo(id, name, iconFactory, launch) {
|
|
this._init(id, name, iconFactory, launch);
|
|
}
|
|
|
|
PlaceInfo.prototype = {
|
|
_init: function(id, name, iconFactory, launch) {
|
|
this.id = id;
|
|
this.name = name;
|
|
this._lowerName = name.toLowerCase();
|
|
this.iconFactory = iconFactory;
|
|
this.launch = launch;
|
|
},
|
|
|
|
matchTerms: function(terms) {
|
|
let mtype = Search.MatchType.NONE;
|
|
for (let i = 0; i < terms.length; i++) {
|
|
let term = terms[i];
|
|
let idx = this._lowerName.indexOf(term);
|
|
if (idx == 0) {
|
|
mtype = Search.MatchType.PREFIX;
|
|
} else if (idx > 0) {
|
|
if (mtype == Search.MatchType.NONE)
|
|
mtype = Search.MatchType.SUBSTRING;
|
|
} else {
|
|
return Search.MatchType.NONE;
|
|
}
|
|
}
|
|
return mtype;
|
|
},
|
|
|
|
isRemovable: function() {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
function PlaceDeviceInfo(mount) {
|
|
this._init(mount);
|
|
}
|
|
|
|
PlaceDeviceInfo.prototype = {
|
|
__proto__: PlaceInfo.prototype,
|
|
|
|
_init: function(mount) {
|
|
this._mount = mount;
|
|
this.name = mount.get_name();
|
|
this._lowerName = this.name.toLowerCase();
|
|
this.id = 'mount:' + mount.get_root().get_uri();
|
|
},
|
|
|
|
iconFactory: function(size) {
|
|
let icon = this._mount.get_icon();
|
|
return St.TextureCache.get_default().load_gicon(icon, size);
|
|
},
|
|
|
|
launch: function() {
|
|
Gio.app_info_launch_default_for_uri(this._mount.get_root().get_uri(),
|
|
global.create_app_launch_context());
|
|
},
|
|
|
|
isRemovable: function() {
|
|
return this._mount.can_unmount();
|
|
},
|
|
|
|
remove: function() {
|
|
if (!this.isRemovable())
|
|
return;
|
|
|
|
if (this._mount.can_eject())
|
|
this._mount.eject(0, null, Lang.bind(this, this._removeFinish));
|
|
else
|
|
this._mount.unmount(0, null, Lang.bind(this, this._removeFinish));
|
|
},
|
|
|
|
_removeFinish: function(o, res, data) {
|
|
try {
|
|
if (this._mount.can_eject())
|
|
this._mount.eject_finish(res);
|
|
else
|
|
this._mount.unmount_finish(res);
|
|
} catch (e) {
|
|
let message = _("Failed to unmount '%s'").format(o.get_name());
|
|
Main.overview.infoBar.setMessage(message,
|
|
Lang.bind(this, this.remove),
|
|
_("Retry"));
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
function PlacesManager() {
|
|
this._init();
|
|
}
|
|
|
|
PlacesManager.prototype = {
|
|
_init: function() {
|
|
let gconf = GConf.Client.get_default();
|
|
gconf.add_dir(NAUTILUS_PREFS_DIR, GConf.ClientPreloadType.PRELOAD_NONE);
|
|
|
|
this._defaultPlaces = [];
|
|
this._mounts = [];
|
|
this._bookmarks = [];
|
|
this._isDesktopHome = gconf.get_bool(DESKTOP_IS_HOME_KEY);
|
|
|
|
let homeFile = Gio.file_new_for_path (GLib.get_home_dir());
|
|
let homeUri = homeFile.get_uri();
|
|
let homeLabel = Shell.util_get_label_for_uri (homeUri);
|
|
let homeIcon = Shell.util_get_icon_for_uri (homeUri);
|
|
this._home = new PlaceInfo('special:home', homeLabel,
|
|
function(size) {
|
|
return St.TextureCache.get_default().load_gicon(homeIcon, size);
|
|
},
|
|
function() {
|
|
Gio.app_info_launch_default_for_uri(homeUri, global.create_app_launch_context());
|
|
});
|
|
|
|
let desktopPath = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP);
|
|
let desktopFile = Gio.file_new_for_path (desktopPath);
|
|
let desktopUri = desktopFile.get_uri();
|
|
let desktopLabel = Shell.util_get_label_for_uri (desktopUri);
|
|
let desktopIcon = Shell.util_get_icon_for_uri (desktopUri);
|
|
this._desktopMenu = new PlaceInfo('special:desktop', desktopLabel,
|
|
function(size) {
|
|
return St.TextureCache.get_default().load_gicon(desktopIcon, size);
|
|
},
|
|
function() {
|
|
Gio.app_info_launch_default_for_uri(desktopUri, global.create_app_launch_context());
|
|
});
|
|
|
|
this._connect = new PlaceInfo('special:connect', _("Connect to..."),
|
|
function (size) {
|
|
return St.TextureCache.get_default().load_icon_name('applications-internet', size);
|
|
},
|
|
function () {
|
|
new Shell.Process({ args: ['nautilus-connect-server'] }).run();
|
|
});
|
|
|
|
let networkApp = null;
|
|
try {
|
|
networkApp = Shell.AppSystem.get_default().load_from_desktop_file('gnome-network-scheme.desktop');
|
|
} catch(e) {
|
|
try {
|
|
networkApp = Shell.AppSystem.get_default().load_from_desktop_file('network-scheme.desktop');
|
|
} catch(e) {
|
|
log('Cannot create "Network" item, .desktop file not found or corrupt.');
|
|
}
|
|
}
|
|
|
|
if (networkApp != null) {
|
|
this._network = new PlaceInfo('special:network', networkApp.get_name(),
|
|
function(size) {
|
|
return networkApp.create_icon_texture(size);
|
|
},
|
|
function () {
|
|
networkApp.launch();
|
|
});
|
|
}
|
|
|
|
this._defaultPlaces.push(this._home);
|
|
|
|
this._desktopMenuIndex = this._defaultPlaces.length;
|
|
if (!this._isDesktopHome)
|
|
this._defaultPlaces.push(this._desktopMenu);
|
|
|
|
if (this._network)
|
|
this._defaultPlaces.push(this._network);
|
|
|
|
this._defaultPlaces.push(this._connect);
|
|
|
|
/*
|
|
* Show devices, code more or less ported from nautilus-places-sidebar.c
|
|
*/
|
|
this._volumeMonitor = Gio.VolumeMonitor.get();
|
|
this._volumeMonitor.connect('volume-added', Lang.bind(this, this._updateDevices));
|
|
this._volumeMonitor.connect('volume-removed',Lang.bind(this, this._updateDevices));
|
|
this._volumeMonitor.connect('volume-changed', Lang.bind(this, this._updateDevices));
|
|
this._volumeMonitor.connect('mount-added', Lang.bind(this, this._updateDevices));
|
|
this._volumeMonitor.connect('mount-removed', Lang.bind(this, this._updateDevices));
|
|
this._volumeMonitor.connect('mount-changed', Lang.bind(this, this._updateDevices));
|
|
this._volumeMonitor.connect('drive-connected', Lang.bind(this, this._updateDevices));
|
|
this._volumeMonitor.connect('drive-disconnected', Lang.bind(this, this._updateDevices));
|
|
this._volumeMonitor.connect('drive-changed', Lang.bind(this, this._updateDevices));
|
|
this._updateDevices();
|
|
|
|
this._bookmarksPath = GLib.build_filenamev([GLib.get_home_dir(), '.gtk-bookmarks']);
|
|
this._bookmarksFile = Gio.file_new_for_path(this._bookmarksPath);
|
|
let monitor = this._bookmarksFile.monitor_file(Gio.FileMonitorFlags.NONE, null);
|
|
this._bookmarkTimeoutId = 0;
|
|
monitor.connect('changed', Lang.bind(this, function () {
|
|
if (this._bookmarkTimeoutId > 0)
|
|
return;
|
|
/* Defensive event compression */
|
|
this._bookmarkTimeoutId = Mainloop.timeout_add(100, Lang.bind(this, function () {
|
|
this._bookmarkTimeoutId = 0;
|
|
this._reloadBookmarks();
|
|
return false;
|
|
}));
|
|
}));
|
|
|
|
this._reloadBookmarks();
|
|
|
|
gconf.notify_add(DESKTOP_IS_HOME_KEY, Lang.bind(this, this._updateDesktopMenuVisibility));
|
|
|
|
},
|
|
|
|
_updateDevices: function() {
|
|
this._mounts = [];
|
|
|
|
/* first go through all connected drives */
|
|
let drives = this._volumeMonitor.get_connected_drives();
|
|
for (let i = 0; i < drives.length; i++) {
|
|
let volumes = drives[i].get_volumes();
|
|
for(let j = 0; j < volumes.length; j++) {
|
|
let mount = volumes[j].get_mount();
|
|
if(mount != null) {
|
|
this._addMount(mount);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* add all volumes that is not associated with a drive */
|
|
let volumes = this._volumeMonitor.get_volumes();
|
|
for(let i = 0; i < volumes.length; i++) {
|
|
if(volumes[i].get_drive() != null)
|
|
continue;
|
|
|
|
let mount = volumes[i].get_mount();
|
|
if(mount != null) {
|
|
this._addMount(mount);
|
|
}
|
|
}
|
|
|
|
/* add mounts that have no volume (/etc/mtab mounts, ftp, sftp,...) */
|
|
let mounts = this._volumeMonitor.get_mounts();
|
|
for(let i = 0; i < mounts.length; i++) {
|
|
if(mounts[i].is_shadowed())
|
|
continue;
|
|
|
|
if(mounts[i].get_volume())
|
|
continue;
|
|
|
|
this._addMount(mounts[i]);
|
|
}
|
|
|
|
/* We emit two signals, one for a generic 'all places' update
|
|
* and the other for one specific to mounts. We do this because
|
|
* clients like PlaceDisplay may only care about places in general
|
|
* being updated while clients like DashPlaceDisplay care which
|
|
* specific type of place got updated.
|
|
*/
|
|
this.emit('mounts-updated');
|
|
this.emit('places-updated');
|
|
|
|
},
|
|
|
|
_reloadBookmarks: function() {
|
|
|
|
this._bookmarks = [];
|
|
|
|
if (!GLib.file_test(this._bookmarksPath, GLib.FileTest.EXISTS))
|
|
return;
|
|
|
|
let [success, bookmarksContent, len] = GLib.file_get_contents(this._bookmarksPath);
|
|
|
|
if (!success)
|
|
return;
|
|
|
|
let bookmarks = bookmarksContent.split('\n');
|
|
|
|
let bookmarksToLabel = {};
|
|
let bookmarksOrder = [];
|
|
for (let i = 0; i < bookmarks.length; i++) {
|
|
let bookmarkLine = bookmarks[i];
|
|
let components = bookmarkLine.split(' ');
|
|
let bookmark = components[0];
|
|
if (bookmark in bookmarksToLabel)
|
|
continue;
|
|
let label = null;
|
|
if (components.length > 1)
|
|
label = components.slice(1).join(' ');
|
|
bookmarksToLabel[bookmark] = label;
|
|
bookmarksOrder.push(bookmark);
|
|
}
|
|
|
|
for (let i = 0; i < bookmarksOrder.length; i++) {
|
|
let bookmark = bookmarksOrder[i];
|
|
let label = bookmarksToLabel[bookmark];
|
|
let file = Gio.file_new_for_uri(bookmark);
|
|
if (!file.query_exists(null))
|
|
continue;
|
|
if (label == null)
|
|
label = Shell.util_get_label_for_uri(bookmark);
|
|
if (label == null)
|
|
continue;
|
|
let icon = Shell.util_get_icon_for_uri(bookmark);
|
|
|
|
let item = new PlaceInfo('bookmark:' + bookmark, label,
|
|
function(size) {
|
|
return St.TextureCache.get_default().load_gicon(icon, size);
|
|
},
|
|
function() {
|
|
Gio.app_info_launch_default_for_uri(bookmark, global.create_app_launch_context());
|
|
});
|
|
this._bookmarks.push(item);
|
|
}
|
|
|
|
/* See comment in _updateDevices for explanation why there are two signals. */
|
|
this.emit('bookmarks-updated');
|
|
this.emit('places-updated');
|
|
},
|
|
|
|
_updateDesktopMenuVisibility: function() {
|
|
let gconf = GConf.Client.get_default();
|
|
this._isDesktopHome = gconf.get_boolean(DESKTOP_IS_HOME_KEY);
|
|
|
|
if (this._isDesktopHome)
|
|
this._removeById(this._defaultPlaces, 'special:desktop');
|
|
else
|
|
this._defaultPlaces.splice(this._desktopMenuIndex, 0,
|
|
this._desktopMenu);
|
|
|
|
/* See comment in _updateDevices for explanation why there are two signals. */
|
|
this.emit('defaults-updated');
|
|
this.emit('places-updated');
|
|
},
|
|
|
|
_addMount: function(mount) {
|
|
let devItem = new PlaceDeviceInfo(mount);
|
|
this._mounts.push(devItem);
|
|
},
|
|
|
|
getAllPlaces: function () {
|
|
return this.getDefaultPlaces().concat(this.getBookmarks(), this.getMounts());
|
|
},
|
|
|
|
getDefaultPlaces: function () {
|
|
return this._defaultPlaces;
|
|
},
|
|
|
|
getBookmarks: function () {
|
|
return this._bookmarks;
|
|
},
|
|
|
|
getMounts: function () {
|
|
return this._mounts;
|
|
},
|
|
|
|
_lookupIndexById: function(sourceArray, id) {
|
|
for (let i = 0; i < sourceArray.length; i++) {
|
|
let place = sourceArray[i];
|
|
if (place.id == id)
|
|
return i;
|
|
}
|
|
return -1;
|
|
},
|
|
|
|
lookupPlaceById: function(id) {
|
|
let colonIdx = id.indexOf(':');
|
|
let type = id.substring(0, colonIdx);
|
|
let sourceArray = null;
|
|
if (type == 'special')
|
|
sourceArray = this._defaultPlaces;
|
|
else if (type == 'mount')
|
|
sourceArray = this._mounts;
|
|
else if (type == 'bookmark')
|
|
sourceArray = this._bookmarks;
|
|
return sourceArray[this._lookupIndexById(sourceArray, id)];
|
|
},
|
|
|
|
_removeById: function(sourceArray, id) {
|
|
sourceArray.splice(this._lookupIndexById(sourceArray, id), 1);
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(PlacesManager.prototype);
|
|
|
|
/**
|
|
* An entry in the places menu.
|
|
* @info The corresponding PlaceInfo to populate this entry.
|
|
*/
|
|
function DashPlaceDisplayItem(info) {
|
|
this._init(info);
|
|
}
|
|
|
|
DashPlaceDisplayItem.prototype = {
|
|
_init: function(info) {
|
|
this.name = info.name;
|
|
this._info = info;
|
|
this._icon = info.iconFactory(PLACES_ICON_SIZE);
|
|
|
|
this.actor = new St.Clickable({ style_class: 'places-item',
|
|
reactive: true,
|
|
x_align: St.Align.START,
|
|
x_fill: true });
|
|
|
|
let box = new St.BoxLayout({ style_class: 'places-item-box' });
|
|
this.actor.set_child(box);
|
|
|
|
let bin = new St.Bin({ child: this._icon });
|
|
box.add(bin);
|
|
|
|
let text = new St.Label({ text: info.name });
|
|
box.add(text, { expand: true, x_fill: true });
|
|
|
|
if (info.isRemovable()) {
|
|
let removeIcon = St.TextureCache.get_default().load_icon_name ('media-eject', PLACES_ICON_SIZE);
|
|
let removeIconBox = new St.Clickable({ child: removeIcon,
|
|
reactive: true });
|
|
box.add(removeIconBox);
|
|
removeIconBox.connect('clicked',
|
|
Lang.bind(this, function() {
|
|
this._info.remove();
|
|
}));
|
|
}
|
|
|
|
this.actor.connect('clicked', Lang.bind(this, this._onClicked));
|
|
|
|
this.actor._delegate = this;
|
|
this._draggable = DND.makeDraggable(this.actor);
|
|
this._draggable.connect('drag-begin',
|
|
Lang.bind(this, function() {
|
|
Main.overview.beginItemDrag(this);
|
|
}));
|
|
this._draggable.connect('drag-end',
|
|
Lang.bind(this, function() {
|
|
Main.overview.endItemDrag(this);
|
|
}));
|
|
},
|
|
|
|
_onClicked: function(b) {
|
|
this._info.launch();
|
|
Main.overview.hide();
|
|
},
|
|
|
|
getDragActorSource: function() {
|
|
return this._icon;
|
|
},
|
|
|
|
getDragActor: function(stageX, stageY) {
|
|
return this._info.iconFactory(PLACES_ICON_SIZE);
|
|
},
|
|
|
|
//// Drag and drop methods ////
|
|
|
|
shellWorkspaceLaunch: function() {
|
|
this._info.launch();
|
|
}
|
|
};
|
|
|
|
function DashPlaceDisplay() {
|
|
this._init();
|
|
}
|
|
|
|
DashPlaceDisplay.prototype = {
|
|
_init: function() {
|
|
|
|
// Places is divided semi-arbitrarily into left and right; a grid would
|
|
// look better in that there would be an even number of items left+right,
|
|
// but it seems like we want some sort of differentiation between actions
|
|
// like "Connect to server..." and regular folders
|
|
this.actor = new St.Table({ style_class: 'places-section',
|
|
homogeneous: true });
|
|
|
|
this._defaultsList = [];
|
|
this._bookmarksList = [];
|
|
this._mountsList = [];
|
|
|
|
Main.placesManager.connect('defaults-updated', Lang.bind(this, this._updateDefaults));
|
|
Main.placesManager.connect('bookmarks-updated', Lang.bind(this, this._updateBookmarks));
|
|
Main.placesManager.connect('mounts-updated', Lang.bind(this, this._updateMounts));
|
|
|
|
this._updateDefaults();
|
|
this._updateMounts();
|
|
this._updateBookmarks();
|
|
},
|
|
|
|
_updateDefaults: function() {
|
|
for (let i = 0; i < this._defaultsList.length; i++)
|
|
this._defaultsList[i].destroy();
|
|
|
|
this._defaultsList = [];
|
|
let places = Main.placesManager.getDefaultPlaces();
|
|
for (let i = 0; i < places.length; i++) {
|
|
this._defaultsList[i] = new DashPlaceDisplayItem(places[i]).actor;
|
|
this.actor.add(this._defaultsList[i], {row: i, col: 0});
|
|
}
|
|
this._updateMounts();
|
|
},
|
|
|
|
_updateMounts: function() {
|
|
for (let i = 0; i < this._mountsList.length; i++)
|
|
this._mountsList[i].destroy();
|
|
|
|
this._mountsList = [];
|
|
let places = Main.placesManager.getMounts();
|
|
for (let i = 0; i < places.length; i++) {
|
|
this._mountsList[i] = new DashPlaceDisplayItem(places[i]).actor;
|
|
this.actor.add(this._mountsList[i], {row: this._defaultsList.length + i, col: 0});
|
|
}
|
|
},
|
|
|
|
_updateBookmarks: function() {
|
|
for (let i = 0; i < this._bookmarksList.length; i++)
|
|
this._bookmarksList[i].destroy();
|
|
|
|
this._bookmarksList = [];
|
|
let places = Main.placesManager.getBookmarks();
|
|
for (let i = 0; i < places.length; i ++) {
|
|
this._bookmarksList[i] = new DashPlaceDisplayItem(places[i]).actor;
|
|
this.actor.add(this._bookmarksList[i], {row: i, col: 1});
|
|
}
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(DashPlaceDisplay.prototype);
|
|
|
|
function PlaceSearchProvider() {
|
|
this._init();
|
|
}
|
|
|
|
PlaceSearchProvider.prototype = {
|
|
__proto__: Search.SearchProvider.prototype,
|
|
|
|
_init: function() {
|
|
Search.SearchProvider.prototype._init.call(this, _("PLACES & DEVICES"));
|
|
},
|
|
|
|
getResultMeta: function(resultId) {
|
|
let placeInfo = Main.placesManager.lookupPlaceById(resultId);
|
|
if (!placeInfo)
|
|
return null;
|
|
return { 'id': resultId,
|
|
'name': placeInfo.name,
|
|
'icon': placeInfo.iconFactory(Search.RESULT_ICON_SIZE) };
|
|
},
|
|
|
|
activateResult: function(id) {
|
|
let placeInfo = Main.placesManager.lookupPlaceById(id);
|
|
placeInfo.launch();
|
|
},
|
|
|
|
_compareResultMeta: function (idA, idB) {
|
|
let infoA = Main.placesManager.lookupPlaceById(idA);
|
|
let infoB = Main.placesManager.lookupPlaceById(idB);
|
|
return infoA.name.localeCompare(infoB.name);
|
|
},
|
|
|
|
_searchPlaces: function(places, terms) {
|
|
let multiplePrefixResults = [];
|
|
let prefixResults = [];
|
|
let multipleSubstringResults = [];
|
|
let substringResults = [];
|
|
|
|
terms = terms.map(String.toLowerCase);
|
|
|
|
for (let i = 0; i < places.length; i++) {
|
|
let place = places[i];
|
|
let mtype = place.matchTerms(terms);
|
|
if (mtype == Search.MatchType.MULTIPLE_PREFIX)
|
|
multiplePrefixResults.push(place.id);
|
|
else if (mtype == Search.MatchType.PREFIX)
|
|
prefixResults.push(place.id);
|
|
else if (mtype == Search.MatchType.MULTIPLE_SUBSTRING)
|
|
multipleSubstringResults.push(place.id);
|
|
else if (mtype == Search.MatchType.SUBSTRING)
|
|
substringResults.push(place.id);
|
|
}
|
|
multiplePrefixResults.sort(this._compareResultMeta);
|
|
prefixResults.sort(this._compareResultMeta);
|
|
multipleSubstringResults.sort(this._compareResultMeta);
|
|
substringResults.sort(this._compareResultMeta);
|
|
return multiplePrefixResults.concat(prefixResults.concat(multipleSubstringResults.concat(substringResults)));
|
|
},
|
|
|
|
getInitialResultSet: function(terms) {
|
|
let places = Main.placesManager.getAllPlaces();
|
|
return this._searchPlaces(places, terms);
|
|
},
|
|
|
|
getSubsearchResultSet: function(previousResults, terms) {
|
|
let places = previousResults.map(function (id) { return Main.placesManager.lookupPlaceById(id); });
|
|
return this._searchPlaces(places, terms);
|
|
}
|
|
};
|