diff --git a/data/org.cinnamon.gschema.xml b/data/org.cinnamon.gschema.xml
index e01fd3a313..803ec66abe 100644
--- a/data/org.cinnamon.gschema.xml
+++ b/data/org.cinnamon.gschema.xml
@@ -611,6 +611,15 @@
""
Stores the position of the hoverclick window so it can be restored there later - format is x::y
+
+ false
+ Whether to remember password for mounting encrypted or remote filesystems
+
+ When an encrypted device or remote filesystem is mounted, a password
+ dialog may include a "Remember Password" checkbox. This key stores the
+ default state of that checkbox.
+
+
{
_log('LoginManager: Received Lock signal from logind, emitting lock');
+ this.isLocked = true;
this.emit('lock');
});
this._sessionProxy.connectSignal('Unlock', () => {
_log('LoginManager: Received Unlock signal from logind, emitting unlock');
+ this.isLocked = false;
this.emit('unlock');
});
this._sessionProxy.connect('g-properties-changed', (proxy, changed, invalidated) => {
if ('Active' in changed.deep_unpack()) {
let active = this._sessionProxy.Active;
+ this.sessionIsActive = active;
_log(`LoginManager: Session Active property changed: ${active}`);
- if (active) {
- _log('LoginManager: Session became active, emitting active');
- this.emit('active');
- }
+ this.emit('active-changed', active);
}
});
+ this.sessionIsActive = this._sessionProxy.Active;
this.emit('session-ready');
} catch (e) {
global.logError('LoginManager: Failed to connect to logind session: ' + e.message);
@@ -229,6 +231,8 @@ var LoginManagerConsoleKit = class {
constructor() {
this._managerProxy = null;
this._sessionProxy = null;
+ this.sessionIsActive = true;
+ this.isLocked = false;
this._initSession();
}
@@ -273,20 +277,20 @@ var LoginManagerConsoleKit = class {
this._sessionProxy.connectSignal('Lock', () => {
_log('LoginManager: Received Lock signal from ConsoleKit, emitting lock');
+ this.isLocked = true;
this.emit('lock');
});
this._sessionProxy.connectSignal('Unlock', () => {
_log('LoginManager: Received Unlock signal from ConsoleKit, emitting unlock');
+ this.isLocked = false;
this.emit('unlock');
});
this._sessionProxy.connectSignal('ActiveChanged', (proxy, sender, [active]) => {
+ this.sessionIsActive = active;
_log(`LoginManager: ConsoleKit ActiveChanged: ${active}`);
- if (active) {
- _log('LoginManager: Session became active, emitting active');
- this.emit('active');
- }
+ this.emit('active-changed', active);
});
this.emit('session-ready');
diff --git a/js/ui/automountManager.js b/js/ui/automountManager.js
new file mode 100644
index 0000000000..b29ef5ca0d
--- /dev/null
+++ b/js/ui/automountManager.js
@@ -0,0 +1,247 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Component */
+
+const { Gio, GLib } = imports.gi;
+const Params = imports.misc.params;
+
+const LoginManager = imports.misc.loginManager;
+const Main = imports.ui.main;
+const CinnamonMountOperation = imports.ui.cinnamonMountOperation;
+
+// GSettings keys
+const SETTINGS_SCHEMA = 'org.cinnamon.desktop.media-handling';
+const SETTING_ENABLE_AUTOMOUNT = 'automount';
+
+var AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
+
+var AutomountManager = class {
+ constructor() {
+ this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
+ this._activeOperations = new Map();
+ this._volumeQueue = [];
+
+ this._loginManager = LoginManager.getLoginManager();
+ this._loginManager.connect('lock', () => this._onScreenLocked());
+ this._loginManager.connect('unlock', () => this._onScreenUnlocked());
+ this._loginManager.connect('active-changed', (lm, active) => {
+ if (active)
+ this._drainVolumeQueue();
+ });
+
+ this._volumeMonitor = Gio.VolumeMonitor.get();
+ this.enable();
+ }
+
+ enable() {
+ this._volumeMonitor.connectObject(
+ 'volume-added', this._onVolumeAdded.bind(this),
+ 'volume-removed', this._onVolumeRemoved.bind(this),
+ 'drive-connected', this._onDriveConnected.bind(this),
+ 'drive-disconnected', this._onDriveDisconnected.bind(this),
+ 'drive-eject-button', this._onDriveEjectButton.bind(this), this);
+
+ this._mountAllId = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._startupMountAll.bind(this));
+ GLib.Source.set_name_by_id(this._mountAllId, '[cinnamon] this._startupMountAll');
+ }
+
+ disable() {
+ this._volumeMonitor.disconnectObject(this);
+
+ if (this._mountAllId > 0) {
+ GLib.source_remove(this._mountAllId);
+ this._mountAllId = 0;
+ }
+ }
+
+ _onScreenLocked() {
+ // Volumes inserted while locked will be queued
+ }
+
+ _onScreenUnlocked() {
+ this._drainVolumeQueue();
+ }
+
+ _drainVolumeQueue() {
+ while (this._volumeQueue.length > 0) {
+ let volume = this._volumeQueue.shift();
+ this._checkAndMountVolume(volume, {
+ checkSession: false,
+ });
+ }
+ }
+
+ _startupMountAll() {
+ let volumes = this._volumeMonitor.get_volumes();
+ volumes.forEach(volume => {
+ this._checkAndMountVolume(volume, {
+ checkSession: false,
+ useMountOp: false,
+ allowAutorun: false,
+ });
+ });
+
+ this._mountAllId = 0;
+ return GLib.SOURCE_REMOVE;
+ }
+
+ _onDriveConnected() {
+ if (!this._loginManager.sessionIsActive)
+ return;
+
+ let player = global.display.get_sound_player();
+ player.play_from_theme('device-added-media',
+ _("External drive connected"),
+ null);
+ }
+
+ _onDriveDisconnected() {
+ if (!this._loginManager.sessionIsActive)
+ return;
+
+ let player = global.display.get_sound_player();
+ player.play_from_theme('device-removed-media',
+ _("External drive disconnected"),
+ null);
+ }
+
+ _onDriveEjectButton(monitor, drive) {
+ if (!this._loginManager.sessionIsActive)
+ return;
+
+ if (drive.can_stop()) {
+ drive.stop(Gio.MountUnmountFlags.FORCE, null, null,
+ (o, res) => {
+ try {
+ drive.stop_finish(res);
+ } catch (e) {
+ log(`Unable to stop the drive after drive-eject-button ${e.toString()}`);
+ }
+ });
+ } else if (drive.can_eject()) {
+ drive.eject_with_operation(Gio.MountUnmountFlags.FORCE, null, null,
+ (o, res) => {
+ try {
+ drive.eject_with_operation_finish(res);
+ } catch (e) {
+ log(`Unable to eject the drive after drive-eject-button ${e.toString()}`);
+ }
+ });
+ }
+ }
+
+ _onVolumeAdded(monitor, volume) {
+ this._checkAndMountVolume(volume);
+ }
+
+ _checkAndMountVolume(volume, params) {
+ params = Params.parse(params, {
+ checkSession: true,
+ useMountOp: true,
+ allowAutorun: true,
+ });
+
+ if (params.checkSession) {
+ if (!this._loginManager.sessionIsActive)
+ return;
+
+ if (this._loginManager.isLocked) {
+ this._volumeQueue.push(volume);
+ return;
+ }
+ }
+
+ if (volume.get_mount())
+ return;
+
+ if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) ||
+ !volume.should_automount() ||
+ !volume.can_mount()) {
+ this._allowAutorun(volume);
+ this._allowAutorunExpire(volume);
+
+ return;
+ }
+
+ if (params.useMountOp) {
+ let operation = new CinnamonMountOperation.CinnamonMountOperation(volume);
+ this._mountVolume(volume, operation, params.allowAutorun);
+ } else {
+ this._mountVolume(volume, null, params.allowAutorun);
+ }
+ }
+
+ _mountVolume(volume, operation, allowAutorun) {
+ if (allowAutorun)
+ this._allowAutorun(volume);
+
+ const mountOp = operation?.mountOp ?? null;
+ this._activeOperations.set(volume, operation);
+
+ volume.mount(0, mountOp, null,
+ this._onVolumeMounted.bind(this));
+ }
+
+ _onVolumeMounted(volume, res) {
+ this._allowAutorunExpire(volume);
+
+ try {
+ volume.mount_finish(res);
+ this._closeOperation(volume);
+ } catch (e) {
+ if (e.message.includes('No key available with this passphrase') ||
+ e.message.includes('No key available to unlock device') ||
+ e.message.includes('Failed to activate device: Incorrect passphrase') ||
+ e.message.includes('Failed to load device\'s parameters: Invalid argument')) {
+ this._reaskPassword(volume);
+ } else {
+ if (e.message.includes('Compiled against a version of libcryptsetup that does not support the VeraCrypt PIM setting')) {
+ Main.notifyError(_("Unable to unlock volume"),
+ _("The installed udisks version does not support the PIM setting"));
+ }
+
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+ log(`Unable to mount volume ${volume.get_name()}: ${e.toString()}`);
+ this._closeOperation(volume);
+ }
+ }
+ }
+
+ _onVolumeRemoved(monitor, volume) {
+ if (volume._allowAutorunExpireId && volume._allowAutorunExpireId > 0) {
+ GLib.source_remove(volume._allowAutorunExpireId);
+ delete volume._allowAutorunExpireId;
+ }
+
+ this._volumeQueue = this._volumeQueue.filter(v => v !== volume);
+ }
+
+ _reaskPassword(volume) {
+ let prevOperation = this._activeOperations.get(volume);
+ const existingDialog = prevOperation?.borrowDialog();
+ let operation =
+ new CinnamonMountOperation.CinnamonMountOperation(volume, { existingDialog });
+ this._mountVolume(volume, operation);
+ }
+
+ _closeOperation(volume) {
+ let operation = this._activeOperations.get(volume);
+ if (!operation)
+ return;
+ operation.close();
+ this._activeOperations.delete(volume);
+ }
+
+ _allowAutorun(volume) {
+ volume.allowAutorun = true;
+ }
+
+ _allowAutorunExpire(volume) {
+ let id = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, AUTORUN_EXPIRE_TIMEOUT_SECS, () => {
+ volume.allowAutorun = false;
+ delete volume._allowAutorunExpireId;
+ return GLib.SOURCE_REMOVE;
+ });
+ volume._allowAutorunExpireId = id;
+ GLib.Source.set_name_by_id(id, '[cinnamon] volume.allowAutorun');
+ }
+};
diff --git a/js/ui/autorunManager.js b/js/ui/autorunManager.js
new file mode 100644
index 0000000000..518aed7397
--- /dev/null
+++ b/js/ui/autorunManager.js
@@ -0,0 +1,479 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Component */
+
+const { Clutter, Gio, GObject, Pango, St } = imports.gi;
+
+const CheckBox = imports.ui.checkBox;
+const Dialog = imports.ui.dialog;
+const ModalDialog = imports.ui.modalDialog;
+
+const hotplugSnifferIface =
+' \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+';
+
+// GSettings keys
+const SETTINGS_SCHEMA = 'org.cinnamon.desktop.media-handling';
+const SETTING_DISABLE_AUTORUN = 'autorun-never';
+const SETTING_START_APP = 'autorun-x-content-start-app';
+const SETTING_IGNORE = 'autorun-x-content-ignore';
+const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
+const SETTING_AUTOMOUNT_OPEN = 'automount-open';
+
+var AutorunSetting = {
+ RUN: 0,
+ IGNORE: 1,
+ FILES: 2,
+ ASK: 3,
+};
+
+const AUTORUN_ITEM_APP = 0;
+const AUTORUN_ITEM_OPEN_FOLDER = 1;
+const AUTORUN_ITEM_DO_NOTHING = 2;
+
+var LIST_ITEM_ICON_SIZE = 36;
+
+function shouldAutorunMount(mount) {
+ let root = mount.get_root();
+ let volume = mount.get_volume();
+
+ if (!volume || !volume.allowAutorun)
+ return false;
+
+ // Consume the flag so subsequent mounts of the same volume
+ // (e.g. user manually mounting via Nemo) don't re-trigger autorun.
+ volume.allowAutorun = false;
+
+ if (root.is_native() && isMountRootHidden(root))
+ return false;
+
+ return true;
+}
+
+function isMountRootHidden(root) {
+ let path = root.get_path();
+
+ return path.includes('/.');
+}
+
+function isMountNonLocal(mount) {
+ let volume = mount.get_volume();
+ if (volume == null)
+ return true;
+
+ return volume.get_identifier("class") == "network";
+}
+
+function startAppForMount(app, mount) {
+ let root = mount.get_root();
+
+ try {
+ return app.launch([root], global.create_app_launch_context());
+ } catch (e) {
+ log(`Unable to launch the app ${app.get_name()}: ${e}`);
+ }
+
+ return false;
+}
+
+function _setAutorunPreferences(contentType, startApp, ignore, openFolder) {
+ let settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
+
+ let startAppTypes = settings.get_strv(SETTING_START_APP).filter(t => t !== contentType);
+ if (startApp)
+ startAppTypes.push(contentType);
+ settings.set_strv(SETTING_START_APP, startAppTypes);
+
+ let ignoreTypes = settings.get_strv(SETTING_IGNORE).filter(t => t !== contentType);
+ if (ignore)
+ ignoreTypes.push(contentType);
+ settings.set_strv(SETTING_IGNORE, ignoreTypes);
+
+ let openFolderTypes = settings.get_strv(SETTING_OPEN_FOLDER).filter(t => t !== contentType);
+ if (openFolder)
+ openFolderTypes.push(contentType);
+ settings.set_strv(SETTING_OPEN_FOLDER, openFolderTypes);
+}
+
+const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(hotplugSnifferIface);
+function HotplugSniffer() {
+ return new HotplugSnifferProxy(Gio.DBus.session,
+ 'org.Cinnamon.HotplugSniffer',
+ '/org/Cinnamon/HotplugSniffer');
+}
+
+var ContentTypeDiscoverer = class {
+ constructor() {
+ this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
+ }
+
+ guessContentTypes(mount, callback) {
+ let autorunEnabled = !this._settings.get_boolean(SETTING_DISABLE_AUTORUN);
+ let shouldScan = autorunEnabled && !isMountNonLocal(mount);
+
+ if (!shouldScan) {
+ callback([], []);
+ return;
+ }
+
+ mount.guess_content_type(false, null, (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}`);
+ }
+
+ if (contentTypes.length === 0) {
+ let root = mount.get_root();
+ let hotplugSniffer = new HotplugSniffer();
+ hotplugSniffer.SniffURIRemote(root.get_uri(), (result, error) => {
+ if (!error && result)
+ contentTypes = result[0] || [];
+
+ this._resolveApps(contentTypes, callback);
+ });
+ return;
+ }
+
+ this._resolveApps(contentTypes, callback);
+ });
+ }
+
+ _resolveApps(contentTypes, callback) {
+ contentTypes = contentTypes.filter(
+ type => type !== 'x-content/win32-software');
+
+ const apps = [];
+ contentTypes.forEach(type => {
+ const app = Gio.app_info_get_default_for_type(type, false);
+
+ if (app)
+ apps.push(app);
+ });
+
+ callback(apps, contentTypes);
+ }
+};
+
+var AutorunManager = class {
+ constructor() {
+ this._volumeMonitor = Gio.VolumeMonitor.get();
+ this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
+
+ this._dispatcher = new AutorunDispatcher(this);
+ this.enable();
+ }
+
+ enable() {
+ this._volumeMonitor.connectObject(
+ 'mount-added', this._onMountAdded.bind(this),
+ 'mount-removed', this._onMountRemoved.bind(this), this);
+ }
+
+ disable() {
+ this._volumeMonitor.disconnectObject(this);
+ }
+
+ _onMountAdded(monitor, mount) {
+ if (!shouldAutorunMount(mount))
+ return;
+
+ const discoverer = new ContentTypeDiscoverer();
+ discoverer.guessContentTypes(mount, (apps, contentTypes) => {
+ log(`autorunManager: mount=${mount.get_name()} contentTypes=[${contentTypes}] apps=[${apps.map(a => a.get_name())}]`);
+
+ if (apps.length === 0) {
+ if (this._settings.get_boolean(SETTING_AUTOMOUNT_OPEN))
+ this._openFolderForMount(mount);
+ return;
+ }
+
+ this._dispatcher.addMount(mount, apps, contentTypes);
+ });
+ }
+
+ _openFolderForMount(mount) {
+ let app = Gio.app_info_get_default_for_type('inode/directory', false);
+ if (app)
+ startAppForMount(app, mount);
+ }
+
+ _onMountRemoved(monitor, mount) {
+ this._dispatcher.removeMount(mount);
+ }
+};
+
+var AutorunDispatcher = class {
+ constructor(manager) {
+ this._manager = manager;
+ this._dialogs = [];
+ this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
+ }
+
+ _getAutorunSettingForType(contentType) {
+ let runApp = this._settings.get_strv(SETTING_START_APP);
+ if (runApp.includes(contentType))
+ return AutorunSetting.RUN;
+
+ let ignore = this._settings.get_strv(SETTING_IGNORE);
+ if (ignore.includes(contentType))
+ return AutorunSetting.IGNORE;
+
+ let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
+ if (openFiles.includes(contentType))
+ return AutorunSetting.FILES;
+
+ return AutorunSetting.ASK;
+ }
+
+ _getDialogForMount(mount) {
+ return this._dialogs.find(d => d.mount === mount) ?? null;
+ }
+
+ _showDialog(mount, apps, contentType) {
+ if (this._getDialogForMount(mount))
+ return;
+
+ let dialog = new AutorunDialog(mount, apps, contentType);
+ this._dialogs.push(dialog);
+
+ dialog.connect('destroy', () => {
+ this._dialogs = this._dialogs.filter(d => d !== dialog);
+ });
+
+ dialog.open();
+ }
+
+ addMount(mount, apps, contentTypes) {
+ if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
+ return;
+
+ let contentType = contentTypes.length > 0 ? contentTypes[0] : null;
+
+ let setting;
+ if (contentType)
+ setting = this._getAutorunSettingForType(contentType);
+ else
+ setting = AutorunSetting.ASK;
+
+ if (setting === AutorunSetting.IGNORE)
+ return;
+
+ let success = false;
+ let app = null;
+
+ if (setting === AutorunSetting.RUN && contentType)
+ app = Gio.app_info_get_default_for_type(contentType, false);
+ else if (setting === AutorunSetting.FILES)
+ app = Gio.app_info_get_default_for_type('inode/directory', false);
+
+ if (app)
+ success = startAppForMount(app, mount);
+
+ if (!success)
+ this._showDialog(mount, apps, contentType);
+ }
+
+ removeMount(mount) {
+ let dialog = this._getDialogForMount(mount);
+ if (!dialog)
+ return;
+
+ dialog.close();
+ }
+};
+
+var AutorunDialog = GObject.registerClass(
+class AutorunDialog extends ModalDialog.ModalDialog {
+ _init(mount, apps, contentType) {
+ super._init({ styleClass: 'autorun-dialog' });
+
+ this.mount = mount;
+ this._apps = apps;
+ this._contentType = contentType;
+
+ let mountName = mount.get_name();
+ let contentDescription = contentType
+ ? Gio.content_type_get_description(contentType)
+ : null;
+
+ let heading = new St.Label({
+ style_class: 'autorun-dialog-heading',
+ text: _("New media detected"),
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this.contentLayout.add_child(heading);
+
+ let descriptionText;
+ descriptionText = _("Select how to open '%s' and whether to perform this action in the future.")
+ .format(mountName, contentDescription);
+
+ let description = new St.Label({
+ style_class: 'autorun-dialog-description',
+ text: descriptionText,
+ });
+ description.clutter_text.line_wrap = true;
+ description.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
+ description.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this.contentLayout.add_child(description);
+
+ this._appSection = new Dialog.ListSection({ selectable: true });
+ this.contentLayout.add_child(this._appSection);
+
+ this._populateAppList();
+ this._appSection.selectIndex(0);
+
+ if (contentDescription) {
+ this._alwaysCheckBox = new CheckBox.CheckBox(_("Always perform this action for type '%s'").format(contentDescription));
+ this.contentLayout.add_child(this._alwaysCheckBox);
+ }
+
+ let canEject = mount.can_eject();
+ let ejectLabel = canEject ? _("Eject") : _("Unmount");
+
+ this.setButtons([
+ {
+ label: _("Cancel"),
+ action: () => this.close(),
+ key: Clutter.KEY_Escape,
+ },
+ {
+ label: ejectLabel,
+ action: () => this._onEject(),
+ },
+ {
+ label: _("OK"),
+ action: () => this._onOk(),
+ default: true,
+ },
+ ]);
+
+ this._unmountedId = mount.connect('unmounted', () => this.close());
+ }
+
+ _populateAppList() {
+ this._apps.forEach((app, idx) => {
+ let icon = app.get_icon();
+ let iconActor = icon
+ ? new St.Icon({ gicon: icon, icon_size: LIST_ITEM_ICON_SIZE })
+ : new St.Icon({ icon_name: 'application-x-executable', icon_size: LIST_ITEM_ICON_SIZE });
+
+ let item = new Dialog.ListSectionItem({
+ icon_actor: iconActor,
+ title: app.get_name(),
+ });
+ item._autorunType = AUTORUN_ITEM_APP;
+ item._appIndex = idx;
+ this._appSection.addItem(item);
+ });
+
+ let openFolderItem = new Dialog.ListSectionItem({
+ icon_actor: new St.Icon({ icon_name: 'folder-open', icon_size: LIST_ITEM_ICON_SIZE, icon_type: St.IconType.FULLCOLOR }),
+ title: _("Open Folder"),
+ });
+ openFolderItem._autorunType = AUTORUN_ITEM_OPEN_FOLDER;
+ this._appSection.addItem(openFolderItem);
+
+ let doNothingItem = new Dialog.ListSectionItem({
+ icon_actor: new St.Icon({ icon_name: 'window-close', icon_size: LIST_ITEM_ICON_SIZE }),
+ title: _("Do Nothing"),
+ });
+ doNothingItem._autorunType = AUTORUN_ITEM_DO_NOTHING;
+ this._appSection.addItem(doNothingItem);
+ }
+
+ _onOk() {
+ let selected = this._appSection.selectedItem;
+ if (!selected)
+ return;
+
+ let remember = this._alwaysCheckBox.checked;
+
+ if (selected._autorunType === AUTORUN_ITEM_DO_NOTHING) {
+ if (remember && this._contentType)
+ _setAutorunPreferences(this._contentType, false, true, false);
+ else if (this._contentType)
+ _setAutorunPreferences(this._contentType, false, false, false);
+
+ this.close();
+ return;
+ }
+
+ if (selected._autorunType === AUTORUN_ITEM_OPEN_FOLDER) {
+ if (remember && this._contentType)
+ _setAutorunPreferences(this._contentType, false, false, true);
+ else if (this._contentType)
+ _setAutorunPreferences(this._contentType, false, false, false);
+
+ let app = Gio.app_info_get_default_for_type('inode/directory', false);
+ if (app)
+ startAppForMount(app, this.mount);
+
+ this.close();
+ return;
+ }
+
+ // App selected
+ let app = this._apps[selected._appIndex];
+ if (remember && this._contentType) {
+ _setAutorunPreferences(this._contentType, true, false, false);
+ if (app)
+ app.set_as_default_for_type(this._contentType);
+ } else if (this._contentType) {
+ _setAutorunPreferences(this._contentType, false, false, false);
+ }
+
+ if (app)
+ startAppForMount(app, this.mount);
+
+ this.close();
+ }
+
+ _onEject() {
+ if (this.mount.can_eject()) {
+ this.mount.eject_with_operation(
+ Gio.MountUnmountFlags.NONE, null, null,
+ (mount, res) => {
+ try {
+ mount.eject_with_operation_finish(res);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+ log(`Failed to eject: ${e}`);
+ }
+ }
+ );
+ } else {
+ this.mount.unmount_with_operation(
+ Gio.MountUnmountFlags.NONE, null, null,
+ (mount, res) => {
+ try {
+ mount.unmount_with_operation_finish(res);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
+ log(`Failed to unmount: ${e}`);
+ }
+ }
+ );
+ }
+
+ this.close();
+ }
+
+ close() {
+ if (this._unmountedId) {
+ this.mount.disconnect(this._unmountedId);
+ this._unmountedId = 0;
+ }
+
+ super.close();
+ }
+});
diff --git a/js/ui/cinnamonMountOperation.js b/js/ui/cinnamonMountOperation.js
new file mode 100644
index 0000000000..6ecaaa305c
--- /dev/null
+++ b/js/ui/cinnamonMountOperation.js
@@ -0,0 +1,778 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported CinnamonMountOperation, CinnamonMountOpHandler */
+
+const { Clutter, Gio, GLib, GObject, Pango, Cinnamon, St } = imports.gi;
+
+const CheckBox = imports.ui.checkBox;
+const Dialog = imports.ui.dialog;
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+const ModalDialog = imports.ui.modalDialog;
+const Params = imports.misc.params;
+const CinnamonEntry = imports.ui.cinnamonEntry;
+
+const Util = imports.misc.util;
+
+var LIST_ITEM_ICON_SIZE = 48;
+
+const REMEMBER_MOUNT_PASSWORD_KEY = 'remember-mount-password';
+
+const MountOperationHandlerIface =
+' \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+';
+
+/* ------ Common Utils ------- */
+function _setButtonsForChoices(dialog, oldChoices, choices) {
+ let buttons = [];
+ let buttonsChanged = oldChoices.length !== choices.length;
+
+ for (let idx = 0; idx < choices.length; idx++) {
+ let button = idx;
+
+ buttonsChanged ||= oldChoices[idx] !== choices[idx];
+
+ buttons.unshift({
+ label: choices[idx],
+ action: () => dialog.emit('response', button),
+ });
+ }
+
+ if (buttonsChanged)
+ dialog.setButtons(buttons);
+}
+
+function _setLabelsForMessage(content, message) {
+ let labels = message.split('\n');
+
+ content.title = labels.shift();
+ content.description = labels.join('\n');
+}
+
+/* -------------------------------------------------------- */
+
+var CinnamonMountOperation = class {
+ constructor(source, params) {
+ params = Params.parse(params, { existingDialog: null });
+
+ this._dialog = null;
+ this._existingDialog = params.existingDialog;
+ this._processesDialog = null;
+
+ this.mountOp = new Cinnamon.MountOperation();
+
+ this.mountOp.connect('ask-question',
+ this._onAskQuestion.bind(this));
+ this.mountOp.connect('ask-password',
+ this._onAskPassword.bind(this));
+ this.mountOp.connect('show-processes-2',
+ this._onShowProcesses2.bind(this));
+ this.mountOp.connect('aborted',
+ this.close.bind(this));
+ this.mountOp.connect('show-unmount-progress',
+ this._onShowUnmountProgress.bind(this));
+ }
+
+ _closeExistingDialog() {
+ if (!this._existingDialog)
+ return;
+
+ this._existingDialog.close();
+ this._existingDialog = null;
+ }
+
+ _onAskQuestion(op, message, choices) {
+ this._closeExistingDialog();
+ this._dialog = new CinnamonMountQuestionDialog();
+
+ this._dialog.connectObject('response',
+ (object, choice) => {
+ this.mountOp.set_choice(choice);
+ this.mountOp.reply(Gio.MountOperationResult.HANDLED);
+
+ this.close();
+ }, this);
+
+ this._dialog.update(message, choices);
+ this._dialog.open();
+ }
+
+ _onAskPassword(op, message, defaultUser, defaultDomain, flags) {
+ if (this._existingDialog) {
+ this._dialog = this._existingDialog;
+ this._dialog.reaskPassword();
+ } else {
+ this._dialog = new CinnamonMountPasswordDialog(message, flags);
+ }
+
+ this._dialog.connectObject('response',
+ (object, choice, password, remember, hiddenVolume, systemVolume, pim) => {
+ if (choice == -1) {
+ this.mountOp.reply(Gio.MountOperationResult.ABORTED);
+ } else {
+ if (remember)
+ this.mountOp.set_password_save(Gio.PasswordSave.PERMANENTLY);
+ else
+ this.mountOp.set_password_save(Gio.PasswordSave.NEVER);
+
+ this.mountOp.set_password(password);
+ this.mountOp.set_is_tcrypt_hidden_volume(hiddenVolume);
+ this.mountOp.set_is_tcrypt_system_volume(systemVolume);
+ this.mountOp.set_pim(pim);
+ this.mountOp.reply(Gio.MountOperationResult.HANDLED);
+ }
+ }, this);
+ this._dialog.open();
+ }
+
+ close(_op) {
+ this._closeExistingDialog();
+ this._processesDialog = null;
+
+ if (this._dialog) {
+ this._dialog.close();
+ this._dialog = null;
+ }
+
+ if (this._notifier) {
+ this._notifier.done();
+ this._notifier = null;
+ }
+ }
+
+ _onShowProcesses2(op) {
+ this._closeExistingDialog();
+ let processes = op.get_show_processes_pids();
+ let choices = op.get_show_processes_choices();
+ let message = op.get_show_processes_message();
+
+ if (!this._processesDialog) {
+ this._processesDialog = new CinnamonProcessesDialog();
+ this._dialog = this._processesDialog;
+
+ this._processesDialog.connectObject('response',
+ (object, choice) => {
+ if (choice == -1) {
+ this.mountOp.reply(Gio.MountOperationResult.ABORTED);
+ } else {
+ this.mountOp.set_choice(choice);
+ this.mountOp.reply(Gio.MountOperationResult.HANDLED);
+ }
+
+ this.close();
+ }, this);
+ this._processesDialog.open();
+ }
+
+ this._processesDialog.update(message, processes, choices);
+ }
+
+ _onShowUnmountProgress(op, message, timeLeft, bytesLeft) {
+ if (!this._notifier)
+ this._notifier = new CinnamonUnmountNotifier();
+
+ if (bytesLeft == 0)
+ this._notifier.done(message);
+ else
+ this._notifier.show(message);
+ }
+
+ borrowDialog() {
+ this._dialog?.disconnectObject(this);
+ return this._dialog;
+ }
+};
+
+var CinnamonUnmountNotifier = class extends MessageTray.Source {
+ constructor() {
+ super('unmount-notifier');
+
+ this._notification = null;
+ Main.messageTray.add(this);
+ }
+
+ show(message) {
+ let [header, text] = message.split('\n', 2);
+
+ if (!this._notification) {
+ this._notification = new MessageTray.Notification(this, header, text);
+ this._notification.setTransient(true);
+ this._notification.setUrgency(MessageTray.Urgency.CRITICAL);
+ } else {
+ this._notification.update(header, text);
+ }
+
+ this.notify(this._notification);
+ }
+
+ done(message) {
+ if (this._notification) {
+ this._notification.destroy();
+ this._notification = null;
+ }
+
+ if (message) {
+ let [header, text] = message.split('\n', 2);
+ let notification = new MessageTray.Notification(this, header, text);
+ notification.setTransient(true);
+
+ this.notify(notification);
+ }
+ }
+
+ createNotificationIcon () {
+ return new St.Icon({
+ icon_name: 'xapp-media-removable',
+ icon_type: St.IconType.SYMBOLIC,
+ icon_size: this.ICON_SIZE
+ });
+ }
+};
+
+var CinnamonMountQuestionDialog = GObject.registerClass({
+ Signals: { 'response': { param_types: [GObject.TYPE_INT] } },
+}, class CinnamonMountQuestionDialog extends ModalDialog.ModalDialog {
+ _init() {
+ super._init({ styleClass: 'mount-question-dialog' });
+
+ this._oldChoices = [];
+
+ this._content = new Dialog.MessageDialogContent();
+ this.contentLayout.add_child(this._content);
+ }
+
+ vfunc_key_release_event(event) {
+ if (event.keyval === Clutter.KEY_Escape) {
+ this.emit('response', -1);
+ return Clutter.EVENT_STOP;
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ update(message, choices) {
+ _setLabelsForMessage(this._content, message);
+ _setButtonsForChoices(this, this._oldChoices, choices);
+ this._oldChoices = choices;
+ }
+});
+
+var CinnamonMountPasswordDialog = GObject.registerClass({
+ Signals: {
+ 'response': {
+ param_types: [
+ GObject.TYPE_INT,
+ GObject.TYPE_STRING,
+ GObject.TYPE_BOOLEAN,
+ GObject.TYPE_BOOLEAN,
+ GObject.TYPE_BOOLEAN,
+ GObject.TYPE_UINT,
+ ],
+ },
+ },
+}, class CinnamonMountPasswordDialog extends ModalDialog.ModalDialog {
+ _init(message, flags) {
+ let strings = message.split('\n');
+ let title = strings.shift() || null;
+ let description = strings.shift() || null;
+ super._init({ styleClass: 'prompt-dialog' });
+
+ let disksApp = Cinnamon.AppSystem.get_default().lookup_app('org.gnome.DiskUtility.desktop');
+
+ let content = new Dialog.MessageDialogContent({ title, description });
+
+ let passwordGridLayout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL });
+ let passwordGrid = new St.Widget({
+ style_class: 'prompt-dialog-password-grid',
+ layout_manager: passwordGridLayout,
+ });
+ passwordGridLayout.hookup_style(passwordGrid);
+
+ let rtl = passwordGrid.get_text_direction() === Clutter.TextDirection.RTL;
+ let curGridRow = 0;
+
+ if (flags & Gio.AskPasswordFlags.TCRYPT) {
+ this._hiddenVolume = new CheckBox.CheckBox(_("Hidden Volume"));
+ content.add_child(this._hiddenVolume);
+
+ this._systemVolume = new CheckBox.CheckBox(_("Windows System Volume"));
+ content.add_child(this._systemVolume);
+
+ this._keyfilesCheckbox = new CheckBox.CheckBox(_("Uses Keyfiles"));
+ this._keyfilesCheckbox.connect("clicked", this._onKeyfilesCheckboxClicked.bind(this));
+ content.add_child(this._keyfilesCheckbox);
+
+ this._keyfilesLabel = new St.Label({ visible: false });
+ if (disksApp) {
+ this._keyfilesLabel.clutter_text.set_markup(
+ /* Translators: %s is the Disks application */
+ _('To unlock a volume that uses keyfiles, use the %s utility instead.')
+ .format(disksApp.get_name()));
+ } else {
+ this._keyfilesLabel.clutter_text.set_markup(
+ _('You need an external utility like Disks to unlock a volume that uses keyfiles.'));
+ }
+ this._keyfilesLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._keyfilesLabel.clutter_text.line_wrap = true;
+ content.add_child(this._keyfilesLabel);
+
+ this._pimEntry = new St.PasswordEntry({
+ style_class: 'prompt-dialog-password-entry',
+ hint_text: _('PIM Number'),
+ can_focus: true,
+ x_expand: true,
+ });
+ this._pimEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this));
+ CinnamonEntry.addContextMenu(this._pimEntry);
+
+ if (rtl)
+ passwordGridLayout.attach(this._pimEntry, 1, curGridRow, 1, 1);
+ else
+ passwordGridLayout.attach(this._pimEntry, 0, curGridRow, 1, 1);
+ curGridRow += 1;
+ } else {
+ this._hiddenVolume = null;
+ this._systemVolume = null;
+ this._pimEntry = null;
+ }
+
+ this._passwordEntry = new St.PasswordEntry({
+ style_class: 'prompt-dialog-password-entry',
+ hint_text: _('Password'),
+ can_focus: true,
+ x_expand: true,
+ });
+ this._passwordEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this));
+ this.setInitialKeyFocus(this._passwordEntry);
+ CinnamonEntry.addContextMenu(this._passwordEntry);
+
+ if (rtl)
+ passwordGridLayout.attach(this._passwordEntry, 1, curGridRow, 1, 1);
+ else
+ passwordGridLayout.attach(this._passwordEntry, 0, curGridRow, 1, 1);
+ curGridRow += 1;
+
+ let warningBox = new St.BoxLayout({ vertical: true });
+
+ let capsLockWarning = new CinnamonEntry.CapsLockWarning();
+ warningBox.add_child(capsLockWarning);
+
+ this._errorMessageLabel = new St.Label({
+ style_class: 'prompt-dialog-error-label',
+ opacity: 0,
+ });
+ this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._errorMessageLabel.clutter_text.line_wrap = true;
+ warningBox.add_child(this._errorMessageLabel);
+
+ passwordGridLayout.attach(warningBox, 0, curGridRow, 2, 1);
+
+ content.add_child(passwordGrid);
+
+ if (flags & Gio.AskPasswordFlags.SAVING_SUPPORTED) {
+ this._rememberChoice = new CheckBox.CheckBox(_("Remember Password"));
+ this._rememberChoice.checked =
+ global.settings.get_boolean(REMEMBER_MOUNT_PASSWORD_KEY);
+ content.add_child(this._rememberChoice);
+ } else {
+ this._rememberChoice = null;
+ }
+
+ this.contentLayout.add_child(content);
+
+ this._defaultButtons = [{
+ label: _("Cancel"),
+ action: this._onCancelButton.bind(this),
+ key: Clutter.KEY_Escape,
+ }, {
+ label: _("Unlock"),
+ action: this._onUnlockButton.bind(this),
+ default: true,
+ }];
+
+ this._usesKeyfilesButtons = [{
+ label: _("Cancel"),
+ action: this._onCancelButton.bind(this),
+ key: Clutter.KEY_Escape,
+ }];
+
+ if (disksApp) {
+ this._usesKeyfilesButtons.push({
+ /* Translators: %s is the Disks application */
+ label: _('Open %s').format(disksApp.get_name()),
+ action: () => {
+ disksApp.activate();
+ this._onCancelButton();
+ },
+ default: true,
+ });
+ }
+
+ this.setButtons(this._defaultButtons);
+ }
+
+ reaskPassword() {
+ this._passwordEntry.end_busy();
+ this._passwordEntry.set_text('');
+ this._errorMessageLabel.text = _('Please try again');
+ this._errorMessageLabel.opacity = 255;
+
+ Util.wiggle(this._passwordEntry);
+ }
+
+ _onCancelButton() {
+ this._passwordEntry.end_busy();
+ this.emit('response', -1, '', false, false, false, 0);
+ }
+
+ _onUnlockButton() {
+ this._onEntryActivate();
+ }
+
+ _onEntryActivate() {
+ let pim = 0;
+ if (this._pimEntry !== null) {
+ pim = this._pimEntry.get_text();
+
+ if (isNaN(pim)) {
+ this._pimEntry.set_text('');
+ this._errorMessageLabel.text = _('The PIM must be a number or empty.');
+ this._errorMessageLabel.opacity = 255;
+ return;
+ }
+
+ this._errorMessageLabel.opacity = 0;
+ }
+
+ global.settings.set_boolean(REMEMBER_MOUNT_PASSWORD_KEY,
+ this._rememberChoice && this._rememberChoice.checked);
+
+ this._passwordEntry.start_busy();
+ this.emit('response', 1,
+ this._passwordEntry.get_text(),
+ this._rememberChoice &&
+ this._rememberChoice.checked,
+ this._hiddenVolume &&
+ this._hiddenVolume.checked,
+ this._systemVolume &&
+ this._systemVolume.checked,
+ parseInt(pim));
+ }
+
+ _onKeyfilesCheckboxClicked() {
+ let useKeyfiles = this._keyfilesCheckbox.checked;
+ this._passwordEntry.reactive = !useKeyfiles;
+ this._passwordEntry.can_focus = !useKeyfiles;
+ this._pimEntry.reactive = !useKeyfiles;
+ this._pimEntry.can_focus = !useKeyfiles;
+ this._rememberChoice.reactive = !useKeyfiles;
+ this._rememberChoice.can_focus = !useKeyfiles;
+ this._keyfilesLabel.visible = useKeyfiles;
+ this.setButtons(useKeyfiles ? this._usesKeyfilesButtons : this._defaultButtons);
+ }
+});
+
+var CinnamonProcessesDialog = GObject.registerClass({
+ Signals: { 'response': { param_types: [GObject.TYPE_INT] } },
+}, class CinnamonProcessesDialog extends ModalDialog.ModalDialog {
+ _init() {
+ super._init({ styleClass: 'processes-dialog' });
+
+ this._oldChoices = [];
+
+ this._content = new Dialog.MessageDialogContent();
+ this.contentLayout.add_child(this._content);
+
+ this._applicationSection = new Dialog.ListSection();
+ this._applicationSection.hide();
+ this.contentLayout.add_child(this._applicationSection);
+ }
+
+ vfunc_key_release_event(event) {
+ if (event.keyval === Clutter.KEY_Escape) {
+ this.emit('response', -1);
+ return Clutter.EVENT_STOP;
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ _setAppsForPids(pids) {
+ // remove all the items
+ this._applicationSection.list.destroy_all_children();
+ pids.forEach(pid => {
+ let tracker = Cinnamon.WindowTracker.get_default();
+ let app = tracker.get_app_from_pid(pid);
+
+ if (!app)
+ return;
+
+ let listItem = new Dialog.ListSectionItem({
+ icon_actor: app.create_icon_texture(LIST_ITEM_ICON_SIZE),
+ title: app.get_name(),
+ });
+ this._applicationSection.list.add_child(listItem);
+ });
+
+ this._applicationSection.visible =
+ this._applicationSection.list.get_n_children() > 0;
+ }
+
+ update(message, processes, choices) {
+ this._setAppsForPids(processes);
+ _setLabelsForMessage(this._content, message);
+ _setButtonsForChoices(this, this._oldChoices, choices);
+ this._oldChoices = choices;
+ }
+});
+
+var CinnamonMountOperationType = {
+ NONE: 0,
+ ASK_PASSWORD: 1,
+ ASK_QUESTION: 2,
+ SHOW_PROCESSES: 3,
+};
+
+var CinnamonMountOpHandler = class {
+ constructor() {
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(MountOperationHandlerIface, this);
+ this._dbusImpl.export(Gio.DBus.session, '/org/gtk/MountOperationHandler');
+ Gio.bus_own_name_on_connection(Gio.DBus.session, 'org.gtk.MountOperationHandler',
+ Gio.BusNameOwnerFlags.REPLACE, null, null);
+
+ this._dialog = null;
+
+ this._ensureEmptyRequest();
+ }
+
+ _ensureEmptyRequest() {
+ this._currentId = null;
+ this._currentInvocation = null;
+ this._currentType = CinnamonMountOperationType.NONE;
+ }
+
+ _clearCurrentRequest(response, details) {
+ if (this._currentInvocation) {
+ this._currentInvocation.return_value(
+ GLib.Variant.new('(ua{sv})', [response, details]));
+ }
+
+ this._ensureEmptyRequest();
+ }
+
+ _setCurrentRequest(invocation, id, type) {
+ let oldId = this._currentId;
+ let oldType = this._currentType;
+ let requestId = `${id}@${invocation.get_sender()}`;
+
+ this._clearCurrentRequest(Gio.MountOperationResult.UNHANDLED, {});
+
+ this._currentInvocation = invocation;
+ this._currentId = requestId;
+ this._currentType = type;
+
+ if (this._dialog && (oldId == requestId) && (oldType == type))
+ return true;
+
+ return false;
+ }
+
+ _closeDialog() {
+ if (this._dialog) {
+ this._dialog.close();
+ this._dialog = null;
+ }
+ }
+
+ /**
+ * AskPassword:
+ * @param {Array} params
+ * {string} id: an opaque ID identifying the object for which
+ * the operation is requested
+ * {string} message: the message to display
+ * {string} icon_name: the name of an icon to display
+ * {string} default_user: the default username for display
+ * {string} default_domain: the default domain for display
+ * {Gio.AskPasswordFlags} flags: a set of GAskPasswordFlags
+ * {Gio.MountOperationResults} response: a GMountOperationResult
+ * {Object} response_details: a dictionary containing response details as
+ * entered by the user. The dictionary MAY contain the following
+ * properties:
+ * - "password" -> (s): a password to be used to complete the mount operation
+ * - "password_save" -> (u): a GPasswordSave
+ * @param {Gio.DBusMethodInvocation} invocation
+ * The ID must be unique in the context of the calling process.
+ *
+ * The dialog will stay visible until clients call the Close() method, or
+ * another dialog becomes visible.
+ * Calling AskPassword again for the same id will have the effect to clear
+ * the existing dialog and update it with a message indicating the previous
+ * attempt went wrong.
+ */
+ AskPasswordAsync(params, invocation) {
+ let [id, message, iconName_, defaultUser_, defaultDomain_, flags] = params;
+ if (this._setCurrentRequest(invocation, id, CinnamonMountOperationType.ASK_PASSWORD)) {
+ this._dialog.reaskPassword();
+ return;
+ }
+
+ this._closeDialog();
+
+ this._dialog = new CinnamonMountPasswordDialog(message, flags);
+ this._dialog.connect('response',
+ (object, choice, password, remember, hiddenVolume, systemVolume, pim) => {
+ let details = {};
+ let response;
+
+ if (choice == -1) {
+ response = Gio.MountOperationResult.ABORTED;
+ } else {
+ response = Gio.MountOperationResult.HANDLED;
+
+ let passSave = remember ? Gio.PasswordSave.PERMANENTLY : Gio.PasswordSave.NEVER;
+ details['password_save'] = GLib.Variant.new('u', passSave);
+ details['password'] = GLib.Variant.new('s', password);
+ details['hidden_volume'] = GLib.Variant.new('b', hiddenVolume);
+ details['system_volume'] = GLib.Variant.new('b', systemVolume);
+ details['pim'] = GLib.Variant.new('u', pim);
+ }
+
+ this._clearCurrentRequest(response, details);
+ });
+ this._dialog.open();
+ }
+
+ /**
+ * AskQuestion:
+ * @param {Array} params - params
+ * {string} id: an opaque ID identifying the object for which
+ * the operation is requested
+ * The ID must be unique in the context of the calling process.
+ * {string} message: the message to display
+ * {string} icon_name: the name of an icon to display
+ * {string[]} choices: an array of choice strings
+ * @param {Gio.DBusMethodInvocation} invocation - invocation
+ *
+ * The dialog will stay visible until clients call the Close() method, or
+ * another dialog becomes visible.
+ * Calling AskQuestion again for the same id will have the effect to clear
+ * update the dialog with the new question.
+ */
+ AskQuestionAsync(params, invocation) {
+ let [id, message, iconName_, choices] = params;
+ if (this._setCurrentRequest(invocation, id, CinnamonMountOperationType.ASK_QUESTION)) {
+ this._dialog.update(message, choices);
+ return;
+ }
+
+ this._closeDialog();
+
+ this._dialog = new CinnamonMountQuestionDialog(message);
+ this._dialog.connect('response', (object, choice) => {
+ let response;
+ let details = {};
+
+ if (choice == -1) {
+ response = Gio.MountOperationResult.ABORTED;
+ } else {
+ response = Gio.MountOperationResult.HANDLED;
+ details['choice'] = GLib.Variant.new('i', choice);
+ }
+
+ this._clearCurrentRequest(response, details);
+ });
+
+ this._dialog.update(message, choices);
+ this._dialog.open();
+ }
+
+ /**
+ * ShowProcesses:
+ * @param {Array} params - params
+ * {string} id: an opaque ID identifying the object for which
+ * the operation is requested
+ * The ID must be unique in the context of the calling process.
+ * {string} message: the message to display
+ * {string} icon_name: the name of an icon to display
+ * {number[]} application_pids: the PIDs of the applications to display
+ * {string[]} choices: an array of choice strings
+ * @param {Gio.DBusMethodInvocation} invocation - invocation
+ *
+ * The dialog will stay visible until clients call the Close() method, or
+ * another dialog becomes visible.
+ * Calling ShowProcesses again for the same id will have the effect to clear
+ * the existing dialog and update it with the new message and the new list
+ * of processes.
+ */
+ ShowProcessesAsync(params, invocation) {
+ let [id, message, iconName_, applicationPids, choices] = params;
+
+ if (this._setCurrentRequest(invocation, id, CinnamonMountOperationType.SHOW_PROCESSES)) {
+ this._dialog.update(message, applicationPids, choices);
+ return;
+ }
+
+ this._closeDialog();
+ this._dialog = new CinnamonProcessesDialog();
+ this._dialog.connect('response', (object, choice) => {
+ let response;
+ let details = {};
+
+ if (choice == -1) {
+ response = Gio.MountOperationResult.ABORTED;
+ } else {
+ response = Gio.MountOperationResult.HANDLED;
+ details['choice'] = GLib.Variant.new('i', choice);
+ }
+
+ this._clearCurrentRequest(response, details);
+ });
+
+ this._dialog.update(message, applicationPids, choices);
+ this._dialog.open();
+ }
+
+ /**
+ * Close:
+ * @param {Array} _params - params
+ * @param {Gio.DBusMethodInvocation} _invocation - invocation
+ *
+ * Closes a dialog previously opened by AskPassword, AskQuestion or ShowProcesses.
+ * If no dialog is open, does nothing.
+ */
+ Close(_params, _invocation) {
+ this._clearCurrentRequest(Gio.MountOperationResult.UNHANDLED, {});
+ this._closeDialog();
+ }
+};
diff --git a/js/ui/dialog.js b/js/ui/dialog.js
index d96b3ebcaa..01e596d378 100644
--- a/js/ui/dialog.js
+++ b/js/ui/dialog.js
@@ -280,10 +280,19 @@ var ListSection = GObject.registerClass({
GObject.ParamFlags.READWRITE |
GObject.ParamFlags.CONSTRUCT,
null),
+ 'selectable': GObject.ParamSpec.boolean(
+ 'selectable', 'selectable', 'selectable',
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT,
+ false),
+ },
+ Signals: {
+ 'selection-changed': { param_types: [GObject.TYPE_INT] },
},
}, class ListSection extends St.BoxLayout {
_init(params) {
this._title = new St.Label({ style_class: 'dialog-list-title' });
+ this._selectedIndex = -1;
this.list = new St.BoxLayout({
style_class: 'dialog-list-box',
@@ -317,6 +326,58 @@ var ListSection = GObject.registerClass({
_setLabel(this._title, title);
this.notify('title');
}
+
+ get selectedIndex() {
+ return this._selectedIndex;
+ }
+
+ get selectedItem() {
+ if (this._selectedIndex < 0)
+ return null;
+
+ let children = this.list.get_children();
+ return children[this._selectedIndex] ?? null;
+ }
+
+ selectIndex(index) {
+ let children = this.list.get_children();
+
+ if (index < 0 || index >= children.length)
+ return;
+
+ if (index === this._selectedIndex)
+ return;
+
+ children.forEach((child, i) => {
+ if (i === index)
+ child.add_style_pseudo_class('selected');
+ else
+ child.remove_style_pseudo_class('selected');
+ });
+
+ this._selectedIndex = index;
+ this.emit('selection-changed', index);
+ }
+
+ _onItemClicked(item) {
+ let children = this.list.get_children();
+ let index = children.indexOf(item);
+ if (index >= 0)
+ this.selectIndex(index);
+ }
+
+ addItem(item) {
+ this.list.add_child(item);
+
+ if (this.selectable) {
+ item.reactive = true;
+ item.track_hover = true;
+ item.connect('button-release-event', () => {
+ this._onItemClicked(item);
+ return Clutter.EVENT_STOP;
+ });
+ }
+ }
});
var ListSectionItem = GObject.registerClass({
@@ -336,7 +397,7 @@ var ListSectionItem = GObject.registerClass({
GObject.ParamFlags.CONSTRUCT,
null),
},
-}, class ListSectionItem extends St.BoxLayout{
+}, class ListSectionItem extends St.BoxLayout {
_init(params) {
this._iconActorBin = new St.Bin();
@@ -361,7 +422,6 @@ var ListSectionItem = GObject.registerClass({
...params,
});
-
this.label_actor = this._title;
this.add_child(this._iconActorBin);
this.add_child(textLayout);
diff --git a/js/ui/main.js b/js/ui/main.js
index 912bdcbcb8..fd72d77fc8 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -91,6 +91,8 @@ const GObject = imports.gi.GObject;
const XApp = imports.gi.XApp;
const PointerTracker = imports.misc.pointerTracker;
+const AutomountManager = imports.ui.automountManager;
+const AutorunManager = imports.ui.autorunManager;
const AudioDeviceSelection = imports.ui.audioDeviceSelection;
const SoundManager = imports.ui.soundManager;
const BackgroundManager = imports.ui.backgroundManager;
@@ -116,6 +118,7 @@ const NetworkAgent = imports.ui.networkAgent;
const NotificationDaemon = imports.ui.notificationDaemon;
const WindowAttentionHandler = imports.ui.windowAttentionHandler;
const CinnamonDBus = imports.ui.cinnamonDBus;
+const CinnamonMountOperation = imports.ui.cinnamonMountOperation;
const Screenshot = imports.ui.screenshot;
const ScreensaverController = imports.ui.screensaver.controller;
const ThemeManager = imports.ui.themeManager;
@@ -165,6 +168,9 @@ var windowAttentionHandler = null;
var screenRecorder = null;
var cinnamonAudioSelectionDBusService = null;
var cinnamonDBusService = null;
+var cinnamonMountOpDBusService = null;
+var automountManager = null;
+var autorunManager = null;
var screenshotService = null;
var modalCount = 0;
var modalActorFocusStack = [];
@@ -345,6 +351,10 @@ function start() {
new CinnamonPortalHandler();
cinnamonAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus();
cinnamonDBusService = new CinnamonDBus.CinnamonDBus();
+ cinnamonMountOpDBusService = new CinnamonMountOperation.CinnamonMountOpHandler();
+ automountManager = new AutomountManager.AutomountManager();
+ autorunManager = new AutorunManager.AutorunManager();
+
setRunState(RunState.STARTUP);
screenshotService = new Screenshot.ScreenshotService();
diff --git a/js/ui/placesManager.js b/js/ui/placesManager.js
index 8e232d8ed3..5d932fb027 100644
--- a/js/ui/placesManager.js
+++ b/js/ui/placesManager.js
@@ -8,6 +8,7 @@ const Mainloop = imports.mainloop;
const Signals = imports.signals;
const St = imports.gi.St;
+const CinnamonMountOperation = imports.ui.cinnamonMountOperation;
const Main = imports.ui.main;
const MessageTray = imports.ui.messageTray;
const Params = imports.misc.params;
@@ -125,172 +126,23 @@ PlaceDeviceInfo.prototype = {
if (!this.isRemovable())
return;
- let mountOp = new Gio.MountOperation();
+ let mountOp = new CinnamonMountOperation.CinnamonMountOperation();
let drive = this._mount.get_drive();
let volume = this._mount.get_volume();
if (drive &&
drive.get_start_stop_type() == Gio.DriveStartStopType.SHUTDOWN &&
drive.can_stop()) {
- drive.stop(0, mountOp, null, Lang.bind(this, this._stopFinish));
+ drive.stop(0, mountOp.mountOp, null, null);
} else {
- if (drive && drive.can_eject())
- drive.eject_with_operation(0, mountOp, null, Lang.bind(this, this._ejectFinish, true));
- else if (volume && volume.can_eject())
- volume.eject_with_operation(0, mountOp, null, Lang.bind(this, this._ejectFinish, false));
- else if (this._mount.can_eject())
- this._mount.eject_with_operation(0, mountOp, null, Lang.bind(this, this._ejectFinish, false));
+ if ((drive && drive.can_eject()) || (volume && volume.can_eject()) || this._mount.can_eject())
+ drive.eject_with_operation(0, mountOp.mountOp, null, null);
else if (this._mount.can_unmount())
- this._mount.unmount_with_operation(0, mountOp, null, Lang.bind(this, this._removeFinish));
+ this._mount.unmount_with_operation(0, mountOp.mountOp, null, null);
}
this.busyWaitId = 0;
return false;
- },
-
- _sendNotification: function(msg1, msg2 = null, withButton = false, persistent = false) {
- if (Main.messageTray) {
- if (persistent && this.busyNotification != null) {
- return;
- }
-
- if (!persistent && this.busyNotification) {
- this.busyNotification.destroy();
- this.busyNotification = null;
- }
-
- let source = new MessageTray.SystemNotificationSource();
- Main.messageTray.add(source);
- let notification = new MessageTray.Notification(source, msg1, msg2);
- notification.setTransient(true);
- notification.setUrgency(persistent ? MessageTray.Urgency.CRITICAL : MessageTray.Urgency.NORMAL);
- if (withButton) {
- notification.addButton('system-undo', _("Retry"));
- notification.connect('action-invoked', Lang.bind(this, this.remove));
- }
- source.notify(notification);
- if (persistent) {
- this.busyNotification = notification;
- this.destroySignalId = notification.connect("destroy", () => {
- this.busyNotification.disconnect(this.destroySignalId);
- this.busyNotification = null;
- this.destroySignalId = 0;
- })
- }
- } else {
- if (msg2)
- global.log(msg1 + ': ' + msg2);
- else
- global.log(msg1);
- }
- },
-
- _stopFinish: function(drive, res) {
- if (DEBUG) global.log("PlacesManager: **_stopFinish**");
- let driveName = drive.get_name(); // Ex: USB Flash Drive
- let unixDevice = drive.get_identifier('unix-device'); // Ex: /dev/sdc
- let msg1 = _("%s (%s) has just been stopped.").format(driveName, this.name);
- let msg2 = _("Device %s can be turned off, if necessary.").format(unixDevice);
- let btn = false; // Show the 'Retry' button?
- try {
- drive.stop_finish(res);
- } catch(e) {
- if (e.code == Gio.IOErrorEnum.BUSY) {
- msg1 = _("Device %s is busy, please wait.".format(drive.get_name()));
- msg2 = _("Do not disconnect or data loss may occur.");
-
- this._sendNotification(msg1, msg2, false, true);
- this.busyWaitId = Mainloop.timeout_add_seconds(2, ()=>this._tryRemove());
- return;
- }
- btn = true;
- msg1 = _("Unable to stop the drive %s (%s)").format(drive.get_name(), this.name);
- msg2 = e.message;
- }
- if (DEBUG) global.log(msg1 + ": " + msg2);
- this._sendNotification(msg1, msg2, btn);
- },
-
- _ejectFinish: function(source, res, is_drive) {
- if (DEBUG) global.log("PlacesManager: **_ejectFinish**");
- let msg1;
- let msg2 = null;
- let btn = false;
-
- if (is_drive) {
- let driveName = source.get_name(); // Ex: USB Flash Drive
- let unixDevice = source.get_identifier('unix-device'); // Ex: /dev/sdc
- msg1 = _("%s (%s) can be safely unplugged.").format(driveName, this.name);
- msg2 = _("Device %s can be removed.").format(unixDevice);
- } else {
- msg1 = _("%s (%s) has just been ejected.").format(source.get_name(), this.name);
- }
- try {
- source.eject_with_operation_finish(res);
- } catch(e) {
- if (e.code == Gio.IOErrorEnum.BUSY) {
- msg1 = _("Device %s is busy, please wait.".format(source.get_name()));
- msg2 = _("Do not remove or data loss may occur.");
-
- this._sendNotification(msg1, msg2, false, true);
- this.busyWaitId = Mainloop.timeout_add_seconds(2, ()=>this._tryRemove());
- return;
- }
- btn = true;
- msg1 = _("Unable to eject the drive %s (%s)").format(source.get_name(), this.name);
- msg2 = e.message;
- }
- if (DEBUG) global.log(msg1 + ": " + msg2);
- this._sendNotification(msg1, msg2, btn);
- },
-
- _removeFinish: function(o, res, data) {
- if (DEBUG) global.log("PlacesManager: **_removeFinish**");
- let msg1 = _("Successfully unmounted %s (%s)").format(o.get_name(), this.name);
- let msg2 = null;
- let btn = false;
-
- // 'this._mount.can_eject()' seems to be ever false. Thus, only the 'else' part will be used.
- // If no issues are reported, these 19 lines of code commented below can be deleted.
- //~ if (this._mount.can_eject()) {
- //~ msg1 = _("%s (%s) can be safely unplugged").format(o.get_name(), this.name);
- //~ msg2 = _("Device can be removed");
- //~ try {
- //~ this._mount.eject_with_operation_finish(res);
- //~ } catch(e) {
- //~ btn = true;
- //~ msg1 = _("Failed to eject %s (%s)").format(o.get_name(), this.name);
- //~ msg2 = e.message;
- //~ }
- //~ } else {
- //~ try {
- //~ this._mount.unmount_with_operation_finish(res);
- //~ } catch(e) {
- //~ btn = true;
- //~ msg1 = _("Failed to unmount %s (%s)").format(o.get_name(), this.name);
- //~ msg2 = e.message;
- //~ }
- //~ }
- // <--Beginning of the code replacing the 19 lines above:
- try {
- this._mount.unmount_with_operation_finish(res);
- } catch(e) {
- if (e.code == Gio.IOErrorEnum.BUSY) {
- msg1 = _("Device %s is busy, please wait.".format(o.get_name()));
- msg2 = _("Do not disconnect or data loss may occur.");
-
- this._sendNotification(msg1, msg2, false, true);
- this.busyWaitId = Mainloop.timeout_add_seconds(2, ()=>this._tryRemove());
- return;
- }
- btn = true;
- msg1 = _("Failed to unmount %s (%s)").format(o.get_name(), this.name);
- msg2 = e.message;
- }
- // End of this code.-->
-
- if (DEBUG) global.log(msg1 + ": " + msg2);
- this._sendNotification(msg1, msg2, btn);
}
};
diff --git a/js/ui/screensaver/screenShield.js b/js/ui/screensaver/screenShield.js
index d3679b0746..9f6ac0dd4c 100644
--- a/js/ui/screensaver/screenShield.js
+++ b/js/ui/screensaver/screenShield.js
@@ -188,7 +188,7 @@ var ScreenShield = GObject.registerClass({
this._loginManager.connect('lock', this._onSessionLock.bind(this));
this._loginManager.connect('unlock', this._onSessionUnlock.bind(this));
- this._loginManager.connect('active', this._onSessionActive.bind(this));
+ this._loginManager.connect('active-changed', this._onSessionActiveChanged.bind(this));
this._monitorsChangedId = Main.layoutManager.connect('monitors-changed',
this._onMonitorsChanged.bind(this));
@@ -671,8 +671,10 @@ var ScreenShield = GObject.registerClass({
}
}
- _onSessionActive() {
- _log(`ScreenShield: Received active signal from LoginManager (state=${this._state})`);
+ _onSessionActiveChanged(lm, active) {
+ _log(`ScreenShield: Received active-changed signal from LoginManager (active=${active}, state=${this._state})`);
+ if (!active)
+ return;
if (this._state === State.LOCKED) {
this.showUnlockDialog();
}
diff --git a/js/ui/testAutorunDialog.js b/js/ui/testAutorunDialog.js
new file mode 100644
index 0000000000..27ad6ad089
--- /dev/null
+++ b/js/ui/testAutorunDialog.js
@@ -0,0 +1,31 @@
+// Test helper - call from Looking Glass:
+// imports.ui.testAutorunDialog.show()
+// imports.ui.testAutorunDialog.show("My USB", "x-content/image-dcf")
+
+const Gio = imports.gi.Gio;
+const AutorunManager = imports.ui.autorunManager;
+
+function show(mountName, contentType) {
+ mountName = mountName || "Test USB Drive";
+ contentType = contentType || "x-content/unix-software";
+
+ let nextId = 1;
+ let mount = {
+ get_name() { return mountName; },
+ can_eject() { return true; },
+ get_root() { return Gio.File.new_for_path("/"); },
+ connect() { return nextId++; },
+ disconnect() {},
+ eject_with_operation() {},
+ unmount_with_operation() {},
+ };
+
+ let apps = [];
+ let app = Gio.app_info_get_default_for_type("inode/directory", false);
+ if (app)
+ apps.push(app);
+
+ let dialog = new AutorunManager.AutorunDialog(mount, apps, contentType);
+ dialog.open();
+ return dialog;
+}
diff --git a/js/ui/testMountDialogs.js b/js/ui/testMountDialogs.js
new file mode 100644
index 0000000000..37b7a67fd0
--- /dev/null
+++ b/js/ui/testMountDialogs.js
@@ -0,0 +1,63 @@
+// Test helpers for mount operation dialogs.
+// Call from Looking Glass:
+//
+// imports.ui.testMountDialogs.askPassword()
+// imports.ui.testMountDialogs.askPassword("Unlock encrypted volume\nEnter password for 'My Drive'")
+// imports.ui.testMountDialogs.askPasswordTcrypt()
+// imports.ui.testMountDialogs.askQuestion()
+// imports.ui.testMountDialogs.askQuestion("Trust this certificate?\nThe identity of 'server.local' cannot be verified.", ["Cancel", "Trust", "Trust Always"])
+// imports.ui.testMountDialogs.showProcesses()
+
+const Gio = imports.gi.Gio;
+const MountOp = imports.ui.cinnamonMountOperation;
+
+function askPassword(message, flags) {
+ message = message || "Enter a password to unlock the volume\nThe password is needed to access encrypted data on My Encrypted Drive.";
+ flags = flags || Gio.AskPasswordFlags.NEED_PASSWORD;
+
+ let dialog = new MountOp.CinnamonMountPasswordDialog(message, flags);
+ dialog.connect('response', (obj, choice, password, remember, hidden, system, pim) => {
+ log(`[testMountDialogs] askPassword response: choice=${choice} password=${password}`);
+ dialog.close();
+ });
+ dialog.open();
+ return dialog;
+}
+
+function askPasswordTcrypt(message) {
+ message = message || "Enter a password to unlock the volume\nThe password is needed to access encrypted data on My VeraCrypt Drive.";
+ let flags = Gio.AskPasswordFlags.NEED_PASSWORD | Gio.AskPasswordFlags.TCRYPT;
+
+ return askPassword(message, flags);
+}
+
+function askQuestion(message, choices) {
+ message = message || "Mount point is not empty\nThe mount point /mnt/data already contains files. Do you want to merge?";
+ choices = choices || ["Cancel", "Merge"];
+
+ let dialog = new MountOp.CinnamonMountQuestionDialog();
+ dialog.connect('response', (obj, choice) => {
+ log(`[testMountDialogs] askQuestion response: choice=${choice}`);
+ dialog.close();
+ });
+ dialog.update(message, choices);
+ dialog.open();
+ return dialog;
+}
+
+function showProcesses(message, choices) {
+ message = message || "Volume is busy\nOne or more applications are keeping the volume busy.";
+ choices = choices || ["Cancel", "Unmount Anyway"];
+
+ let dialog = new MountOp.CinnamonProcessesDialog();
+ dialog.connect('response', (obj, choice) => {
+ log(`[testMountDialogs] showProcesses response: choice=${choice}`);
+ dialog.close();
+ });
+
+ // Use real PIDs of running apps if possible, otherwise empty
+ let pids = [];
+ dialog.update(message, pids, choices);
+ dialog.open();
+ return dialog;
+}
diff --git a/src/cinnamon-mount-operation.c b/src/cinnamon-mount-operation.c
new file mode 100644
index 0000000000..d7096886eb
--- /dev/null
+++ b/src/cinnamon-mount-operation.c
@@ -0,0 +1,189 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ *
+ * Author: Cosimo Cecchi
+ *
+ */
+
+#include "cinnamon-mount-operation.h"
+
+/* This is a dummy class; we would like to be able to subclass the
+ * object from JS but we can't yet; the default GMountOperation impl
+ * automatically calls g_mount_operation_reply(UNHANDLED) after an idle,
+ * in interactive methods. We want to handle the reply ourselves
+ * instead, so we just override the default methods with empty ones,
+ * except for ask-password, as we don't want to handle that.
+ *
+ * Also, we need to workaround the fact that gjs doesn't support type
+ * annotations for signals yet (so we can't effectively forward e.g.
+ * the GPid array to JS).
+ * See https://bugzilla.gnome.org/show_bug.cgi?id=645978
+ */
+
+enum {
+ SHOW_PROCESSES_2,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+typedef struct _CinnamonMountOperationPrivate CinnamonMountOperationPrivate;
+
+struct _CinnamonMountOperation
+{
+ GMountOperation parent_instance;
+
+ CinnamonMountOperationPrivate *priv;
+};
+
+struct _CinnamonMountOperationPrivate {
+ GArray *pids;
+ gchar **choices;
+ gchar *message;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CinnamonMountOperation, cinnamon_mount_operation, G_TYPE_MOUNT_OPERATION);
+
+static void
+cinnamon_mount_operation_init (CinnamonMountOperation *self)
+{
+ self->priv = cinnamon_mount_operation_get_instance_private (self);
+}
+
+static void
+cinnamon_mount_operation_ask_password (GMountOperation *op,
+ const char *message,
+ const char *default_user,
+ const char *default_domain,
+ GAskPasswordFlags flags)
+{
+ /* do nothing */
+}
+
+static void
+cinnamon_mount_operation_ask_question (GMountOperation *op,
+ const char *message,
+ const char *choices[])
+{
+ /* do nothing */
+}
+
+static void
+cinnamon_mount_operation_show_processes (GMountOperation *operation,
+ const gchar *message,
+ GArray *processes,
+ const gchar *choices[])
+{
+ CinnamonMountOperation *self = CINNAMON_MOUNT_OPERATION (operation);
+
+ if (self->priv->pids != NULL)
+ {
+ g_array_unref (self->priv->pids);
+ self->priv->pids = NULL;
+ }
+
+ g_free (self->priv->message);
+ g_strfreev (self->priv->choices);
+
+ /* save the parameters */
+ self->priv->pids = g_array_ref (processes);
+ self->priv->choices = g_strdupv ((gchar **) choices);
+ self->priv->message = g_strdup (message);
+
+ g_signal_emit (self, signals[SHOW_PROCESSES_2], 0);
+}
+
+static void
+cinnamon_mount_operation_finalize (GObject *obj)
+{
+ CinnamonMountOperation *self = CINNAMON_MOUNT_OPERATION (obj);
+
+ g_strfreev (self->priv->choices);
+ g_free (self->priv->message);
+
+ if (self->priv->pids != NULL)
+ {
+ g_array_unref (self->priv->pids);
+ self->priv->pids = NULL;
+ }
+
+ G_OBJECT_CLASS (cinnamon_mount_operation_parent_class)->finalize (obj);
+}
+
+static void
+cinnamon_mount_operation_class_init (CinnamonMountOperationClass *klass)
+{
+ GMountOperationClass *mclass;
+ GObjectClass *oclass;
+
+ mclass = G_MOUNT_OPERATION_CLASS (klass);
+ mclass->show_processes = cinnamon_mount_operation_show_processes;
+ mclass->ask_question = cinnamon_mount_operation_ask_question;
+ mclass->ask_password = cinnamon_mount_operation_ask_password;
+
+ oclass = G_OBJECT_CLASS (klass);
+ oclass->finalize = cinnamon_mount_operation_finalize;
+
+ signals[SHOW_PROCESSES_2] =
+ g_signal_new ("show-processes-2",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+GMountOperation *
+cinnamon_mount_operation_new (void)
+{
+ return g_object_new (CINNAMON_TYPE_MOUNT_OPERATION, NULL);
+}
+
+/**
+ * cinnamon_mount_operation_get_show_processes_pids:
+ * @self: a #CinnamonMountOperation
+ *
+ * Returns: (transfer full) (element-type GPid): a #GArray
+ */
+GArray *
+cinnamon_mount_operation_get_show_processes_pids (CinnamonMountOperation *self)
+{
+ return g_array_ref (self->priv->pids);
+}
+
+/**
+ * cinnamon_mount_operation_get_show_processes_choices:
+ * @self: a #CinnamonMountOperation
+ *
+ * Returns: (transfer full):
+ */
+gchar **
+cinnamon_mount_operation_get_show_processes_choices (CinnamonMountOperation *self)
+{
+ return g_strdupv (self->priv->choices);
+}
+
+/**
+ * cinnamon_mount_operation_get_show_processes_message:
+ * @self: a #CinnamonMountOperation
+ *
+ * Returns: (transfer full):
+ */
+gchar *
+cinnamon_mount_operation_get_show_processes_message (CinnamonMountOperation *self)
+{
+ return g_strdup (self->priv->message);
+}
diff --git a/src/cinnamon-mount-operation.h b/src/cinnamon-mount-operation.h
new file mode 100644
index 0000000000..ed79929cd6
--- /dev/null
+++ b/src/cinnamon-mount-operation.h
@@ -0,0 +1,41 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ *
+ * Author: Cosimo Cecchi
+ *
+ */
+
+#ifndef __CINNAMON_MOUNT_OPERATION_H__
+#define __CINNAMON_MOUNT_OPERATION_H__
+
+#include
+
+G_BEGIN_DECLS
+
+#define CINNAMON_TYPE_MOUNT_OPERATION (cinnamon_mount_operation_get_type ())
+G_DECLARE_FINAL_TYPE (CinnamonMountOperation, cinnamon_mount_operation,
+ CINNAMON, MOUNT_OPERATION, GMountOperation)
+
+GMountOperation *cinnamon_mount_operation_new (void);
+
+GArray * cinnamon_mount_operation_get_show_processes_pids (CinnamonMountOperation *self);
+gchar ** cinnamon_mount_operation_get_show_processes_choices (CinnamonMountOperation *self);
+gchar * cinnamon_mount_operation_get_show_processes_message (CinnamonMountOperation *self);
+
+G_END_DECLS
+
+#endif /* __CINNAMON_MOUNT_OPERATION_H__ */
diff --git a/src/hotplug-sniffer/cinnamon-mime-sniffer.c b/src/hotplug-sniffer/cinnamon-mime-sniffer.c
index aaa50066c8..c17ca210eb 100644
--- a/src/hotplug-sniffer/cinnamon-mime-sniffer.c
+++ b/src/hotplug-sniffer/cinnamon-mime-sniffer.c
@@ -14,9 +14,7 @@
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA
- * 02110-1335, USA.
+ * along with this program; if not, see .
*
* Author: Cosimo Cecchi
*
@@ -45,8 +43,6 @@
#define DIRECTORY_LOAD_ITEMS_PER_CALLBACK 100
#define HIGH_SCORE_RATIO 0.10
-G_DEFINE_TYPE (CinnamonMimeSniffer, cinnamon_mime_sniffer, G_TYPE_OBJECT);
-
enum {
PROP_FILE = 1,
NUM_PROPERTIES
@@ -74,16 +70,26 @@ typedef struct {
gint total_items;
} DeepCountState;
+typedef struct _CinnamonMimeSnifferPrivate CinnamonMimeSnifferPrivate;
+
+struct _CinnamonMimeSniffer
+{
+ GObject parent_instance;
+
+ CinnamonMimeSnifferPrivate *priv;
+};
+
struct _CinnamonMimeSnifferPrivate {
GFile *file;
GCancellable *cancellable;
guint watchdog_id;
- GSimpleAsyncResult *async_result;
- gchar **sniffed_mime;
+ GTask *task;
};
+G_DEFINE_TYPE_WITH_PRIVATE (CinnamonMimeSniffer, cinnamon_mime_sniffer, G_TYPE_OBJECT);
+
static void deep_count_load (DeepCountState *state,
GFile *file);
@@ -181,6 +187,7 @@ prepare_async_result (DeepCountState *state)
GArray *results;
GPtrArray *sniffed_mime;
SniffedResult result;
+ char **mimes;
sniffed_mime = g_ptr_array_new ();
results = g_array_new (TRUE, TRUE, sizeof (SniffedResult));
@@ -222,16 +229,16 @@ prepare_async_result (DeepCountState *state)
out:
g_ptr_array_add (sniffed_mime, NULL);
- self->priv->sniffed_mime = (gchar **) g_ptr_array_free (sniffed_mime, FALSE);
+ mimes = (gchar **) g_ptr_array_free (sniffed_mime, FALSE);
g_array_free (results, TRUE);
- g_simple_async_result_complete_in_idle (self->priv->async_result);
+ g_task_return_pointer (self->priv->task, mimes, (GDestroyNotify)g_strfreev);
}
/* adapted from nautilus/libnautilus-private/nautilus-directory-async.c */
static void
deep_count_one (DeepCountState *state,
- GFileInfo *info)
+ GFileInfo *info)
{
GFile *subdir;
const char *content_type;
@@ -242,11 +249,13 @@ deep_count_one (DeepCountState *state,
subdir = g_file_get_child (state->file, g_file_info_get_name (info));
state->deep_count_subdirectories =
g_list_append (state->deep_count_subdirectories, subdir);
- }
+ }
else
{
content_type = g_file_info_get_content_type (info);
- add_content_type_to_cache (state, content_type);
+
+ if (content_type)
+ add_content_type_to_cache (state, content_type);
}
}
@@ -297,8 +306,8 @@ deep_count_next_dir (DeepCountState *state)
static void
deep_count_more_files_callback (GObject *source_object,
- GAsyncResult *res,
- gpointer user_data)
+ GAsyncResult *res,
+ gpointer user_data)
{
DeepCountState *state;
GList *files, *l;
@@ -311,10 +320,10 @@ deep_count_more_files_callback (GObject *source_object,
deep_count_finish (state);
return;
}
-
+
files = g_file_enumerator_next_files_finish (state->enumerator,
res, NULL);
-
+
for (l = files; l != NULL; l = l->next)
{
info = l->data;
@@ -345,8 +354,8 @@ deep_count_more_files_callback (GObject *source_object,
static void
deep_count_callback (GObject *source_object,
- GAsyncResult *res,
- gpointer user_data)
+ GAsyncResult *res,
+ gpointer user_data)
{
DeepCountState *state;
GFileEnumerator *enumerator;
@@ -361,7 +370,7 @@ deep_count_callback (GObject *source_object,
enumerator = g_file_enumerate_children_finish (G_FILE (source_object),
res, NULL);
-
+
if (enumerator == NULL)
{
deep_count_next_dir (state);
@@ -418,21 +427,18 @@ query_info_async_ready_cb (GObject *source,
if (error != NULL)
{
- g_simple_async_result_take_error (self->priv->async_result,
- error);
- g_simple_async_result_complete_in_idle (self->priv->async_result);
+ g_task_return_error (self->priv->task, error);
return;
}
if (g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY)
{
- g_simple_async_result_set_error (self->priv->async_result,
- G_IO_ERROR,
- G_IO_ERROR_NOT_DIRECTORY,
- "Not a directory");
- g_simple_async_result_complete_in_idle (self->priv->async_result);
- g_object_unref(info);
+ g_task_return_new_error (self->priv->task,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_DIRECTORY,
+ "Not a directory");
+
return;
}
@@ -477,27 +483,13 @@ cinnamon_mime_sniffer_dispose (GObject *object)
g_clear_object (&self->priv->file);
g_clear_object (&self->priv->cancellable);
- g_clear_object (&self->priv->async_result);
+ g_clear_object (&self->priv->task);
- if (self->priv->watchdog_id != 0)
- {
- g_source_remove (self->priv->watchdog_id);
- self->priv->watchdog_id = 0;
- }
+ g_clear_handle_id (&self->priv->watchdog_id, g_source_remove);
G_OBJECT_CLASS (cinnamon_mime_sniffer_parent_class)->dispose (object);
}
-static void
-cinnamon_mime_sniffer_finalize (GObject *object)
-{
- CinnamonMimeSniffer *self = CINNAMON_MIME_SNIFFER (object);
-
- g_strfreev (self->priv->sniffed_mime);
-
- G_OBJECT_CLASS (cinnamon_mime_sniffer_parent_class)->finalize (object);
-}
-
static void
cinnamon_mime_sniffer_get_property (GObject *object,
guint prop_id,
@@ -541,7 +533,6 @@ cinnamon_mime_sniffer_class_init (CinnamonMimeSnifferClass *klass)
oclass = G_OBJECT_CLASS (klass);
oclass->dispose = cinnamon_mime_sniffer_dispose;
- oclass->finalize = cinnamon_mime_sniffer_finalize;
oclass->get_property = cinnamon_mime_sniffer_get_property;
oclass->set_property = cinnamon_mime_sniffer_set_property;
@@ -550,19 +541,15 @@ cinnamon_mime_sniffer_class_init (CinnamonMimeSnifferClass *klass)
"File",
"The loaded file",
G_TYPE_FILE,
- G_PARAM_READWRITE);
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
- g_type_class_add_private (klass, sizeof (CinnamonMimeSnifferPrivate));
g_object_class_install_properties (oclass, NUM_PROPERTIES, properties);
}
static void
cinnamon_mime_sniffer_init (CinnamonMimeSniffer *self)
{
- self->priv =
- G_TYPE_INSTANCE_GET_PRIVATE (self,
- CINNAMON_TYPE_MIME_SNIFFER,
- CinnamonMimeSnifferPrivate);
+ self->priv = cinnamon_mime_sniffer_get_instance_private (self);
init_mimetypes ();
}
@@ -580,18 +567,16 @@ cinnamon_mime_sniffer_sniff_async (CinnamonMimeSniffer *self,
gpointer user_data)
{
g_assert (self->priv->watchdog_id == 0);
- g_assert (self->priv->async_result == NULL);
-
- self->priv->async_result =
- g_simple_async_result_new (G_OBJECT (self),
- callback, user_data,
- cinnamon_mime_sniffer_sniff_finish);
+ g_assert (self->priv->task == NULL);
self->priv->cancellable = g_cancellable_new ();
+ self->priv->task = g_task_new (self, self->priv->cancellable,
+ callback, user_data);
self->priv->watchdog_id =
g_timeout_add (WATCHDOG_TIMEOUT,
watchdog_timeout_reached_cb, self);
+ g_source_set_name_by_id (self->priv->watchdog_id, "[gnome-shell] watchdog_timeout_reached_cb");
start_loading_file (self);
}
@@ -601,8 +586,5 @@ cinnamon_mime_sniffer_sniff_finish (CinnamonMimeSniffer *self,
GAsyncResult *res,
GError **error)
{
- if (g_simple_async_result_propagate_error (self->priv->async_result, error))
- return NULL;
-
- return g_strdupv (self->priv->sniffed_mime);
+ return g_task_propagate_pointer (self->priv->task, error);
}
diff --git a/src/hotplug-sniffer/cinnamon-mime-sniffer.h b/src/hotplug-sniffer/cinnamon-mime-sniffer.h
index d6e47cd074..dcfcf934a1 100644
--- a/src/hotplug-sniffer/cinnamon-mime-sniffer.h
+++ b/src/hotplug-sniffer/cinnamon-mime-sniffer.h
@@ -13,9 +13,7 @@
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA
- * 02110-1335, USA.
+ * along with this program; if not, see .
*
* Author: Cosimo Cecchi
*
@@ -29,30 +27,9 @@
G_BEGIN_DECLS
-#define CINNAMON_TYPE_MIME_SNIFFER (cinnamon_mime_sniffer_get_type ())
-#define CINNAMON_MIME_SNIFFER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CINNAMON_TYPE_MIME_SNIFFER, CinnamonMimeSniffer))
-#define CINNAMON_IS_MIME_SNIFFER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CINNAMON_TYPE_MIME_SNIFFER))
-#define CINNAMON_MIME_SNIFFER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CINNAMON_TYPE_MIME_SNIFFER, CinnamonMimeSnifferClass))
-#define CINNAMON_IS_MIME_SNIFFER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CINNAMON_TYPE_MIME_SNIFFER))
-#define CINNAMON_MIME_SNIFFER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CINNAMON_TYPE_MIME_SNIFFER, CinnamonMimeSnifferClass))
-
-typedef struct _CinnamonMimeSniffer CinnamonMimeSniffer;
-typedef struct _CinnamonMimeSnifferPrivate CinnamonMimeSnifferPrivate;
-typedef struct _CinnamonMimeSnifferClass CinnamonMimeSnifferClass;
-
-struct _CinnamonMimeSniffer
-{
- GObject parent_instance;
-
- CinnamonMimeSnifferPrivate *priv;
-};
-
-struct _CinnamonMimeSnifferClass
-{
- GObjectClass parent_class;
-};
-
-GType cinnamon_mime_sniffer_get_type (void) G_GNUC_CONST;
+#define CINNAMON_TYPE_MIME_SNIFFER (cinnamon_mime_sniffer_get_type ())
+G_DECLARE_FINAL_TYPE (CinnamonMimeSniffer, cinnamon_mime_sniffer,
+ CINNAMON, MIME_SNIFFER, GObject)
CinnamonMimeSniffer *cinnamon_mime_sniffer_new (GFile *file);
diff --git a/src/hotplug-sniffer/hotplug-sniffer.c b/src/hotplug-sniffer/hotplug-sniffer.c
index 343d2e9ca4..6682db84fc 100644
--- a/src/hotplug-sniffer/hotplug-sniffer.c
+++ b/src/hotplug-sniffer/hotplug-sniffer.c
@@ -13,9 +13,7 @@
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street - Suite 500, Boston, MA
- * 02110-1335, USA.
+ * along with this program; if not, see .
*
* Authors: David Zeuthen
* Cosimo Cecchi
@@ -62,11 +60,7 @@ ensure_autoquit_off (void)
if (g_getenv ("HOTPLUG_SNIFFER_PERSIST") != NULL)
return;
- if (autoquit_id != 0)
- {
- g_source_remove (autoquit_id);
- autoquit_id = 0;
- }
+ g_clear_handle_id (&autoquit_id, g_source_remove);
}
static void
@@ -78,6 +72,7 @@ ensure_autoquit_on (void)
autoquit_id =
g_timeout_add_seconds (AUTOQUIT_TIMEOUT,
autoquit_timeout_cb, NULL);
+ g_source_set_name_by_id (autoquit_id, "[cinnamon] autoquit_timeout_cb");
}
typedef struct {
@@ -91,7 +86,7 @@ invocation_data_new (GVariant *params,
{
InvocationData *ret;
- ret = g_slice_new0 (InvocationData);
+ ret = g_new0 (InvocationData, 1);
ret->parameters = g_variant_ref (params);
ret->invocation = g_object_ref (invocation);
@@ -104,7 +99,7 @@ invocation_data_free (InvocationData *data)
g_variant_unref (data->parameters);
g_clear_object (&data->invocation);
- g_slice_free (InvocationData, data);
+ g_free (data);
}
static void
@@ -128,9 +123,9 @@ sniff_async_ready_cb (GObject *source,
g_dbus_method_invocation_return_value (data->invocation,
g_variant_new ("(^as)", types));
+ g_strfreev (types);
out:
- g_strfreev (types);
invocation_data_free (data);
ensure_autoquit_on ();
}
@@ -267,7 +262,7 @@ main (int argc,
/* ---------------------------------------------------------------------------------------------------- */
-static void
+static void __attribute__((format(printf, 1, 0)))
print_debug (const gchar *format, ...)
{
g_autofree char *s = NULL;
diff --git a/src/meson.build b/src/meson.build
index 548b94f8dc..ed2504d6ce 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -19,6 +19,7 @@ cinnamon_headers = [
'cinnamon-glsl-effect.h',
'cinnamon-gtk-embed.h',
'cinnamon-global.h',
+ 'cinnamon-mount-operation.h',
'cinnamon-perf-log.h',
'cinnamon-screen.h',
'cinnamon-screenshot.h',
@@ -53,6 +54,7 @@ cinnamon_sources = [
'cinnamon-global.c',
'cinnamon-keyring-prompt.c',
'cinnamon-keyring-prompt.h',
+ 'cinnamon-mount-operation.c',
'cinnamon-perf-log.c',
'cinnamon-polkit-authentication-agent.c',
'cinnamon-polkit-authentication-agent.h',