[gnome-shell] autorun: add an AutorunManager class
- From: Cosimo Cecchi <cosimoc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell] autorun: add an AutorunManager class
- Date: Wed, 13 Jul 2011 18:41:07 +0000 (UTC)
commit 534b371d422c69678c0a8c4772a9c65cf81ead10
Author: Cosimo Cecchi <cosimoc gnome org>
Date: Tue Jul 12 09:47:43 2011 -0400
autorun: add an AutorunManager class
AutorunManager is a class that takes care of displaying and managing
notifications and UI for storage devices.
When a mount appears and a number of conditions are satisified, a
transient notification will be displayed to immediately interact with
the device. AutorunTransientDispatcher is the object that takes care of
showing/hiding the notification sources as devices appear/disappear.
Likewise, current mounts are kept in a list and presented within a
list in a resident notification, handled by AutorunResidentSource.
https://bugzilla.gnome.org/show_bug.cgi?id=653520
data/theme/gnome-shell.css | 77 +++++++
js/Makefile.am | 1 +
js/ui/autorunManager.js | 499 ++++++++++++++++++++++++++++++++++++++++++++
js/ui/main.js | 3 +
4 files changed, 580 insertions(+), 0 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index ca20ff7..d25d12c 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -1139,6 +1139,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;
+}
+
+.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;
+ padding-left: 6px;
+}
+
+.hotplug-resident-mount-icon {
+ icon-size: 24px;
+ padding-left: 6px;
+}
+
+.hotplug-resident-eject-icon {
+ icon-size: 16px;
+}
+
+.hotplug-resident-eject-button {
+ padding: 2px;
+ border: 1px solid #2b2b2b;
+ border-radius: 5px;
+
+ 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 33e86c2..5d2d06f 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -16,6 +16,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..4d52a9b
--- /dev/null
+++ b/js/ui/autorunManager.js
@@ -0,0 +1,499 @@
+/* -*- 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;
+
+// misc utils
+function ignoreAutorunForMount(mount) {
+ let root = mount.get_root();
+ let volume = mount.get_volume();
+
+ if ((root.is_native() && !isMountRootHidden(root)) ||
+ (volume && volume.should_automount()))
+ return false;
+
+ return true;
+}
+
+function isMountRootHidden(root) {
+ let path = root.get_path();
+
+ // skip any mounts in hidden directory hierarchies
+ return (path.indexOf('/.') != -1);
+}
+
+function startAppForMount(app, mount) {
+ 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());
+ }
+}
+
+/******************************************/
+
+function ContentTypeDiscoverer(callback) {
+ this._init(callback);
+}
+
+ContentTypeDiscoverer.prototype = {
+ _init: function(callback) {
+ this._callback = callback;
+ },
+
+ guessContentTypes: function(mount) {
+ // guess mount's content types 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._volumeMonitor = Gio.VolumeMonitor.get();
+
+ this._volumeMonitor.connect('mount-added',
+ Lang.bind(this,
+ this._onMountAdded));
+ this._volumeMonitor.connect('mount-removed',
+ Lang.bind(this,
+ this._onMountRemoved));
+
+ this._transDispatcher = new AutorunTransientDispatcher();
+ this._residentSource = new AutorunResidentSource();
+ this._residentSource.connect('destroy', Lang.bind(this,
+ 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);
+ }));
+ },
+
+ _onMountAdded: function(monitor, mount) {
+ 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 = [];
+
+ this._notification = new AutorunResidentNotification(this);
+ this._setSummaryIcon(this.createNotificationIcon(HOTPLUG_ICON_SIZE));
+ },
+
+ addMount: function(mount, contentTypes) {
+ if (ignoreAutorunForMount(mount))
+ return;
+
+ 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();
+
+ 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();
+
+ // prepare the mount button content
+ let mountLayout = new St.BoxLayout();
+
+ 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 mountButton = new St.Button({ child: mountLayout,
+ x_align: St.Align.START,
+ x_fill: true,
+ style_class: 'hotplug-resident-mount',
+ button_mask: St.ButtonMask.ONE });
+ item.add(mountButton, { x_align: St.Align.START,
+ expand: true });
+
+ let ejectIcon =
+ new St.Icon({ icon_name: 'media-eject',
+ style_class: 'hotplug-resident-eject-icon' });
+
+ let ejectButton =
+ new St.Button({ style_class: 'hotplug-resident-eject-button',
+ button_mask: St.ButtonMask.ONE,
+ child: ejectIcon });
+ item.add(ejectButton, { x_align: St.Align.END });
+
+ // TODO: need to do something better here...
+ if (!contentTypes.length)
+ contentTypes.push('inode/directory');
+
+ // now connect signals
+ mountButton.connect('clicked', Lang.bind(this, function(actor, event) {
+ let app = Gio.app_info_get_default_for_type(contentTypes[0], false);
+
+ if (app)
+ startAppForMount(app, mount);
+ }));
+
+ ejectButton.connect('clicked', Lang.bind(this, function() {
+ Main.autorunManager.ejectMount(mount);
+ }));
+
+ return item;
+ },
+}
+
+function AutorunTransientDispatcher() {
+ this._init();
+}
+
+AutorunTransientDispatcher.prototype = {
+ _init: function() {
+ this._sources = [];
+ this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
+ },
+
+ _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 (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._notification = new AutorunTransientNotification(this);
+ this._setSummaryIcon(this.createNotificationIcon(this.ICON_SIZE));
+
+ // add ourselves as a source, and popup the notification
+ Main.messageTray.add(this);
+ this.notify(this._notification);
+ },
+
+ 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 });
+
+ this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
+ vertical: true });
+ this.addActor(this._box);
+
+ this._mount = source.mount;
+
+ source.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) {
+ let button = this._buttonForContentType('inode/directory');
+
+ if (button)
+ this._box.add (button, { x_fill: true,
+ x_align: St.Align.START });
+ }
+
+ this._box.add(this._buttonForEject(), { x_fill: true,
+ x_align: St.Align.START });
+
+ // set the notification to transient and urgent, so that it
+ // expands out
+ this.setTransient(true);
+ this.setUrgency(MessageTray.Urgency.CRITICAL);
+ },
+
+ _buttonForContentType: function(type) {
+ let app = Gio.app_info_get_default_for_type(type, false);
+
+ if (!app)
+ return null;
+
+ let box = new St.BoxLayout();
+
+ 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);
+
+ let button = new St.Button({ child: box,
+ button_mask: St.ButtonMask.ONE,
+ style_class: 'hotplug-notification-item' });
+
+ button.connect('clicked', Lang.bind(this, function() {
+ startAppForMount(app, this._mount);
+ this.destroy();
+ }));
+
+ return button;
+ },
+
+ _buttonForEject: function() {
+ let box = new St.BoxLayout();
+ let icon = new St.Icon({ icon_name: 'media-eject',
+ style_class: 'hotplug-notification-item-icon' });
+ box.add(icon);
+
+ let label = new St.Bin({ y_align: St.Align.MIDDLE,
+ child: new St.Label
+ ({ text: _("Eject") })
+ });
+ box.add(label);
+
+ let button = new St.Button({ child: box,
+ x_fill: true,
+ x_align: St.Align.START,
+ button_mask: St.ButtonMask.ONE,
+ style_class: 'hotplug-notification-item' });
+
+ button.connect('clicked', Lang.bind(this, function() {
+ Main.autorunManager.ejectMount(this._mount);
+ }));
+
+ return button;
+ }
+}
+
diff --git a/js/ui/main.js b/js/ui/main.js
index 8ca9da9..0fed731 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -12,6 +12,7 @@ const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
+const AutorunManager = imports.ui.autorunManager;
const Chrome = imports.ui.chrome;
const CtrlAltTab = imports.ui.ctrlAltTab;
const EndSessionDialog = imports.ui.endSessionDialog;
@@ -39,6 +40,7 @@ const Util = imports.misc.util;
const DEFAULT_BACKGROUND_COLOR = new Clutter.Color();
DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff);
+let autorunManager = null;
let chrome = null;
let panel = null;
let hotCorners = [];
@@ -142,6 +144,7 @@ function start() {
notificationDaemon = new NotificationDaemon.NotificationDaemon();
windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
telepathyClient = new TelepathyClient.Client();
+ autorunManager = new AutorunManager.AutorunManager();
layoutManager.init();
overview.init();
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]