diff --git a/js/misc/util.js b/js/misc/util.js index 0d1d5c464..e61f0dd94 100644 --- a/js/misc/util.js +++ b/js/misc/util.js @@ -1,5 +1,16 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; + +const Gettext = imports.gettext.domain('gnome-shell'); +const _ = Gettext.gettext; + /* http://daringfireball.net/2010/07/improved_regex_for_matching_urls */ const _urlRegexp = new RegExp('\b(([a-z][\w-]+:(/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)([^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))', 'gi'); @@ -17,3 +28,123 @@ function findUrls(str) { res.push({ url: match[0], pos: match.index }); return res; } + +// spawn: +// @argv: an argv array +// +// Runs @argv in the background, handling any errors that occur +// when trying to start the program. +function spawn(argv) { + try { + trySpawn(argv); + } catch (err) { + _handleSpawnError(argv[0], err); + } +} + +// spawnCommandLine: +// @command_line: a command line +// +// Runs @command_line in the background, handling any errors that +// occur when trying to parse or start the program. +function spawnCommandLine(command_line) { + try { + let [success, argc, argv] = GLib.shell_parse_argv(command_line); + trySpawn(argv); + } catch (err) { + _handleSpawnError(command_line, err); + } +} + +// spawnDesktop: +// @id: a desktop file ID +// +// Spawns the desktop file identified by @id using startup notification, +// etc, handling any errors that occur when trying to find or start +// the program. +function spawnDesktop(id) { + try { + trySpawnDesktop(id); + } catch (err) { + _handleSpawnError(id, err); + } +} + +// trySpawn: +// @argv: an argv array +// +// Runs @argv in the background. If launching @argv fails, +// this will throw an error. +function trySpawn(argv) +{ + try { + GLib.spawn_async(null, argv, null, + GLib.SpawnFlags.SEARCH_PATH, + null, null); + } catch (err) { + // The exception from gjs contains an error string like: + // Error invoking GLib.spawn_command_line_async: Failed to + // execute child process "foo" (No such file or directory) + // We are only interested in the part in the parentheses. (And + // we can't pattern match the text, since it gets localized.) + err.message = err.message.replace(/.*\((.+)\)/, '$1'); + throw err; + } +} + +// trySpawnCommandLine: +// @command_line: a command line +// +// Runs @command_line in the background. If launching @command_line +// fails, this will throw an error. +function trySpawnCommandLine(command_line) { + let success, argc, argv; + + try { + [success, argc, argv] = GLib.shell_parse_argv(command_line); + } catch (err) { + // Replace "Error invoking GLib.shell_parse_argv: " with + // something nicer + err.message = err.message.replace(/[^:]*: /, _("Could not parse command:") + "\n"); + throw err; + } + + trySpawn(argv); +} + +// trySpawnDesktop: +// @id: a desktop file ID +// +// Spawns the desktop file identified by @id using startup notification. +// On error, throws an exception. +function trySpawnDesktop(id) { + let app; + + // shell_app_system_load_from_desktop_file() will end up returning + // a stupid error message if the desktop file doesn't exist, but + // that's the only case it returns an error for, so we just + // substitute our own error in instead + try { + app = Shell.AppSystem.get_default().load_from_desktop_file(id + '.desktop'); + } catch (err) { + throw new Error(_("No such application")); + } + + try { + app.launch(); + } catch(err) { + // see trySpawn + err.message = err.message.replace(/.*\((.+)\)/, '$1'); + throw err; + } +} + +function _handleSpawnError(command, err) { + let title = _("Execution of '%s' failed:").format(command); + + let source = new MessageTray.SystemNotificationSource(); + Main.messageTray.add(source); + let notification = new MessageTray.Notification(source, title, err.message); + notification.setTransient(true); + source.notify(notification); +} diff --git a/js/ui/main.js b/js/ui/main.js index 8574fe701..1ee806516 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -39,6 +39,7 @@ const WindowManager = imports.ui.windowManager; const Magnifier = imports.ui.magnifier; const XdndHandler = imports.ui.xdndHandler; const StatusIconDispatcher = imports.ui.statusIconDispatcher; +const Util = imports.misc.util; const DEFAULT_BACKGROUND_COLOR = new Clutter.Color(); DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff); @@ -317,11 +318,8 @@ function _globalKeyPressHandler(actor, event) { if (action == Meta.KeyBindingAction.COMMAND_SCREENSHOT) { let gconf = GConf.Client.get_default(); let command = gconf.get_string('/apps/metacity/keybinding_commands/command_screenshot'); - if (command != null && command != '') { - let [ok, len, args] = GLib.shell_parse_argv(command); - let p = new Shell.Process({'args' : args}); - p.run(); - } + if (command != null && command != '') + Util.spawnCommandLine(command); return true; } diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index e085421ef..57f5acf02 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -106,8 +106,7 @@ URLHighlighter.prototype = { return true; } catch (e) { // TODO: remove this after gnome 3 release - let p = new Shell.Process({ 'args' : ['gvfs-open', url] }); - p.run(); + Util.spawn(['gvfs-open', url]); return true; } } diff --git a/js/ui/placeDisplay.js b/js/ui/placeDisplay.js index fb96149c9..9b7134b89 100644 --- a/js/ui/placeDisplay.js +++ b/js/ui/placeDisplay.js @@ -14,6 +14,7 @@ const _ = Gettext.gettext; const DND = imports.ui.dnd; const Main = imports.ui.main; const Search = imports.ui.search; +const Util = imports.misc.util; const NAUTILUS_PREFS_DIR = '/apps/nautilus/preferences'; const DESKTOP_IS_HOME_KEY = NAUTILUS_PREFS_DIR + '/desktop_is_home_dir'; @@ -163,7 +164,7 @@ PlacesManager.prototype = { icon_size: size }); }, function () { - new Shell.Process({ args: ['nautilus-connect-server'] }).run(); + Util.spawn(['nautilus-connect-server']); }); let networkApp = null; diff --git a/js/ui/runDialog.js b/js/ui/runDialog.js index d09639ceb..955f1e777 100644 --- a/js/ui/runDialog.js +++ b/js/ui/runDialog.js @@ -14,6 +14,7 @@ const _ = Gettext.gettext; const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; const Tweener = imports.ui.tweener; +const Util = imports.misc.util; const MAX_FILE_DELETED_BEFORE_INVALID = 10; @@ -354,9 +355,7 @@ RunDialog.prototype = { try { if (inTerminal) command = 'gnome-terminal -x ' + input; - let [ok, len, args] = GLib.shell_parse_argv(command); - let p = new Shell.Process({ 'args' : args }); - p.run(); + Util.trySpawnCommandLine(command); } catch (e) { // Mmmh, that failed - see if @input matches an existing file let path = null; @@ -374,13 +373,8 @@ RunDialog.prototype = { global.create_app_launch_context()); } else { this._commandError = true; - // The exception contains an error string like: - // Error invoking Shell.run: Failed to execute child - // process "foo" (No such file or directory) - // We are only interested in the actual error, so parse - //that out. - let m = /.+\((.+)\)/.exec(e); - let errorStr = _("Execution of '%s' failed:").format(command) + '\n' + m[1]; + + let errorStr = _("Execution of '%s' failed:").format(command) + '\n' + e.message; this._errorMessage.set_text(errorStr); this._errorBox.show(); diff --git a/js/ui/status/accessibility.js b/js/ui/status/accessibility.js index f96e193a8..11a95f373 100644 --- a/js/ui/status/accessibility.js +++ b/js/ui/status/accessibility.js @@ -12,6 +12,7 @@ const St = imports.gi.St; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; @@ -108,8 +109,7 @@ ATIndicator.prototype = { this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addAction(_("Universal Access Settings"), function() { - let p = new Shell.Process({ args: ['gnome-control-center','universal-access'] }); - p.run(); + Util.spawnDesktop('gnome-universal-access-panel'); }); }, diff --git a/js/ui/status/power.js b/js/ui/status/power.js index 3a909c4cd..fe787f790 100644 --- a/js/ui/status/power.js +++ b/js/ui/status/power.js @@ -10,6 +10,7 @@ const St = imports.gi.St; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; @@ -83,7 +84,7 @@ Indicator.prototype = { this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addAction(_("Power Settings"),function() { - GLib.spawn_command_line_async('gnome-control-center power'); + Util.spawnDesktop('gnome-power-panel'); }); this._proxy.connect('Changed', Lang.bind(this, this._devicesChanged)); @@ -134,8 +135,7 @@ Indicator.prototype = { this._batteryItem.actor.reactive = true; this._batteryItem.actor.can_focus = true; this._batteryItem.connect('activate', function(item) { - let p = new Shell.Process({ args: ['gnome-power-statistics', '--device', device_id] }); - p.run(); + Util.spawn(['gnome-power-statistics', '--device', device_id]); }); } else { // virtual device @@ -164,8 +164,7 @@ Indicator.prototype = { let item = new DeviceItem (devices[i]); item.connect('activate', function() { - let p = new Shell.Process({ args: ['gnome-power-statistics', '--device', device_id] }); - p.run(); + Util.spawn(['gnome-power-statistics', '--device', device_id]); }); this._deviceItems.push(item); this.menu.addMenuItem(item, this._otherDevicePosition + position); @@ -198,7 +197,7 @@ Indicator.prototype = { _checkError: function(error) { if (!this._restarted && error && error.message.match(/org\.freedesktop\.DBus\.Error\.(UnknownMethod|InvalidArgs)/)) { GLib.spawn_command_line_sync('pkill -f "^gnome-power-manager$"'); - GLib.spawn_command_line_async('gnome-power-manager'); + Util.spawn(['gnome-power-manager']); this._restarted = true; } } diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js index 63f76fe0d..9211968f9 100644 --- a/js/ui/status/volume.js +++ b/js/ui/status/volume.js @@ -11,6 +11,7 @@ const St = imports.gi.St; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; @@ -60,8 +61,7 @@ Indicator.prototype = { this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addAction(_("Sound Settings"), function() { - let p = new Shell.Process({ args: ['gnome-control-center', 'sound'] }); - p.run(); + Util.spawnDesktop('gnome-sound-panel'); }); this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); diff --git a/js/ui/statusMenu.js b/js/ui/statusMenu.js index e2d13c68d..f834aad49 100644 --- a/js/ui/statusMenu.js +++ b/js/ui/statusMenu.js @@ -12,6 +12,7 @@ const GnomeSession = imports.misc.gnomeSession; const Main = imports.ui.main; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; // Adapted from gdm/gui/user-switch-applet/applet.c // @@ -31,7 +32,7 @@ StatusMenuButton.prototype = { this.actor.set_child(box); this._gdm = Gdm.UserManager.ref_default(); - this._gdm.queue_load() + this._gdm.queue_load(); this._user = this._gdm.get_user(GLib.get_user_name()); this._presence = new GnomeSession.Presence(); @@ -153,17 +154,17 @@ StatusMenuButton.prototype = { _onMyAccountActivate: function() { Main.overview.hide(); - this._spawn(['gnome-control-center', 'user-accounts']); + Util.spawnDesktop('gnome-user-accounts-panel'); }, _onPreferencesActivate: function() { Main.overview.hide(); - this._spawn(['gnome-control-center', '-o']); + Util.spawnDesktop('gnome-control-center'); }, _onLockScreenActivate: function() { Main.overview.hide(); - this._spawn(['gnome-screensaver-command', '--lock']); + Util.spawn(['gnome-screensaver-command', '--lock']); }, _onLoginScreenActivate: function() { @@ -174,19 +175,11 @@ StatusMenuButton.prototype = { _onQuitSessionActivate: function() { Main.overview.hide(); - this._spawn(['gnome-session-save', '--logout-dialog']); + Util.spawn(['gnome-session-save', '--logout-dialog']); }, _onShutDownActivate: function() { Main.overview.hide(); - this._spawn(['gnome-session-save', '--shutdown-dialog']); - }, - - _spawn: function(args) { - // FIXME: once Shell.Process gets support for signalling - // errors we should pop up an error dialog or something here - // on failure - let p = new Shell.Process({'args' : args}); - p.run(); + Util.spawn(['gnome-session-save', '--shutdown-dialog']); } }; diff --git a/po/POTFILES.in b/po/POTFILES.in index cb9c39f0b..255a3d651 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,6 +1,7 @@ data/gnome-shell.desktop.in.in data/org.gnome.shell.gschema.xml.in data/org.gnome.accessibility.magnifier.gschema.xml.in +js/misc/util.js js/ui/appDisplay.js js/ui/appFavorites.js js/ui/dash.js