1
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
3
const GLib = imports.gi.GLib;
4
const Gio = imports.gi.Gio;
5
const Shell = imports.gi.Shell;
6
const Lang = imports.lang;
7
const Mainloop = imports.mainloop;
8
const Signals = imports.signals;
9
const St = imports.gi.St;
11
const DND = imports.ui.dnd;
12
const Main = imports.ui.main;
13
const Params = imports.misc.params;
14
const Search = imports.ui.search;
15
const Util = imports.misc.util;
18
* Represents a place object, which is most normally a bookmark entry,
19
* a mount/volume, or a special place like the Home Folder, Computer, and Network.
22
* @iconFactory: A JavaScript callback which will create an icon texture given a size parameter
23
* @launch: A JavaScript callback to launch the entry
25
const PlaceInfo = new Lang.Class({
28
_init: function(id, name, iconFactory, launch) {
31
this._lowerName = name.toLowerCase();
32
this.iconFactory = iconFactory;
36
matchTerms: function(terms) {
37
let mtype = Search.MatchType.NONE;
38
for (let i = 0; i < terms.length; i++) {
40
let idx = this._lowerName.indexOf(term);
42
mtype = Search.MatchType.PREFIX;
44
if (mtype == Search.MatchType.NONE)
45
mtype = Search.MatchType.SUBSTRING;
47
return Search.MatchType.NONE;
53
isRemovable: function() {
58
// Helper function to translate launch parameters into a GAppLaunchContext
59
function _makeLaunchContext(params)
61
params = Params.parse(params, { workspace: -1,
64
let launchContext = global.create_app_launch_context();
65
if (params.workspace != -1)
66
launchContext.set_desktop(params.workspace);
67
if (params.timestamp != 0)
68
launchContext.set_timestamp(params.timestamp);
73
const PlaceDeviceInfo = new Lang.Class({
74
Name: 'PlaceDeviceInfo',
77
_init: function(mount) {
79
this.name = mount.get_name();
80
this._lowerName = this.name.toLowerCase();
81
this.id = 'mount:' + mount.get_root().get_uri();
84
iconFactory: function(size) {
85
let icon = this._mount.get_icon();
86
return St.TextureCache.get_default().load_gicon(null, icon, size);
89
launch: function(params) {
90
Gio.app_info_launch_default_for_uri(this._mount.get_root().get_uri(),
91
_makeLaunchContext(params));
94
isRemovable: function() {
95
return this._mount.can_unmount();
99
if (!this.isRemovable())
102
if (this._mount.can_eject())
103
this._mount.eject(0, null, Lang.bind(this, this._removeFinish));
105
this._mount.unmount(0, null, Lang.bind(this, this._removeFinish));
108
_removeFinish: function(o, res, data) {
110
if (this._mount.can_eject())
111
this._mount.eject_finish(res);
113
this._mount.unmount_finish(res);
115
let message = _("Failed to unmount '%s'").format(o.get_name());
116
Main.overview.setMessage(message,
117
Lang.bind(this, this.remove),
123
const PlacesManager = new Lang.Class({
124
Name: 'PlacesManager',
127
this._defaultPlaces = [];
129
this._bookmarks = [];
131
let homeFile = Gio.file_new_for_path (GLib.get_home_dir());
132
let homeUri = homeFile.get_uri();
133
let homeLabel = Shell.util_get_label_for_uri (homeUri);
134
let homeIcon = Shell.util_get_icon_for_uri (homeUri);
135
this._home = new PlaceInfo('special:home', homeLabel,
137
return St.TextureCache.get_default().load_gicon(null, homeIcon, size);
140
Gio.app_info_launch_default_for_uri(homeUri, _makeLaunchContext(params));
143
let desktopPath = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP);
144
let desktopFile = Gio.file_new_for_path (desktopPath);
145
let desktopUri = desktopFile.get_uri();
146
let desktopLabel = Shell.util_get_label_for_uri (desktopUri);
147
let desktopIcon = Shell.util_get_icon_for_uri (desktopUri);
148
this._desktopMenu = new PlaceInfo('special:desktop', desktopLabel,
150
return St.TextureCache.get_default().load_gicon(null, desktopIcon, size);
153
Gio.app_info_launch_default_for_uri(desktopUri, _makeLaunchContext(params));
156
this._connect = new PlaceInfo('special:connect', _("Connect to..."),
158
// do NOT use St.Icon here, it crashes the shell
159
// see wanda.js for details
160
return St.TextureCache.get_default().load_icon_name(null,
161
'applications-internet',
162
St.IconType.FULLCOLOR,
166
// BUG: nautilus-connect-server doesn't have a desktop file, so we can't
167
// launch it with the workspace from params. It's probably pretty rare
168
// and odd to drag this place onto a workspace in any case
170
Util.spawn(['nautilus-connect-server']);
173
this._defaultPlaces.push(this._home);
174
this._defaultPlaces.push(this._desktopMenu);
175
this._defaultPlaces.push(this._connect);
178
* Show devices, code more or less ported from nautilus-places-sidebar.c
180
this._volumeMonitor = Gio.VolumeMonitor.get();
181
this._volumeMonitor.connect('volume-added', Lang.bind(this, this._updateDevices));
182
this._volumeMonitor.connect('volume-removed',Lang.bind(this, this._updateDevices));
183
this._volumeMonitor.connect('volume-changed', Lang.bind(this, this._updateDevices));
184
this._volumeMonitor.connect('mount-added', Lang.bind(this, this._updateDevices));
185
this._volumeMonitor.connect('mount-removed', Lang.bind(this, this._updateDevices));
186
this._volumeMonitor.connect('mount-changed', Lang.bind(this, this._updateDevices));
187
this._volumeMonitor.connect('drive-connected', Lang.bind(this, this._updateDevices));
188
this._volumeMonitor.connect('drive-disconnected', Lang.bind(this, this._updateDevices));
189
this._volumeMonitor.connect('drive-changed', Lang.bind(this, this._updateDevices));
190
this._updateDevices();
192
this._bookmarksPath = GLib.build_filenamev([GLib.get_home_dir(), '.gtk-bookmarks']);
193
this._bookmarksFile = Gio.file_new_for_path(this._bookmarksPath);
194
this._monitor = this._bookmarksFile.monitor_file(Gio.FileMonitorFlags.NONE, null);
195
this._bookmarkTimeoutId = 0;
196
this._monitor.connect('changed', Lang.bind(this, function () {
197
if (this._bookmarkTimeoutId > 0)
199
/* Defensive event compression */
200
this._bookmarkTimeoutId = Mainloop.timeout_add(100, Lang.bind(this, function () {
201
this._bookmarkTimeoutId = 0;
202
this._reloadBookmarks();
207
this._reloadBookmarks();
210
_updateDevices: function() {
213
/* first go through all connected drives */
214
let drives = this._volumeMonitor.get_connected_drives();
215
for (let i = 0; i < drives.length; i++) {
216
let volumes = drives[i].get_volumes();
217
for(let j = 0; j < volumes.length; j++) {
218
let mount = volumes[j].get_mount();
220
this._addMount(mount);
225
/* add all volumes that is not associated with a drive */
226
let volumes = this._volumeMonitor.get_volumes();
227
for(let i = 0; i < volumes.length; i++) {
228
if(volumes[i].get_drive() != null)
231
let mount = volumes[i].get_mount();
233
this._addMount(mount);
237
/* add mounts that have no volume (/etc/mtab mounts, ftp, sftp,...) */
238
let mounts = this._volumeMonitor.get_mounts();
239
for(let i = 0; i < mounts.length; i++) {
240
if(mounts[i].is_shadowed())
243
if(mounts[i].get_volume())
246
this._addMount(mounts[i]);
249
/* We emit two signals, one for a generic 'all places' update
250
* and the other for one specific to mounts. We do this because
251
* clients like PlaceDisplay may only care about places in general
252
* being updated while clients like DashPlaceDisplay care which
253
* specific type of place got updated.
255
this.emit('mounts-updated');
256
this.emit('places-updated');
260
_reloadBookmarks: function() {
262
this._bookmarks = [];
264
if (!GLib.file_test(this._bookmarksPath, GLib.FileTest.EXISTS))
267
let bookmarksContent = Shell.get_file_contents_utf8_sync(this._bookmarksPath);
269
let bookmarks = bookmarksContent.split('\n');
271
let bookmarksToLabel = {};
272
let bookmarksOrder = [];
273
for (let i = 0; i < bookmarks.length; i++) {
274
let bookmarkLine = bookmarks[i];
275
let components = bookmarkLine.split(' ');
276
let bookmark = components[0];
277
if (bookmark in bookmarksToLabel)
280
if (components.length > 1)
281
label = components.slice(1).join(' ');
282
bookmarksToLabel[bookmark] = label;
283
bookmarksOrder.push(bookmark);
286
for (let i = 0; i < bookmarksOrder.length; i++) {
287
let bookmark = bookmarksOrder[i];
288
let label = bookmarksToLabel[bookmark];
289
let file = Gio.file_new_for_uri(bookmark);
290
if (!file.query_exists(null))
293
label = Shell.util_get_label_for_uri(bookmark);
296
let icon = Shell.util_get_icon_for_uri(bookmark);
298
let item = new PlaceInfo('bookmark:' + bookmark, label,
300
return St.TextureCache.get_default().load_gicon(null, icon, size);
303
Gio.app_info_launch_default_for_uri(bookmark, _makeLaunchContext(params));
305
this._bookmarks.push(item);
308
/* See comment in _updateDevices for explanation why there are two signals. */
309
this.emit('bookmarks-updated');
310
this.emit('places-updated');
313
_addMount: function(mount) {
314
let devItem = new PlaceDeviceInfo(mount);
315
this._mounts.push(devItem);
318
getAllPlaces: function () {
319
return this.getDefaultPlaces().concat(this.getBookmarks(), this.getMounts());
322
getDefaultPlaces: function () {
323
return this._defaultPlaces;
326
getBookmarks: function () {
327
return this._bookmarks;
330
getMounts: function () {
334
_lookupIndexById: function(sourceArray, id) {
335
for (let i = 0; i < sourceArray.length; i++) {
336
let place = sourceArray[i];
343
lookupPlaceById: function(id) {
344
let colonIdx = id.indexOf(':');
345
let type = id.substring(0, colonIdx);
346
let sourceArray = null;
347
if (type == 'special')
348
sourceArray = this._defaultPlaces;
349
else if (type == 'mount')
350
sourceArray = this._mounts;
351
else if (type == 'bookmark')
352
sourceArray = this._bookmarks;
353
return sourceArray[this._lookupIndexById(sourceArray, id)];
356
_removeById: function(sourceArray, id) {
357
sourceArray.splice(this._lookupIndexById(sourceArray, id), 1);
360
Signals.addSignalMethods(PlacesManager.prototype);
362
const PlaceSearchProvider = new Lang.Class({
363
Name: 'PlaceSearchProvider',
364
Extends: Search.SearchProvider,
367
this.parent(_("PLACES & DEVICES"));
370
getResultMetas: function(resultIds) {
372
for (let i = 0; i < resultIds.length; i++) {
373
let placeInfo = Main.placesManager.lookupPlaceById(resultIds[i]);
377
metas.push({ 'id': resultIds[i],
378
'name': placeInfo.name,
379
'createIcon': function(size) {
380
return placeInfo.iconFactory(size);
387
activateResult: function(id, params) {
388
let placeInfo = Main.placesManager.lookupPlaceById(id);
389
placeInfo.launch(params);
392
_compareResultMeta: function (idA, idB) {
393
let infoA = Main.placesManager.lookupPlaceById(idA);
394
let infoB = Main.placesManager.lookupPlaceById(idB);
395
return infoA.name.localeCompare(infoB.name);
398
_searchPlaces: function(places, terms) {
399
let multiplePrefixResults = [];
400
let prefixResults = [];
401
let multipleSubstringResults = [];
402
let substringResults = [];
404
terms = terms.map(String.toLowerCase);
406
for (let i = 0; i < places.length; i++) {
407
let place = places[i];
408
let mtype = place.matchTerms(terms);
409
if (mtype == Search.MatchType.MULTIPLE_PREFIX)
410
multiplePrefixResults.push(place.id);
411
else if (mtype == Search.MatchType.PREFIX)
412
prefixResults.push(place.id);
413
else if (mtype == Search.MatchType.MULTIPLE_SUBSTRING)
414
multipleSubstringResults.push(place.id);
415
else if (mtype == Search.MatchType.SUBSTRING)
416
substringResults.push(place.id);
418
multiplePrefixResults.sort(this._compareResultMeta);
419
prefixResults.sort(this._compareResultMeta);
420
multipleSubstringResults.sort(this._compareResultMeta);
421
substringResults.sort(this._compareResultMeta);
422
return multiplePrefixResults.concat(prefixResults.concat(multipleSubstringResults.concat(substringResults)));
425
getInitialResultSet: function(terms) {
426
let places = Main.placesManager.getAllPlaces();
427
return this._searchPlaces(places, terms);
430
getSubsearchResultSet: function(previousResults, terms) {
431
let places = previousResults.map(function (id) { return Main.placesManager.lookupPlaceById(id); });
432
return this._searchPlaces(places, terms);