[gnome-shell/hotplug: 1/21] autorun: add a AutorunManager class



commit 0337ebfc3904e462a7d07c9f0a18f76b2954a71f
Author: Cosimo Cecchi <cosimoc gnome org>
Date:   Mon Jun 20 10:05:19 2011 -0400

    autorun: add a AutorunManager class
    
    First implementation of AutorunManager

 data/theme/gnome-shell.css |   77 +++++++
 js/Makefile.am             |    1 +
 js/ui/autorunManager.js    |  521 ++++++++++++++++++++++++++++++++++++++++++++
 js/ui/main.js              |    6 +
 4 files changed, 605 insertions(+), 0 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 7e7e786..a63e338 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -1136,6 +1136,83 @@ StTooltip StLabel {
     icon-size: 36px;
 }
 
+.hotplug-transient-box {
+    spacing: 6px;
+    padding: 2px 72px 2px 12px;
+}
+
+.hotplug-notification-item {
+    background-color: #3c3c3c;
+    padding: 0px 10px;
+    border-radius: 8px;
+    border: 1px solid #181818;
+}
+
+.hotplug-notification-item:hover {
+    border: 1px solid #a1a1a1;
+}
+
+.hotplug-notification-item:focus {
+    background-color: #666666;
+}
+
+.hotplug-notification-item:active {
+    border: 1px solid #a1a1a1;
+    background-color: #2b2b2b;
+}
+
+.hotplug-notification-item-icon {
+    icon-size: 24px;
+    padding: 2px 5px;
+}
+
+.hotplug-resident-box {
+    spacing: 8px;
+    padding-right: 72px;
+}
+
+.hotplug-resident-mount {
+    spacing: 8px;
+    border-radius: 4px;
+
+    color: #ccc;
+}
+
+.hotplug-resident-mount:hover {
+    background-gradient-direction: horizontal;
+    background-gradient-start: rgba(255, 255, 255, 0.1);
+    background-gradient-end: rgba(255, 255, 255, 0);
+
+    color: #fff;
+}
+
+.hotplug-resident-mount-label {
+    color: inherit;
+}
+
+.hotplug-resident-mount-icon {
+    icon-size: 24px;
+    padding-left: 6px;
+}
+
+.hotplug-resident-eject-icon {
+    icon-size: 24px;
+}
+
+.hotplug-resident-eject-button {
+    padding: 2px;
+    border: 1px solid #2b2b2b;
+    border-radius: 8px;
+
+    color: #ccc;
+}
+
+.hotplug-resident-eject-button:hover {
+    color: #fff;
+    background-color: #2b2b2b;
+    border: 1px solid #a1a1a1;
+}
+
 .chat-log-message {
     color: #888888;
 }
diff --git a/js/Makefile.am b/js/Makefile.am
index a085bfc..55deff2 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -15,6 +15,7 @@ nobase_dist_js_DATA = 	\
 	ui/altTab.js		\
 	ui/appDisplay.js	\
 	ui/appFavorites.js	\
+	ui/autorunManager.js    \
 	ui/boxpointer.js	\
 	ui/calendar.js		\
 	ui/chrome.js		\
diff --git a/js/ui/autorunManager.js b/js/ui/autorunManager.js
new file mode 100644
index 0000000..4bad96b
--- /dev/null
+++ b/js/ui/autorunManager.js
@@ -0,0 +1,521 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Lang = imports.lang;
+const Gio = imports.gi.Gio;
+const St = imports.gi.St;
+
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+
+// GSettings keys
+const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
+const SETTING_DISABLE_AUTORUN = 'autorun-never';
+
+const HOTPLUG_ICON_SIZE = 16;
+
+function ContentTypeDiscoverer(callback) {
+    this._init(callback);
+}
+
+ContentTypeDiscoverer.prototype = {
+    _init: function(callback) {
+        this._callback = callback;
+    },
+
+    guessContentTypes: function(mount) {
+        // guess mount's content type using GIO
+        mount.guess_content_type(false, null,
+                                 Lang.bind(this,
+                                           this._onContentTypeGuessed));
+    },
+
+    _onContentTypeGuessed: function(mount, res) {
+        let contentTypes = [];
+
+        try {
+            contentTypes = mount.guess_content_type_finish(res);
+        } catch (e) {
+            log('Unable to guess content types on added mount ' + mount.get_name()
+                + ': ' + e.toString());
+        }
+
+        // we're not interested in win32 software content types here
+        contentTypes = contentTypes.filter(function(type) {
+            return (type != 'x-content/win32-software');
+        });
+
+        this._callback(mount, contentTypes);
+    }
+}
+
+function AutorunManager() {
+    this._init();
+}
+
+AutorunManager.prototype = {
+    _init: function() {
+        this._initVolumeMonitor();
+
+        this._initTransientDispatcher();
+        this._initResidentSource();
+    },
+
+    _initTransientDispatcher: function() {
+        this._transDispatcher = 
+            new AutorunTransientDispatcher();
+    },
+
+    _initResidentSource: function() {
+        this._residentSource =
+            new AutorunResidentSource();
+
+        let mounts = this._volumeMonitor.get_mounts();
+
+        mounts.forEach(Lang.bind(this, function (mount) {
+            let discoverer = new ContentTypeDiscoverer(
+                Lang.bind (this, function (mount, contentTypes) {
+                    this._residentSource.addMount(mount, contentTypes);
+                }));
+
+            discoverer.guessContentTypes(mount);
+        }));
+    },
+
+    _initVolumeMonitor: function() {
+        this._volumeMonitor = Gio.VolumeMonitor.get();
+
+        this._volumeMonitor.connect('mount-added',
+                                    Lang.bind(this,
+                                              this._onMountAdded));
+        this._volumeMonitor.connect('mount-removed',
+                                    Lang.bind(this,
+                                              this._onMountRemoved));
+    },
+
+    _onMountAdded: function(monitor, mount) {
+        // don't do anything if our session is not the currently
+        // active one
+        if (!Main.automountManager.ckListener.sessionActive)
+            return;
+
+        let discoverer = new ContentTypeDiscoverer
+            (Lang.bind (this, function (mount, contentTypes) {
+                this._transDispatcher.addMount(mount, contentTypes);
+                this._residentSource.addMount(mount, contentTypes);
+            }));
+
+        discoverer.guessContentTypes(mount);
+    },
+
+    _onMountRemoved: function(monitor, mount) {
+        this._transDispatcher.removeMount(mount);
+        this._residentSource.removeMount(mount);
+    },
+
+    ejectMount: function(mount) {
+        // TODO: we need to have a StMountOperation here to e.g. trigger
+        // shell dialogs when applications are blocking the mount.
+        if (mount.can_eject())
+            mount.eject_with_operation(0, null, null,
+                                       Lang.bind(this, this._onMountEject));
+        else
+            mount.unmount_with_operation(0, null, null,
+                                         Lang.bind(this, this._onMountEject));
+    },
+
+    _onMountEject: function(mount, res) {
+        try {
+            if (mount.can_eject())
+                mount.eject_with_operation_finish(res);
+            else
+                mount.unmount_with_operation_finish(res);
+        } catch (e) {
+            log('Unable to eject the mount ' + mount.get_name() 
+                + ': ' + e.toString());
+        }
+    },
+}
+
+function AutorunResidentSource() {
+    this._init();
+}
+
+AutorunResidentSource.prototype = {
+    __proto__: MessageTray.Source.prototype,
+
+    _init: function() {
+        MessageTray.Source.prototype._init.call(this, _('Removable Devices'));
+
+        this._mounts = new Array();
+        this._initNotification();
+    },
+
+    _initNotification: function() {
+        this._notification = new AutorunResidentNotification(this);
+        this._setSummaryIcon(this.createNotificationIcon(HOTPLUG_ICON_SIZE));
+    },
+
+    addMount: function(mount, contentTypes) {
+        let filtered = this._mounts.filter(function (element) {
+            return (element.mount == mount);
+        });
+
+        if (filtered.length != 0)
+            return;
+
+        let element = { mount: mount, contentTypes: contentTypes };
+        this._mounts.push(element);
+        this._redisplay();
+    },
+
+    removeMount: function(mount) {
+        this._mounts =
+            this._mounts.filter(function (element) {
+                return (element.mount != mount);
+            });
+
+        this._redisplay();
+    },
+
+    _redisplay: function() {
+        if (this._mounts.length == 0) {
+            this._notification.destroy();
+            this.destroy();
+
+            // reset the notification for the next time
+            this._initNotification();
+
+            return;
+        }
+
+        this._notification.updateForMounts(this._mounts);
+
+        // add ourselves as a source, and push the notification
+        if (!Main.messageTray.contains(this)) {
+            Main.messageTray.add(this);
+            this.pushNotification(this._notification);
+        }
+    },
+
+    createNotificationIcon: function(iconSize) {
+        return new St.Icon ({ icon_name: 'drive-harddisk',
+                              icon_size: iconSize ? iconSize : this.ICON_SIZE });
+    }
+}
+
+function AutorunResidentNotification(source) {
+    this._init(source);
+}
+
+AutorunResidentNotification.prototype = {
+    __proto__: MessageTray.Notification.prototype,
+
+    _init: function(source) {
+        MessageTray.Notification.prototype._init.call(this, source,
+                                                      source.title, null,
+                                                      { customContent: true });
+
+        // set the notification as resident
+        this.setResident(true);
+
+        this._layout = new St.BoxLayout ({ style_class: 'hotplug-resident-box',
+                                           vertical: true });
+
+        this.addActor(this._layout,
+                      { x_expand: true,
+                        x_fill: true });
+    },
+
+    updateForMounts: function(mounts) {
+        // remove all the layout content
+        this._layout.destroy_children();
+
+        for (let idx = 0; idx < mounts.length; idx++) {
+            let element = mounts[idx];
+
+            let actor = this._itemForMount(element.mount, element.contentTypes);
+            this._layout.add(actor, { x_fill: true,
+                                      expand: true });
+        }
+    },
+
+    _itemForMount: function(mount, contentTypes) {
+        let item = new St.BoxLayout();
+
+        let mountLayout = new St.BoxLayout({ style_class: 'hotplug-resident-mount',
+                                             track_hover: true,
+                                             reactive: true });
+        item.add(mountLayout, { x_align: St.Align.START,
+                                expand: true });
+
+        let mountIcon = new St.Icon({ gicon: mount.get_icon(),
+                                      style_class: 'hotplug-resident-mount-icon' });
+        mountLayout.add_actor(mountIcon);
+
+        let labelBin = new St.Bin({ y_align: St.Align.MIDDLE });
+        let mountLabel =
+            new St.Label ({ text: mount.get_name(),
+                            style_class: 'hotplug-resident-mount-label',
+                            track_hover: true,
+                            reactive: true });
+        labelBin.add_actor(mountLabel);
+        mountLayout.add_actor(labelBin);
+
+        let ejectButton = new St.Button({ 
+            style_class: 'hotplug-resident-eject-button',
+            button_mask: St.ButtonMask.ONE,
+            child: new St.Icon
+            ({ icon_name: 'media-eject',
+               style_class: 'hotplug-resident-eject-icon' })});
+
+        item.add(ejectButton, { x_align: St.Align.END });
+
+        // now connect signals
+        mountLayout.connect('button-press-event', Lang.bind(this, function(actor, event) {
+            // ignore clicks not coming from the left mouse button
+            if (event.get_button() != 1)
+                return false;
+
+            // TODO: need to do something better here...
+            if (!contentTypes.length)
+                contentTypes.push('inode/directory');
+
+            let app = Gio.app_info_get_default_for_type(contentTypes[0], false);
+            let files = [];
+            let root = mount.get_root();
+
+            files.push(root);
+
+            try {
+                app.launch(files, 
+                           global.create_app_launch_context())
+            } catch (e) {
+                log('Unable to launch the application ' + app.get_name()
+                    + ': ' + e.toString());
+            }
+
+            return true;
+        }));
+
+        ejectButton.connect('clicked', Lang.bind(this, function() {
+            Main.autorunManager.ejectMount(mount);
+        }));
+
+        return item;
+    },
+}
+
+function AutorunTransientDispatcher() {
+    this._init();
+}
+
+AutorunTransientDispatcher.prototype = {
+    _init: function() {
+        this._sources = new Array();
+        this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
+    },
+
+    _ignoreAutorunForMount: function(mount) {
+        let root = mount.get_root();
+        let volume = mount.get_volume();
+
+        if ((root.is_native() && !this._mountRootIsHidden(root)) ||
+            (volume && volume.should_automount()))
+            return false;
+
+        return true;
+    },
+
+    _mountRootIsHidden: function(root) {
+        let path = root.get_path();
+
+        // skip any mounts in hidden directory hierarchies
+        return (path.indexOf('/.') != -1);
+    },
+
+    _getSourceForMount: function(mount) {
+        let filtered =
+            this._sources.filter(function (source) {
+                return (source.mount == mount);
+            });
+
+        // we always make sure not to add two sources for the same
+        // mount in addMount(), so it's safe to assume filtered.length
+        // is always either 1 or 0.
+        if (filtered.length == 1)
+            return filtered[0];
+
+        return null;
+    },
+
+    addMount: function(mount, contentTypes) {
+        // if autorun is disabled globally, return
+        if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
+            return;
+
+        // if the mount doesn't want to be autorun, return
+        if (this._ignoreAutorunForMount(mount))
+            return;
+
+        // finally, if we already have a source showing for this 
+        // mount, return
+        if (this._getSourceForMount(mount))
+            return;
+     
+        // add a new source
+        this._sources.push(new AutorunTransientSource(mount, contentTypes));
+    },
+
+    removeMount: function(mount) {
+        let source = this._getSourceForMount(mount);
+        
+        // if we aren't tracking this mount, don't do anything
+        if (!source)
+            return;
+
+        // destroy the notification source
+        source.destroy();
+    }
+}
+
+function AutorunTransientSource(mount, contentTypes) {
+    this._init(mount, contentTypes);
+}
+
+AutorunTransientSource.prototype = {
+    __proto__: MessageTray.Source.prototype,
+
+    _init: function(mount, contentTypes) {
+        MessageTray.Source.prototype._init.call(this, mount.get_name());
+
+        this._mount = mount;
+        this._contentTypes = contentTypes;
+
+        this._buildNotification();
+    },
+
+    _buildNotification: function() {
+        this._notification = new AutorunTransientNotification(this);
+        this._setSummaryIcon(this.createNotificationIcon(this.ICON_SIZE));
+
+        this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
+                                       vertical: true });
+        this._notification.addActor(this._box);
+
+        this._contentTypes.forEach(Lang.bind(this, function (type) {
+            let actor = this._buttonForContentType(type);
+
+            if (actor)
+                this._box.add(actor, { x_fill: true,
+                                       x_align: St.Align.START });
+        }));
+
+        // TODO: ideally we never want to show the file manager entry here,
+        // but we want to detect which kind of files are present on the device,
+        // and use those to present a more meaningful choice.
+        if (this._contentTypes.length == 0)
+            this._box.add (this._buttonForContentType('inode/directory'),
+                           { x_fill: true,
+                             x_align: St.Align.START });
+
+        this._box.add(this._buttonForEject(), { x_fill: true,
+                                                x_align: St.Align.START });
+
+        // add ourselves as a source, and popup the notification
+        Main.messageTray.add(this);
+        this.notify(this._notification);
+    },
+
+    _buttonForContentType: function(type) {
+        let app = Gio.app_info_get_default_for_type(type, false);
+
+        if (!app)
+            return null;
+
+        let box = new St.BoxLayout({ style_class: 'hotplug-notification-item',
+                                     track_hover: true,
+                                     reactive: true });
+        let icon = new St.Icon({ gicon: app.get_icon(),
+                                 style_class: 'hotplug-notification-item-icon' });
+        box.add(icon);
+
+        let label = new St.Bin({ y_align: St.Align.MIDDLE,
+                                 child: new St.Label
+                                 ({ text: _("Open with %s").format(app.get_display_name()) })
+                               });
+        box.add(label);
+
+        box._delegate = app;
+        box.connect('button-press-event',
+                    Lang.bind(this,
+                              this._onAppButtonClicked));
+
+        return box;
+    },
+
+    _onAppButtonClicked: function(actor, button) {
+        let files = [];
+        let app = actor._delegate;
+        let root = this._mount.get_root();
+
+        files.push(root);
+
+        try {
+            app.launch(files, 
+                       global.create_app_launch_context())
+        } catch (e) {
+            log('Unable to launch the application ' + app.get_name()
+                + ': ' + e.toString());
+        }
+
+        this.destroy();
+    },
+
+    _buttonForEject: function() {
+        let box = new St.BoxLayout({ style_class: 'hotplug-notification-item',
+                                     track_hover: true,
+                                     reactive: true });
+
+        let icon = new St.Icon({ icon_name: 'media-eject',
+                                 style_class: 'hotplug-notification-item-icon' });
+        box.add_actor(icon);
+
+        let label = new St.Bin({ y_align: St.Align.MIDDLE,
+                                 child: new St.Label
+                                 ({ text: _("Eject") })
+                               });
+        box.add(label);
+
+        box.connect('button-press-event',
+                    Lang.bind(this, function() {
+                        Main.autorunManager.ejectMount(this._mount);
+                    }));
+
+        return box;
+    },
+
+    createNotificationIcon: function(iconSize) {
+        return new St.Icon({ gicon: this._mount.get_icon(),
+                             icon_size: iconSize ? iconSize : this.ICON_SIZE });
+    }
+}
+
+function AutorunTransientNotification(source) {
+    this._init(source);
+}
+
+AutorunTransientNotification.prototype = {
+    __proto__: MessageTray.Notification.prototype,
+
+    _init: function(source) {
+        MessageTray.Notification.prototype._init.call(this, source,
+                                                      source.title, null,
+                                                      { customContent: true });
+
+        // set the notification to transient and urgent, so that it
+        // expands out
+        this.setTransient(true);
+        this.setUrgency(MessageTray.Urgency.CRITICAL);
+    }
+}
+
diff --git a/js/ui/main.js b/js/ui/main.js
index 43fcfa5..d1aaf12 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -12,6 +12,8 @@ const Meta = imports.gi.Meta;
 const Shell = imports.gi.Shell;
 const St = imports.gi.St;
 
+const AutomountManager = imports.ui.automountManager;
+const AutorunManager = imports.ui.autorunManager;
 const Chrome = imports.ui.chrome;
 const CtrlAltTab = imports.ui.ctrlAltTab;
 const EndSessionDialog = imports.ui.endSessionDialog;
@@ -38,6 +40,8 @@ const Util = imports.misc.util;
 const DEFAULT_BACKGROUND_COLOR = new Clutter.Color();
 DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff);
 
+let automountManager = null;
+let autorunManager = null;
 let chrome = null;
 let panel = null;
 let hotCorners = [];
@@ -139,6 +143,8 @@ function start() {
     notificationDaemon = new NotificationDaemon.NotificationDaemon();
     windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
     telepathyClient = new TelepathyClient.Client();
+    automountManager = new AutomountManager.AutomountManager();
+    autorunManager = new AutorunManager.AutorunManager();
 
     overview.init();
     statusIconDispatcher.start(messageTray.actor);



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