// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
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 DND = imports.ui.dnd;
const Main = imports.ui.main;
const Params = imports.misc.params;
const Search = imports.ui.search;
const ShellMountOperation = imports.ui.shellMountOperation;
const Util = imports.misc.util;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const N_ = function(x) { return x; }
const BACKGROUND_SCHEMA = 'org.gnome.desktop.background';
const Hostname1Iface = '<node> \
<interface name="org.freedesktop.hostname1"> \
<property name="PrettyHostname" type="s" access="read" /> \
</interface> \
</node>';
const Hostname1 = Gio.DBusProxy.makeProxyWrapper(Hostname1Iface);
const PlaceInfo = new Lang.Class({
Name: 'PlaceInfo',
_init: function(kind, file, name, icon) {
this.kind = kind;
this.file = file;
this.name = name || this._getFileName();
this.icon = icon ? new Gio.ThemedIcon({ name: icon }) : this.getIcon();
},
destroy: function() {
},
isRemovable: function() {
return false;
},
_createLaunchCallback: function(launchContext, tryMount) {
return (_ignored, result) => {
try {
Gio.AppInfo.launch_default_for_uri_finish(result);
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) {
let source = {
get_icon: () => { return this.icon; }
};
let op = new ShellMountOperation.ShellMountOperation(source);
this.file.mount_enclosing_volume(0, op.mountOp, null, (file, result) => {
try {
op.close();
file.mount_enclosing_volume_finish(result);
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED)) {
// e.g. user canceled the password dialog
return;
} catch(e) {
Main.notifyError(_("Failed to mount volume for ā%sā").format(this.name), e.message);
return;
}
if (tryMount) {
let callback = this._createLaunchCallback(launchContext, false);
Gio.AppInfo.launch_default_for_uri_async(file.get_uri(),
launchContext,
null,
callback);
}
});
} catch(e) {
Main.notifyError(_("Failed to launch ā%sā").format(this.name), e.message);
}
}
},
launch: function(timestamp) {
let launchContext = global.create_app_launch_context(timestamp, -1);
let callback = this._createLaunchCallback(launchContext, true);
Gio.AppInfo.launch_default_for_uri_async(this.file.get_uri(),
launchContext,
null,
callback);
},
getIcon: function() {
this.file.query_info_async('standard::symbolic-icon', 0, 0, null,
Lang.bind(this, function(file, result) {
try {
let info = file.query_info_finish(result);
this.icon = info.get_symbolic_icon();
this.emit('changed');
} catch(e if e instanceof Gio.IOErrorEnum) {
return;
}
}));
// return a generic icon for this kind for now, until we have the
// icon from the query info above
switch (this.kind) {
case 'network':
return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' });
case 'devices':
return new Gio.ThemedIcon({ name: 'drive-harddisk-symbolic' });
case 'special':
case 'bookmarks':
default:
if (!this.file.is_native())
return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' });
else
return new Gio.ThemedIcon({ name: 'folder-symbolic' });
}
},
_getFileName: function() {
try {
let info = this.file.query_info('standard::display-name', 0, null);
return info.get_display_name();
} catch(e if e instanceof Gio.IOErrorEnum) {
return this.file.get_basename();
}
},
});
Signals.addSignalMethods(PlaceInfo.prototype);
const RootInfo = new Lang.Class({
Name: 'RootInfo',
Extends: PlaceInfo,
_init: function() {
this.parent('devices', Gio.File.new_for_path('/'), _("Computer"));
this._proxy = new Hostname1(Gio.DBus.system,
'org.freedesktop.hostname1',
'/org/freedesktop/hostname1',
Lang.bind(this, function(obj, error) {
if (error)
return;
this._proxy.connect('g-properties-changed',
Lang.bind(this, this._propertiesChanged));
this._propertiesChanged(obj);
}));
},
getIcon: function() {
return new Gio.ThemedIcon({ name: 'drive-harddisk-symbolic' });
},
_propertiesChanged: function(proxy) {
// GDBusProxy will emit a g-properties-changed when hostname1 goes down
// ignore it
if (proxy.g_name_owner) {
this.name = proxy.PrettyHostname || _("Computer");
this.emit('changed');
}
},
destroy: function() {
this._proxy.run_dispose();
this.parent();
}
});
const PlaceDeviceInfo = new Lang.Class({
Name: 'PlaceDeviceInfo',
Extends: PlaceInfo,
_init: function(kind, mount) {
this._mount = mount;
this.parent(kind, mount.get_root(), mount.get_name());
},
getIcon: function() {
return this._mount.get_symbolic_icon();
}
});
const PlaceVolumeInfo = new Lang.Class({
Name: 'PlaceVolumeInfo',
Extends: PlaceInfo,
_init: function(kind, volume) {
this._volume = volume;
this.parent(kind, volume.get_activation_root(), volume.get_name());
},
launch: function(timestamp) {
if (this.file) {
this.parent(timestamp);
return;
}
this._volume.mount(0, null, null, Lang.bind(this, function(volume, result) {
volume.mount_finish(result);
let mount = volume.get_mount();
this.file = mount.get_root();
this.parent(timestamp);
}));
},
getIcon: function() {
return this._volume.get_symbolic_icon();
}
});
const DEFAULT_DIRECTORIES = [
GLib.UserDirectory.DIRECTORY_DOCUMENTS,
GLib.UserDirectory.DIRECTORY_PICTURES,
GLib.UserDirectory.DIRECTORY_MUSIC,
GLib.UserDirectory.DIRECTORY_DOWNLOAD,
GLib.UserDirectory.DIRECTORY_VIDEOS,
];
var PlacesManager = new Lang.Class({
Name: 'PlacesManager',
_init: function() {
this._places = {
special: [],
devices: [],
bookmarks: [],
network: [],
};
this._settings = new Gio.Settings({ schema_id: BACKGROUND_SCHEMA });
this._showDesktopIconsChangedId =
this._settings.connect('changed::show-desktop-icons',
Lang.bind(this, this._updateSpecials));
this._updateSpecials();
/*
* Show devices, code more or less ported from nautilus-places-sidebar.c
*/
this._volumeMonitor = Gio.VolumeMonitor.get();
this._connectVolumeMonitorSignals();
this._updateMounts();
this._bookmarksFile = this._findBookmarksFile()
this._bookmarkTimeoutId = 0;
this._monitor = null;
if (this._bookmarksFile) {
this._monitor = this._bookmarksFile.monitor_file(Gio.FileMonitorFlags.NONE, null);
this._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();
}
},
_connectVolumeMonitorSignals: function() {
const signals = ['volume-added', 'volume-removed', 'volume-changed',
'mount-added', 'mount-removed', 'mount-changed',
'drive-connected', 'drive-disconnected', 'drive-changed'];
this._volumeMonitorSignals = [];
let func = Lang.bind(this, this._updateMounts);
for (let i = 0; i < signals.length; i++) {
let id = this._volumeMonitor.connect(signals[i], func);
this._volumeMonitorSignals.push(id);
}
},
destroy: function() {
if (this._settings)
this._settings.disconnect(this._showDesktopIconsChangedId);
this._settings = null;
for (let i = 0; i < this._volumeMonitorSignals.length; i++)
this._volumeMonitor.disconnect(this._volumeMonitorSignals[i]);
if (this._monitor)
this._monitor.cancel();
if (this._bookmarkTimeoutId)
Mainloop.source_remove(this._bookmarkTimeoutId);
},
_updateSpecials: function() {
this._places.special.forEach(function (p) { p.destroy(); });
this._places.special = [];
let homePath = GLib.get_home_dir();
this._places.special.push(new PlaceInfo('special',
Gio.File.new_for_path(homePath),
_("Home")));
let specials = [];
let dirs = DEFAULT_DIRECTORIES.slice();
if (this._settings.get_boolean('show-desktop-icons'))
dirs.push(GLib.UserDirectory.DIRECTORY_DESKTOP);
for (let i = 0; i < dirs.length; i++) {
let specialPath = GLib.get_user_special_dir(dirs[i]);
if (specialPath == null || specialPath == homePath)
continue;
let file = Gio.File.new_for_path(specialPath), info;
try {
info = new PlaceInfo('special', file);
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
continue;
}
specials.push(info);
}
specials.sort(function(a, b) {
return GLib.utf8_collate(a.name, b.name);
});
this._places.special = this._places.special.concat(specials);
this.emit('special-updated');
},
_updateMounts: function() {
let networkMounts = [];
let networkVolumes = [];
this._places.devices.forEach(function (p) { p.destroy(); });
this._places.devices = [];
this._places.network.forEach(function (p) { p.destroy(); });
this._places.network = [];
/* Add standard places */
this._places.devices.push(new RootInfo());
this._places.network.push(new PlaceInfo('network',
Gio.File.new_for_uri('network:///'),
_("Browse Network"),
'network-workgroup-symbolic'));
/* 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 identifier = volumes[j].get_identifier('class');
if (identifier && identifier.indexOf('network') >= 0) {
networkVolumes.push(volumes[j]);
} else {
let mount = volumes[j].get_mount();
if(mount != null)
this._addMount('devices', 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 identifier = volumes[i].get_identifier('class');
if (identifier && identifier.indexOf('network') >= 0) {
networkVolumes.push(volumes[i]);
} else {
let mount = volumes[i].get_mount();
if(mount != null)
this._addMount('devices', 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;
let root = mounts[i].get_default_location();
if (!root.is_native()) {
networkMounts.push(mounts[i]);
continue;
}
this._addMount('devices', mounts[i]);
}
for (let i = 0; i < networkVolumes.length; i++) {
let mount = networkVolumes[i].get_mount();
if (mount) {
networkMounts.push(mount);
continue;
}
this._addVolume('network', networkVolumes[i]);
}
for (let i = 0; i < networkMounts.length; i++) {
this._addMount('network', networkMounts[i]);
}
this.emit('devices-updated');
this.emit('network-updated');
},
_findBookmarksFile: function() {
let paths = [
GLib.build_filenamev([GLib.get_user_config_dir(), 'gtk-3.0', 'bookmarks']),
GLib.build_filenamev([GLib.get_home_dir(), '.gtk-bookmarks']),
];
for (let i = 0; i < paths.length; i++) {
if (GLib.file_test(paths[i], GLib.FileTest.EXISTS))
return Gio.File.new_for_path(paths[i]);
}
return null;
},
_reloadBookmarks: function() {
this._bookmarks = [];
let content = Shell.get_file_contents_utf8_sync(this._bookmarksFile.get_path());
let lines = content.split('\n');
let bookmarks = [];
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let components = line.split(' ');
let bookmark = components[0];
if (!bookmark)
continue;
let file = Gio.File.new_for_uri(bookmark);
if (file.is_native() && !file.query_exists(null))
continue;
let duplicate = false;
for (let i = 0; i < this._places.special.length; i++) {
if (file.equal(this._places.special[i].file)) {
duplicate = true;
break;
}
}
if (duplicate)
continue;
for (let i = 0; i < bookmarks.length; i++) {
if (file.equal(bookmarks[i].file)) {
duplicate = true;
break;
}
}
if (duplicate)
continue;
let label = null;
if (components.length > 1)
label = components.slice(1).join(' ');
bookmarks.push(new PlaceInfo('bookmarks', file, label));
}
this._places.bookmarks = bookmarks;
this.emit('bookmarks-updated');
},
_addMount: function(kind, mount) {
let devItem;
try {
devItem = new PlaceDeviceInfo(kind, mount);
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
return;
}
this._places[kind].push(devItem);
},
_addVolume: function(kind, volume) {
let volItem;
try {
volItem = new PlaceVolumeInfo(kind, volume);
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
return;
}
this._places[kind].push(volItem);
},
get: function (kind) {
return this._places[kind];
}
});
Signals.addSignalMethods(PlacesManager.prototype);