// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; const Lang = imports.lang; const Signals = imports.signals; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Gtk = imports.gi.Gtk; const Pango = imports.gi.Pango; const St = imports.gi.St; const Shell = imports.gi.Shell; 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 ShellEntry = imports.ui.shellEntry; const LIST_ITEM_ICON_SIZE = 48; const REMEMBER_MOUNT_PASSWORD_KEY = 'remember-mount-password'; /* ------ Common Utils ------- */ function _setLabelText(label, text) { if (text) { label.set_text(text); label.show(); } else { label.set_text(''); label.hide(); } } function _setButtonsForChoices(dialog, choices) { let buttons = []; for (let idx = 0; idx < choices.length; idx++) { let button = idx; buttons.unshift({ label: choices[idx], action: Lang.bind(dialog, function() { dialog.emit('response', button); })}); } dialog.setButtons(buttons); } function _setLabelsForMessage(content, message) { let labels = message.split('\n'); content.title = labels.shift(); content.body = labels.join('\n'); } function _createIcon(gicon) { return new St.Icon({ gicon: gicon, style_class: 'shell-mount-operation-icon' }) } /* -------------------------------------------------------- */ const ListItem = new Lang.Class({ Name: 'ListItem', _init: function(app) { this._app = app; let layout = new St.BoxLayout({ vertical: false}); this.actor = new St.Button({ style_class: 'mount-dialog-app-list-item', can_focus: true, child: layout, reactive: true, x_align: St.Align.START, x_fill: true }); this._icon = this._app.create_icon_texture(LIST_ITEM_ICON_SIZE); let iconBin = new St.Bin({ style_class: 'mount-dialog-app-list-item-icon', child: this._icon }); layout.add(iconBin); this._nameLabel = new St.Label({ text: this._app.get_name(), style_class: 'mount-dialog-app-list-item-name' }); let labelBin = new St.Bin({ y_align: St.Align.MIDDLE, child: this._nameLabel }); layout.add(labelBin); this.actor.connect('clicked', Lang.bind(this, this._onClicked)); }, _onClicked: function() { this.emit('activate'); this._app.activate(); } }); Signals.addSignalMethods(ListItem.prototype); const ShellMountOperation = new Lang.Class({ Name: 'ShellMountOperation', _init: function(source, params) { params = Params.parse(params, { existingDialog: null }); this._dialog = null; this._dialogId = 0; this._existingDialog = params.existingDialog; this._processesDialog = null; this.mountOp = new Shell.MountOperation(); this.mountOp.connect('ask-question', Lang.bind(this, this._onAskQuestion)); this.mountOp.connect('ask-password', Lang.bind(this, this._onAskPassword)); this.mountOp.connect('show-processes-2', Lang.bind(this, this._onShowProcesses2)); this.mountOp.connect('aborted', Lang.bind(this, this.close)); this.mountOp.connect('show-unmount-progress', Lang.bind(this, this._onShowUnmountProgress)); this._gicon = source.get_icon(); }, _closeExistingDialog: function() { if (!this._existingDialog) return; this._existingDialog.close(); this._existingDialog = null; }, _onAskQuestion: function(op, message, choices) { this._closeExistingDialog(); this._dialog = new ShellMountQuestionDialog(this._gicon); this._dialogId = this._dialog.connect('response', Lang.bind(this, function(object, choice) { this.mountOp.set_choice(choice); this.mountOp.reply(Gio.MountOperationResult.HANDLED); this.close(); })); this._dialog.update(message, choices); this._dialog.open(); }, _onAskPassword: function(op, message, defaultUser, defaultDomain, flags) { if (this._existingDialog) { this._dialog = this._existingDialog; this._dialog.reaskPassword(); } else { this._dialog = new ShellMountPasswordDialog(message, this._gicon, flags); } this._dialogId = this._dialog.connect('response', Lang.bind(this, function(object, choice, password, remember) { 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.reply(Gio.MountOperationResult.HANDLED); } })); this._dialog.open(); }, close: function(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: function(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 ShellProcessesDialog(this._gicon); this._dialog = this._processesDialog; this._dialogId = this._processesDialog.connect('response', Lang.bind(this, function(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._processesDialog.open(); } this._processesDialog.update(message, processes, choices); }, _onShowUnmountProgress: function(op, message, timeLeft, bytesLeft) { if (!this._notifier) this._notifier = new ShellUnmountNotifier(); if (bytesLeft == 0) this._notifier.done(message); else this._notifier.show(message); }, borrowDialog: function() { if (this._dialogId != 0) { this._dialog.disconnect(this._dialogId); this._dialogId = 0; } return this._dialog; } }); const ShellUnmountNotifier = new Lang.Class({ Name: 'ShellUnmountNotifier', Extends: MessageTray.Source, _init: function() { this.parent('', 'media-removable'); this._notification = null; Main.messageTray.add(this); }, show: function(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: function(message) { if (this._notification) { this._notification.destroy(); this._notification = null; } if (message) { let notification = new MessageTray.Notification(this, message, null); notification.setTransient(true); this.notify(notification); } } }); const ShellMountQuestionDialog = new Lang.Class({ Name: 'ShellMountQuestionDialog', Extends: ModalDialog.ModalDialog, _init: function(icon) { this.parent({ styleClass: 'mount-dialog' }); this._content = new Dialog.MessageDialogContent({ icon }); this.contentLayout.add(this._content, { x_fill: true, y_fill: false }); }, update: function(message, choices) { _setLabelsForMessage(this._content, message); _setButtonsForChoices(this, choices); } }); Signals.addSignalMethods(ShellMountQuestionDialog.prototype); const ShellMountPasswordDialog = new Lang.Class({ Name: 'ShellMountPasswordDialog', Extends: ModalDialog.ModalDialog, _init: function(message, icon, flags) { let strings = message.split('\n'); let title = strings.shift() || null; let body = strings.shift() || null; this.parent({ styleClass: 'prompt-dialog' }); let content = new Dialog.MessageDialogContent({ icon, title, body }); this.contentLayout.add_actor(content); this._passwordBox = new St.BoxLayout({ vertical: false, style_class: 'prompt-dialog-password-box' }); content.messageBox.add(this._passwordBox); this._passwordLabel = new St.Label(({ style_class: 'prompt-dialog-password-label', text: _("Password") })); this._passwordBox.add(this._passwordLabel, { y_fill: false, y_align: St.Align.MIDDLE }); this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry', text: "", can_focus: true}); ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true }); this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivate)); this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE this._passwordBox.add(this._passwordEntry, {expand: true }); this.setInitialKeyFocus(this._passwordEntry); this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label', text: _("Sorry, that didn’t work. Please try again.") }); this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; this._errorMessageLabel.clutter_text.line_wrap = true; this._errorMessageLabel.hide(); content.messageBox.add(this._errorMessageLabel); if (flags & Gio.AskPasswordFlags.SAVING_SUPPORTED) { this._rememberChoice = new CheckBox.CheckBox(); this._rememberChoice.getLabelActor().text = _("Remember Password"); this._rememberChoice.actor.checked = global.settings.get_boolean(REMEMBER_MOUNT_PASSWORD_KEY); content.messageBox.add(this._rememberChoice.actor); } else { this._rememberChoice = null; } let buttons = [{ label: _("Cancel"), action: Lang.bind(this, this._onCancelButton), key: Clutter.Escape }, { label: _("Unlock"), action: Lang.bind(this, this._onUnlockButton), default: true }]; this.setButtons(buttons); }, reaskPassword: function() { this._passwordEntry.set_text(''); this._errorMessageLabel.show(); }, _onCancelButton: function() { this.emit('response', -1, '', false); }, _onUnlockButton: function() { this._onEntryActivate(); }, _onEntryActivate: function() { global.settings.set_boolean(REMEMBER_MOUNT_PASSWORD_KEY, this._rememberChoice && this._rememberChoice.actor.checked); this.emit('response', 1, this._passwordEntry.get_text(), this._rememberChoice && this._rememberChoice.actor.checked); } }); const ShellProcessesDialog = new Lang.Class({ Name: 'ShellProcessesDialog', Extends: ModalDialog.ModalDialog, _init: function(icon) { this.parent({ styleClass: 'mount-dialog' }); this._content = new Dialog.MessageDialogContent({ icon }); this.contentLayout.add(this._content, { x_fill: true, y_fill: false }); let scrollView = new St.ScrollView({ style_class: 'mount-dialog-app-list'}); scrollView.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); this.contentLayout.add(scrollView, { x_fill: true, y_fill: true }); scrollView.hide(); this._applicationList = new St.BoxLayout({ vertical: true }); scrollView.add_actor(this._applicationList); this._applicationList.connect('actor-added', Lang.bind(this, function() { if (this._applicationList.get_n_children() == 1) scrollView.show(); })); this._applicationList.connect('actor-removed', Lang.bind(this, function() { if (this._applicationList.get_n_children() == 0) scrollView.hide(); })); }, _setAppsForPids: function(pids) { // remove all the items this._applicationList.destroy_all_children(); pids.forEach(Lang.bind(this, function(pid) { let tracker = Shell.WindowTracker.get_default(); let app = tracker.get_app_from_pid(pid); if (!app) return; let item = new ListItem(app); this._applicationList.add(item.actor, { x_fill: true }); item.connect('activate', Lang.bind(this, function() { // use -1 to indicate Cancel this.emit('response', -1); })); })); }, update: function(message, processes, choices) { this._setAppsForPids(processes); _setLabelsForMessage(this._content, message); _setButtonsForChoices(this, choices); } }); Signals.addSignalMethods(ShellProcessesDialog.prototype); const GnomeShellMountOpIface = ' \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ '; const ShellMountOperationType = { NONE: 0, ASK_PASSWORD: 1, ASK_QUESTION: 2, SHOW_PROCESSES: 3 }; const GnomeShellMountOpHandler = new Lang.Class({ Name: 'GnomeShellMountOpHandler', _init: function() { this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellMountOpIface, 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._volumeMonitor = Gio.VolumeMonitor.get(); this._ensureEmptyRequest(); }, _ensureEmptyRequest: function() { this._currentId = null; this._currentInvocation = null; this._currentType = ShellMountOperationType.NONE; }, _clearCurrentRequest: function(response, details) { if (this._currentInvocation) { this._currentInvocation.return_value( GLib.Variant.new('(ua{sv})', [response, details])); } this._ensureEmptyRequest(); }, _setCurrentRequest: function(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: function() { if (this._dialog) { this._dialog.close(); this._dialog = null; } }, _createGIcon: function(iconName) { let realIconName = iconName ? iconName : 'drive-harddisk'; return new Gio.ThemedIcon({ name: realIconName, use_default_fallbacks: true }); }, /** * AskPassword: * @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. * @message: the message to display * @icon_name: the name of an icon to display * @default_user: the default username for display * @default_domain: the default domain for display * @flags: a set of GAskPasswordFlags * @response: a GMountOperationResult * @response_details: a dictionary containing the 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 * * 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: function(params, invocation) { let [id, message, iconName, defaultUser, defaultDomain, flags] = params; if (this._setCurrentRequest(invocation, id, ShellMountOperationType.ASK_PASSWORD)) { this._dialog.reaskPassword(); return; } this._closeDialog(); this._dialog = new ShellMountPasswordDialog(message, this._createGIcon(iconName), flags); this._dialog.connect('response', Lang.bind(this, function(object, choice, password, remember) { 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); } this._clearCurrentRequest(response, details); })); this._dialog.open(); }, /** * AskQuestion: * @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. * @message: the message to display * @icon_name: the name of an icon to display * @choices: an array of choice strings * GetResponse: * @response: a GMountOperationResult * @response_details: a dictionary containing the response details as * entered by the user. The dictionary MAY contain the following properties: * - "choice" -> (i): the chosen answer among the array of strings passed in * * 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: function(params, invocation) { let [id, message, iconName, choices] = params; if (this._setCurrentRequest(invocation, id, ShellMountOperationType.ASK_QUESTION)) { this._dialog.update(message, choices); return; } this._closeDialog(); this._dialog = new ShellMountQuestionDialog(this._createGIcon(iconName), message); this._dialog.connect('response', Lang.bind(this, function(object, choice) { this._clearCurrentRequest(Gio.MountOperationResult.HANDLED, { choice: GLib.Variant.new('i', choice) }); })); this._dialog.update(message, choices); this._dialog.open(); }, /** * ShowProcesses: * @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. * @message: the message to display * @icon_name: the name of an icon to display * @application_pids: the PIDs of the applications to display * @choices: an array of choice strings * @response: a GMountOperationResult * @response_details: a dictionary containing the response details as * entered by the user. The dictionary MAY contain the following properties: * - "choice" -> (i): the chosen answer among the array of strings passed in * * 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: function(params, invocation) { let [id, message, iconName, applicationPids, choices] = params; if (this._setCurrentRequest(invocation, id, ShellMountOperationType.SHOW_PROCESSES)) { this._dialog.update(message, applicationPids, choices); return; } this._closeDialog(); this._dialog = new ShellProcessesDialog(this._createGIcon(iconName)); this._dialog.connect('response', Lang.bind(this, function(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: * * Closes a dialog previously opened by AskPassword, AskQuestion or ShowProcesses. * If no dialog is open, does nothing. */ Close: function(params, invocation) { this._clearCurrentRequest(Gio.MountOperationResult.UNHANDLED, {}); this._closeDialog(); } });