From 297d1356a2e0e9144f2af1c81ada8c5a24d97a8d Mon Sep 17 00:00:00 2001 From: Cosimo Cecchi Date: Wed, 22 Jun 2011 16:43:16 -0400 Subject: [PATCH] mount-operation: add a ShellMountOperation implementation Ideally, this would be an entirely-JS implementation, but we have a couple of issues with gjs and gobject-introspection to work around, so we need a ShellMountOperation class for the time being. This first commit implements the show-processes dialog, with a system modal style very similar to the EndSession dialog. Implementations of ask-question and ask-password will follow shortly. https://bugzilla.gnome.org/show_bug.cgi?id=653520 --- data/theme/gnome-shell.css | 75 +++++++++++ js/Makefile.am | 1 + js/ui/automountManager.js | 9 +- js/ui/autorunManager.js | 12 +- js/ui/shellMountOperation.js | 241 +++++++++++++++++++++++++++++++++++ src/Makefile.am | 2 + src/shell-mount-operation.c | 183 ++++++++++++++++++++++++++ src/shell-mount-operation.h | 63 +++++++++ 8 files changed, 580 insertions(+), 6 deletions(-) create mode 100644 js/ui/shellMountOperation.js create mode 100644 src/shell-mount-operation.c create mode 100644 src/shell-mount-operation.h diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index d25d12c5b..af9fd9f32 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -1673,6 +1673,81 @@ StTooltip StLabel { color: #444444; } +/* Show Processes Dialog */ +.shell-mount-operation-icon { + icon-size: 48px; +} + +.show-processes-dialog { + spacing: 24px; +} + +.show-processes-dialog-subject { + font-size: 12pt; + font-weight: bold; + color: #666666; + padding-top: 10px; + padding-left: 17px; + padding-bottom: 6px; +} + +.show-processes-dialog-subject:rtl { + padding-left: 0px; + padding-right: 17px; +} + +.show-processes-dialog-description { + font-size: 10pt; + color: white; + padding-left: 17px; + width: 28em; +} + +.show-processes-dialog-description:rtl { + padding-right: 17px; +} + +.show-processes-dialog-app-list { + font-size: 10pt; + max-height: 200px; + padding-top: 24px; + padding-left: 49px; + padding-right: 32px; +} + +.show-processes-dialog-app-list:rtl { + padding-right: 49px; + padding-left: 32px; +} + +.show-processes-dialog-app-list-item { + color: #ccc; +} + +.show-processes-dialog-app-list-item:hover { + color: white; +} + +.show-processes-dialog-app-list-item:ltr { + padding-right: 1em; +} + +.show-processes-dialog-app-list-item:rtl { + padding-left: 1em; +} + +.show-processes-dialog-app-list-item-icon:ltr { + padding-right: 17px; +} + +.show-processes-dialog-app-list-item-icon:rtl { + padding-left: 17px; +} + +.show-processes-dialog-app-list-item-name { + font-size: 10pt; +} + /* PolicyKit Authentication Dialog */ .polkit-dialog { /* this is the width of the entire modal popup */ diff --git a/js/Makefile.am b/js/Makefile.am index 2964879b8..fadab2d1c 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -39,6 +39,7 @@ nobase_dist_js_DATA = \ ui/main.js \ ui/messageTray.js \ ui/modalDialog.js \ + ui/shellMountOperation.js \ ui/notificationDaemon.js \ ui/overview.js \ ui/panel.js \ diff --git a/js/ui/automountManager.js b/js/ui/automountManager.js index 3ebeb12e6..9bc426f94 100644 --- a/js/ui/automountManager.js +++ b/js/ui/automountManager.js @@ -7,6 +7,7 @@ const Gio = imports.gi.Gio; const Params = imports.misc.params; const Main = imports.ui.main; +const ShellMountOperation = imports.ui.shellMountOperation; const ScreenSaver = imports.misc.screenSaver; // GSettings keys @@ -157,8 +158,12 @@ AutomountManager.prototype = { return; } - // TODO: mount op - this._mountVolume(volume, null); + if (params.useMountOp) { + let operation = new ShellMountOperation.ShellMountOperation(volume); + this._mountVolume(volume, operation.mountOp); + } else { + this._mountVolume(volume, null); + } }, _mountVolume: function(volume, operation) { diff --git a/js/ui/autorunManager.js b/js/ui/autorunManager.js index 0b37f808b..368467c22 100644 --- a/js/ui/autorunManager.js +++ b/js/ui/autorunManager.js @@ -7,6 +7,7 @@ const St = imports.gi.St; const Main = imports.ui.main; const MessageTray = imports.ui.messageTray; +const ShellMountOperation = imports.ui.shellMountOperation; // GSettings keys const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling'; @@ -202,13 +203,13 @@ AutorunManager.prototype = { }, ejectMount: function(mount) { - // TODO: we need to have a StMountOperation here to e.g. trigger - // shell dialogs when applications are blocking the mount. + let mountOp = new ShellMountOperation.ShellMountOperation(mount); + if (mount.can_eject()) - mount.eject_with_operation(0, null, null, + mount.eject_with_operation(0, mountOp.mountOp, null, Lang.bind(this, this._onMountEject)); else - mount.unmount_with_operation(0, null, null, + mount.unmount_with_operation(0, mountOp.mountOp, null, Lang.bind(this, this._onMountEject)); }, @@ -219,6 +220,9 @@ AutorunManager.prototype = { else mount.unmount_with_operation_finish(res); } catch (e) { + // FIXME: we need to ignore G_IO_ERROR_FAILED_HANDLED errors here + // but we can't access the error code from JS. + // See https://bugzilla.gnome.org/show_bug.cgi?id=591480 log('Unable to eject the mount ' + mount.get_name() + ': ' + e.toString()); } diff --git a/js/ui/shellMountOperation.js b/js/ui/shellMountOperation.js new file mode 100644 index 000000000..e7432fe6d --- /dev/null +++ b/js/ui/shellMountOperation.js @@ -0,0 +1,241 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Lang = imports.lang; +const Signals = imports.signals; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const Pango = imports.gi.Pango; +const St = imports.gi.St; +const Shell = imports.gi.Shell; + +const ModalDialog = imports.ui.modalDialog; + +const LIST_ITEM_ICON_SIZE = 48; + +function _setLabelText(label, text) { + if (text) { + label.set_text(text); + label.show(); + } else { + label.set_text(''); + label.hide(); + } +} + +function ListItem(app) { + this._init(app); +} + +ListItem.prototype = { + _init: function(app) { + this._app = app; + + let layout = new St.BoxLayout({ vertical: false}); + + this.actor = new St.Button({ style_class: 'show-processes-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: 'show-processes-dialog-app-list-item-icon', + child: this._icon }); + layout.add(iconBin); + + this._nameLabel = new St.Label({ text: this._app.get_name(), + style_class: 'show-processes-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(-1); + } +}; +Signals.addSignalMethods(ListItem.prototype); + +function ShellMountOperation(source) { + this._init(source); +} + +ShellMountOperation.prototype = { + _init: function(source) { + 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._onAborted)); + + this._icon = new St.Icon({ gicon: source.get_icon(), + style_class: 'shell-mount-operation-icon' }); + }, + + _onAskQuestion: function(op, message, choices) { + // TODO + }, + + _onAskPassword: function(op, message, defaultUser, defaultDomain, flags) { + // TODO + }, + + _onAborted: function(op) { + // TODO + }, + + _onShowProcesses2: function(op) { + 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._icon); + this._processesDialog.connect('choice-chosen', + 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._processesDialog.close(global.get_current_time()); + })); + this._processesDialog.open(global.get_current_time()); + } + + this._processesDialog.update(message, processes, choices); + }, +} + +function ShellProcessesDialog(icon) { + this._init(icon); +} + +ShellProcessesDialog.prototype = { + __proto__: ModalDialog.ModalDialog.prototype, + + _init: function(icon) { + ModalDialog.ModalDialog.prototype._init.call(this, { styleClass: 'show-processes-dialog' }); + + let mainContentLayout = new St.BoxLayout(); + this.contentLayout.add(mainContentLayout, { x_fill: true, + y_fill: false }); + + this._iconBin = new St.Bin({ child: icon }); + mainContentLayout.add(this._iconBin, + { x_fill: true, + y_fill: false, + x_align: St.Align.END, + y_align: St.Align.MIDDLE }); + + let messageLayout = new St.BoxLayout({ vertical: true }); + mainContentLayout.add(messageLayout, + { y_align: St.Align.START }); + + this._subjectLabel = new St.Label({ style_class: 'show-processes-dialog-subject' }); + + messageLayout.add(this._subjectLabel, + { y_fill: false, + y_align: St.Align.START }); + + this._descriptionLabel = new St.Label({ style_class: 'show-processes-dialog-description' }); + this._descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this._descriptionLabel.clutter_text.line_wrap = true; + + messageLayout.add(this._descriptionLabel, + { y_fill: true, + y_align: St.Align.START }); + + let scrollView = new St.ScrollView({ style_class: 'show-processes-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, + { x_fill: true, + y_fill: true, + x_align: St.Align.START, + y_align: St.Align.MIDDLE }); + + this._applicationList.connect('actor-added', + Lang.bind(this, function() { + if (this._applicationList.get_children().length == 1) + scrollView.show(); + })); + + this._applicationList.connect('actor-removed', + Lang.bind(this, function() { + if (this._applicationList.get_children().length == 0) + scrollView.hide(); + })); + }, + + _setButtonsForChoices: function(choices) { + let buttons = []; + + for (let idx = 0; idx < choices.length; idx++) { + let button = idx; + buttons.unshift({ label: choices[idx], + action: Lang.bind(this, function() { + this.emit('choice-chosen', button); + })}); + } + + this.setButtons(buttons); + }, + + _setAppsForPids: function(pids) { + // remove all the items + this._applicationList.destroy_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('choice-chosen', -1); + })); + })); + }, + + _setLabelsForMessage: function(message) { + let labels = message.split('\n'); + + _setLabelText(this._subjectLabel, labels[0]); + if (labels.length > 1) + _setLabelText(this._descriptionLabel, labels[1]); + }, + + update: function(message, processes, choices) { + this._setLabelsForMessage(message); + this._setAppsForPids(processes); + this._setButtonsForChoices(choices); + } +} +Signals.addSignalMethods(ShellProcessesDialog.prototype); \ No newline at end of file diff --git a/src/Makefile.am b/src/Makefile.am index 9308d15e7..3f9e5c30a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -108,6 +108,7 @@ shell_public_headers_h = \ shell-gtk-embed.h \ shell-global.h \ shell-mobile-providers.h \ + shell-mount-operation.h \ shell-perf-log.h \ shell-slicer.h \ shell-stack.h \ @@ -141,6 +142,7 @@ libgnome_shell_la_SOURCES = \ shell-gtk-embed.c \ shell-global.c \ shell-mobile-providers.c \ + shell-mount-operation.c \ shell-perf-log.c \ shell-polkit-authentication-agent.h \ shell-polkit-authentication-agent.c \ diff --git a/src/shell-mount-operation.c b/src/shell-mount-operation.c new file mode 100644 index 000000000..0d86ef92e --- /dev/null +++ b/src/shell-mount-operation.c @@ -0,0 +1,183 @@ +/* + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Author: Cosimo Cecchi + * + */ + +#include "shell-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 outselves + * instead, so we just override the default methods with empty ones. + * + * 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 + */ +G_DEFINE_TYPE (ShellMountOperation, shell_mount_operation, G_TYPE_MOUNT_OPERATION); + +enum { + SHOW_PROCESSES_2, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +struct _ShellMountOperationPrivate { + GArray *pids; + gchar **choices; + gchar *message; +}; + +static void +shell_mount_operation_init (ShellMountOperation *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, SHELL_TYPE_MOUNT_OPERATION, + ShellMountOperationPrivate); +} + +static void +shell_mount_operation_ask_password (GMountOperation *op, + const char *message, + const char *default_user, + const char *default_domain, + GAskPasswordFlags flags) +{ + /* do nothing */ +} + +static void +shell_mount_operation_ask_question (GMountOperation *op, + const char *message, + const char *choices[]) +{ + /* do nothing */ +} + +static void +shell_mount_operation_show_processes (GMountOperation *operation, + const gchar *message, + GArray *processes, + const gchar *choices[]) +{ + ShellMountOperation *self = SHELL_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 +shell_mount_operation_finalize (GObject *obj) +{ + ShellMountOperation *self = SHELL_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 (shell_mount_operation_parent_class)->finalize (obj); +} + +static void +shell_mount_operation_class_init (ShellMountOperationClass *klass) +{ + GMountOperationClass *mclass; + GObjectClass *oclass; + + mclass = G_MOUNT_OPERATION_CLASS (klass); + mclass->show_processes = shell_mount_operation_show_processes; + mclass->ask_question = shell_mount_operation_ask_question; + mclass->ask_password = shell_mount_operation_ask_password; + + oclass = G_OBJECT_CLASS (klass); + oclass->finalize = shell_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, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + g_type_class_add_private (klass, sizeof (ShellMountOperationPrivate)); +} + +GMountOperation * +shell_mount_operation_new (void) +{ + return g_object_new (SHELL_TYPE_MOUNT_OPERATION, NULL); +} + +/** + * shell_mount_operation_get_show_processes_pids: + * @self: a #ShellMountOperation + * + * Returns: (transfer full) (element-type GPid): a #GArray + */ +GArray * +shell_mount_operation_get_show_processes_pids (ShellMountOperation *self) +{ + return g_array_ref (self->priv->pids); +} + +/** + * shell_mount_operation_get_show_processes_choices: + * @self: a #ShellMountOperation + * + * Returns: (transfer full): + */ +gchar ** +shell_mount_operation_get_show_processes_choices (ShellMountOperation *self) +{ + return g_strdupv (self->priv->choices); +} + +/** + * shell_mount_operation_get_show_processes_message: + * @self: a #ShellMountOperation + * + * Returns: (transfer full): + */ +gchar * +shell_mount_operation_get_show_processes_message (ShellMountOperation *self) +{ + return g_strdup (self->priv->message); +} diff --git a/src/shell-mount-operation.h b/src/shell-mount-operation.h new file mode 100644 index 000000000..222809cd0 --- /dev/null +++ b/src/shell-mount-operation.h @@ -0,0 +1,63 @@ +/* + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Author: Cosimo Cecchi + * + */ + +#ifndef __SHELL_MOUNT_OPERATION_H__ +#define __SHELL_MOUNT_OPERATION_H__ + +#include + +G_BEGIN_DECLS + +#define SHELL_TYPE_MOUNT_OPERATION (shell_mount_operation_get_type ()) +#define SHELL_MOUNT_OPERATION(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SHELL_TYPE_MOUNT_OPERATION, ShellMountOperation)) +#define SHELL_MOUNT_OPERATION_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), SHELL_TYPE_MOUNT_OPERATION, ShellMountOperationClass)) +#define SHELL_IS_MOUNT_OPERATION(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SHELL_TYPE_MOUNT_OPERATION)) +#define SHELL_IS_MOUNT_OPERATION_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SHELL_TYPE_MOUNT_OPERATION)) +#define SHELL_MOUNT_OPERATION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SHELL_TYPE_MOUNT_OPERATION, ShellMountOperationClass)) + +typedef struct _ShellMountOperation ShellMountOperation; +typedef struct _ShellMountOperationClass ShellMountOperationClass; +typedef struct _ShellMountOperationPrivate ShellMountOperationPrivate; + +struct _ShellMountOperation +{ + GMountOperation parent_instance; + + ShellMountOperationPrivate *priv; +}; + +struct _ShellMountOperationClass +{ + GMountOperationClass parent_class; +}; + + +GType shell_mount_operation_get_type (void); +GMountOperation *shell_mount_operation_new (void); + +GArray * shell_mount_operation_get_show_processes_pids (ShellMountOperation *self); +gchar ** shell_mount_operation_get_show_processes_choices (ShellMountOperation *self); +gchar * shell_mount_operation_get_show_processes_message (ShellMountOperation *self); + +G_END_DECLS + +#endif /* __SHELL_MOUNT_OPERATION_H__ */