[gnome-shell-extensions] places-menu: rework to be more similar to the Files sidebar



commit eda45e607247a13b38f99e6e6206d0df7d1f651d
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Thu Sep 6 19:25:54 2012 +0200

    places-menu: rework to be more similar to the Files sidebar
    
    Let's go GNOME 3 style, and make a places menu that looks like the sidebar
    in Files, with for subsections corresponding to default places, devices,
    bookmarks and network mounts.
    Among other things, this should fix the duplicate home or duplicate desktop
    bookmark problem (as now we don't bookmarks that are same as the default
    locations), and it makes us future proof for the removal of PlacesManager
    in core shell.

 extensions/places-menu/Makefile.am     |    2 +
 extensions/places-menu/extension.js    |  162 ++++++------------
 extensions/places-menu/placeDisplay.js |  304 ++++++++++++++++++++++++++++++++
 3 files changed, 359 insertions(+), 109 deletions(-)
---
diff --git a/extensions/places-menu/Makefile.am b/extensions/places-menu/Makefile.am
index 19b537f..7096386 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 5121381..27a0d2f 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(':');
-
-    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;
-    }
+const PlaceMenuItem = new Lang.Class({
+    Name: 'PlaceMenuItem',
+    Extends: PopupMenu.PopupMenuItem,
+
+    _init: function(info) {
+	this.parent(info.name);
+	this._info = info;
+
+	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());
 
-    return new St.Icon({ gicon: gicon,
-			 icon_size: PLACE_ICON_SIZE });
+	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._bookmarksId = this.placesManager.connect('bookmarks-updated',Lang.bind(this,this._redisplayBookmarks));
-        this._mountsId = this.placesManager.connect('mounts-updated',Lang.bind(this,this._redisplayDevices));
-    },
+	this._sections = { };
 
-    destroy: function() {
-        this.placesManager.disconnect(this._bookmarksId);
-        this.placesManager.disconnect(this._mountsId);
+	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.parent();
+	    this._create(id);
+	    this.menu.addMenuItem(this._sections[id].section);
+	}
     },
 
-    _redisplayBookmarks: function(){
-        this._clearBookmarks();
-        this._createBookmarks();
-    },
+    destroy: function() {
+	this.placesManager.destroy();
 
-    _redisplayDevices: function(){
-        this._clearDevices();
-        this._createDevices();
+        this.parent();
     },
 
-    _createDefaultPlaces : function() {
-        this.defaultPlaces = this.placesManager.getDefaultPlaces();
-
-        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();
-            });
-
-        }
+    _redisplay: function(id) {
+	this._sections[id].section.removeAll();
+        this._create(id);
     },
 
-    _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();
-            });
-        }
-    },
+    _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);
 
-    _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();
-    },
+        let places = this.placesManager.get(id);
 
-    _clearBookmarks : function(){
-        this._bookmarksSection.removeAll();
-        this.bookmarkItems = [];
-    },
+        for (let i = 0; i < places.length; i++)
+            this._sections[id].section.addMenuItem(new PlaceMenuItem(places[i]));
 
-    _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 0000000..8984d06
--- /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);



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]