diff --git a/extensions/places-menu/Makefile.am b/extensions/places-menu/Makefile.am index 19b537f0..7096386e 100644 --- a/extensions/places-menu/Makefile.am +++ b/extensions/places-menu/Makefile.am @@ -1,3 +1,5 @@ EXTENSION_ID = places-menu +EXTRA_MODULES = placeDisplay.js + include ../../extension.mk diff --git a/extensions/places-menu/extension.js b/extensions/places-menu/extension.js index 5121381a..27a0d2fe 100644 --- a/extensions/places-menu/extension.js +++ b/extensions/places-menu/extension.js @@ -10,46 +10,43 @@ const Main = imports.ui.main; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; const Panel = imports.ui.panel; -const PlaceDisplay = imports.ui.placeDisplay; const Gettext = imports.gettext.domain('gnome-shell-extensions'); const _ = Gettext.gettext; +const N_ = function(x) { return x; } const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = Me.imports.convenience; +const PlaceDisplay = Me.imports.placeDisplay; const PLACE_ICON_SIZE = 16; -function iconForPlace(place) { - let split = place.id.split(':'); - let kind = split.shift(); - let uri = split.join(':'); +const PlaceMenuItem = new Lang.Class({ + Name: 'PlaceMenuItem', + Extends: PopupMenu.PopupMenuItem, - let gicon = new Gio.ThemedIcon({ name: 'folder-symbolic' }); - switch(kind) { - case 'special': - switch(uri) { - case 'home': - gicon = new Gio.ThemedIcon({ name: 'user-home-symbolic' }); - break; - case 'desktop': - // FIXME: There is no user-desktop-symbolic - gicon = new Gio.ThemedIcon({ name: 'folder-symbolic' }); - break; - } - break; - case 'bookmark': - let info = Gio.File.new_for_uri(uri).query_info('standard::symbolic-icon', 0, null); - gicon = info.get_symbolic_icon(info); - break; - case 'mount': - gicon = place._mount.get_symbolic_icon(); - break; - } + _init: function(info) { + this.parent(info.name); + this._info = info; - return new St.Icon({ gicon: gicon, - icon_size: PLACE_ICON_SIZE }); + this.addActor(new St.Icon({ gicon: info.icon, + icon_size: PLACE_ICON_SIZE }), + { align: St.Align.END, span: -1 }); + }, + + activate: function(event) { + this._info.launch(event.get_time()); + + this.parent(event); + }, +}); + +const SECTIONS = { + 'special': N_("Places"), + 'devices': N_("Devices"), + 'bookmarks': N_("Bookmarks"), + 'network': N_("Network") } const PlacesMenu = new Lang.Class({ @@ -60,98 +57,45 @@ const PlacesMenu = new Lang.Class({ this.parent('folder-symbolic'); this.placesManager = new PlaceDisplay.PlacesManager(); - this.defaultItems = []; - this.bookmarkItems = []; - this.deviceItems = []; - this._createDefaultPlaces(); - this._bookmarksSection = new PopupMenu.PopupMenuSection(); - this.menu.addMenuItem(this._bookmarksSection); - this._createBookmarks(); - this._devicesMenuItem = new PopupMenu.PopupSubMenuMenuItem(_("Removable Devices")); - this.menu.addMenuItem(this._devicesMenuItem); - this._createDevices(); + this._sections = { }; - this._bookmarksId = this.placesManager.connect('bookmarks-updated',Lang.bind(this,this._redisplayBookmarks)); - this._mountsId = this.placesManager.connect('mounts-updated',Lang.bind(this,this._redisplayDevices)); + for (let foo in SECTIONS) { + let id = foo; // stupid JS closure semantics... + this._sections[id] = { section: new PopupMenu.PopupMenuSection(), + title: Gettext.gettext(SECTIONS[id]) }; + this.placesManager.connect(id + '-updated', Lang.bind(this, function() { + this._redisplay(id); + })); + + this._create(id); + this.menu.addMenuItem(this._sections[id].section); + } }, destroy: function() { - this.placesManager.disconnect(this._bookmarksId); - this.placesManager.disconnect(this._mountsId); + this.placesManager.destroy(); this.parent(); }, - _redisplayBookmarks: function(){ - this._clearBookmarks(); - this._createBookmarks(); + _redisplay: function(id) { + this._sections[id].section.removeAll(); + this._create(id); }, - _redisplayDevices: function(){ - this._clearDevices(); - this._createDevices(); - }, + _create: function(id) { + let title = new PopupMenu.PopupMenuItem(this._sections[id].title, + { reactive: false, + style_class: 'popup-subtitle-menu-item' }); + this._sections[id].section.addMenuItem(title); - _createDefaultPlaces : function() { - this.defaultPlaces = this.placesManager.getDefaultPlaces(); + let places = this.placesManager.get(id); - for (let placeid = 0; placeid < this.defaultPlaces.length; placeid++) { - this.defaultItems[placeid] = new PopupMenu.PopupMenuItem(this.defaultPlaces[placeid].name); - let icon = iconForPlace(this.defaultPlaces[placeid]); - this.defaultItems[placeid].addActor(icon, { align: St.Align.END, span: -1 }); - this.defaultItems[placeid].place = this.defaultPlaces[placeid]; - this.menu.addMenuItem(this.defaultItems[placeid]); - this.defaultItems[placeid].connect('activate', function(actor,event) { - actor.place.launch(); - }); + for (let i = 0; i < places.length; i++) + this._sections[id].section.addMenuItem(new PlaceMenuItem(places[i])); - } - }, - - _createBookmarks : function() { - this.bookmarks = this.placesManager.getBookmarks(); - - for (let bookmarkid = 0; bookmarkid < this.bookmarks.length; bookmarkid++) { - this.bookmarkItems[bookmarkid] = new PopupMenu.PopupMenuItem(this.bookmarks[bookmarkid].name); - let icon = iconForPlace(this.bookmarks[bookmarkid]); - this.bookmarkItems[bookmarkid].addActor(icon, { align: St.Align.END, span: -1 }); - this.bookmarkItems[bookmarkid].place = this.bookmarks[bookmarkid]; - this._bookmarksSection.addMenuItem(this.bookmarkItems[bookmarkid]); - this.bookmarkItems[bookmarkid].connect('activate', function(actor,event) { - actor.place.launch(); - }); - } - }, - - _createDevices : function() { - this.devices = this.placesManager.getMounts(); - - for (let devid = 0; devid < this.devices.length; devid++) { - this.deviceItems[devid] = new PopupMenu.PopupMenuItem(this.devices[devid].name); - let icon = iconForPlace(this.devices[devid]); - this.deviceItems[devid].addActor(icon, { align: St.Align.END, span: -1 }); - this.deviceItems[devid].place = this.devices[devid]; - this._devicesMenuItem.menu.addMenuItem(this.deviceItems[devid]); - this.deviceItems[devid].connect('activate', function(actor,event) { - actor.place.launch(); - }); - } - - if (this.devices.length == 0) - this._devicesMenuItem.actor.hide(); - else - this._devicesMenuItem.actor.show(); - }, - - _clearBookmarks : function(){ - this._bookmarksSection.removeAll(); - this.bookmarkItems = []; - }, - - _clearDevices : function(){ - this._devicesMenuItem.menu.removeAll(); - this.deviceItems = []; - }, + this._sections[id].section.actor.visible = places.length > 0; + } }); function init() { diff --git a/extensions/places-menu/placeDisplay.js b/extensions/places-menu/placeDisplay.js new file mode 100644 index 00000000..8984d068 --- /dev/null +++ b/extensions/places-menu/placeDisplay.js @@ -0,0 +1,304 @@ +// -*- 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 Util = imports.misc.util; + +const Gettext = imports.gettext.domain('gnome-shell-extensions'); +const _ = Gettext.gettext; +const N_ = function(x) { return x; } + +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(); + }, + + isRemovable: function() { + return false; + }, + + launch: function(timestamp) { + let launchContext = global.create_app_launch_context(); + launchContext.set_timestamp(timestamp); + + try { + Gio.AppInfo.launch_default_for_uri(this.file.get_uri(), + launchContext); + } catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) { + this.file.mount_enclosing_volume(0, null, null, function(file, result) { + file.mount_enclosing_volume_finish(result); + Gio.AppInfo.launch_default_for_uri(file.get_uri(), launchContext); + }); + } catch(e) { + Main.notifyError(_("Failed to launch \"%s\"").format(this.name), e.message); + } + }, + + getIcon: function() { + try { + let info = this.file.query_info('standard::symbolic-icon', 0, null); + return info.get_symbolic_icon(); + } catch(e if e instanceof Gio.IOErrorEnum) { + // return a generic icon for this kind + 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.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_SUPPORTED)) { + return this.file.get_basename(); + } + }, +}); + +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 DEFAULT_DIRECTORIES = [ + GLib.UserDirectory.DIRECTORY_DOCUMENTS, + GLib.UserDirectory.DIRECTORY_PICTURES, + GLib.UserDirectory.DIRECTORY_MUSIC, + GLib.UserDirectory.DIRECTORY_DOWNLOAD, + GLib.UserDirectory.DIRECTORY_VIDEOS, +]; + +const PlacesManager = new Lang.Class({ + Name: 'PlacesManager', + + _init: function() { + this._places = { + special: [], + devices: [], + bookmarks: [], + network: [], + }; + + let homePath = GLib.get_home_dir(); + + this._places.special.push(new PlaceInfo('special', + Gio.File.new_for_path(homePath), + _("Home"))); + for (let i = 0; i < DEFAULT_DIRECTORIES.length; i++) { + let specialPath = GLib.get_user_special_dir(DEFAULT_DIRECTORIES[i]); + if (specialPath == homePath) + continue; + this._places.special.push(new PlaceInfo('special', + Gio.File.new_for_path(specialPath))); + } + + /* + * Show devices, code more or less ported from nautilus-places-sidebar.c + */ + this._volumeMonitor = Gio.VolumeMonitor.get(); + this._connectVolumeMonitorSignals(); + this._updateMounts(); + + this._bookmarksPath = GLib.build_filenamev([GLib.get_user_config_dir(), 'gtk-3.0', 'bookmarks']); + this._bookmarksFile = Gio.file_new_for_path(this._bookmarksPath); + this._monitor = this._bookmarksFile.monitor_file(Gio.FileMonitorFlags.NONE, null); + this._bookmarkTimeoutId = 0; + 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() { + for (let i = 0; i < this._volumeMonitorSignals.length; i++) + this._volumeMonitor.disconnect(this._volumeMonitorSignals[i]); + + this._monitor.cancel(); + if (this._bookmarkTimeoutId) + Mainloop.source_remove(this._bookmarkTimeoutId); + }, + + _updateMounts: function() { + this._places.devices = []; + this._places.network = []; + + /* Add standard places */ + this._places.devices.push(new PlaceInfo('devices', + Gio.File.new_for_path('/'), + _("File System"), + 'drive-harddisk-symbolic')); + 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 mount = volumes[j].get_mount(); + let kind = 'devices'; + if (volumes[j].get_identifier('class').indexOf('network') >= 0) + kind = 'network'; + + if(mount != null) + this._addMount(kind, 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 kind = 'devices'; + if (volumes.get_identifier('class').indexOf('network') >= 0) + kind = 'network'; + + let mount = volumes[i].get_mount(); + if(mount != null) + this._addMount(kind, 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(); + let kind; + if (root.is_native()) + kind = 'devices'; + else + kind = 'network'; + + this._addMount(kind, mounts[i]); + } + + this.emit('devices-updated'); + this.emit('network-updated'); + }, + + _reloadBookmarks: function() { + + this._bookmarks = []; + + if (!GLib.file_test(this._bookmarksPath, GLib.FileTest.EXISTS)) + return; + + let content = Shell.get_file_contents_utf8_sync(this._bookmarksPath); + 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); + 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 = new PlaceDeviceInfo(kind, mount); + this._places[kind].push(devItem); + }, + + get: function (kind) { + return this._places[kind]; + } +}); +Signals.addSignalMethods(PlacesManager.prototype);