diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css
index 47c3090f7..c27717d88 100644
--- a/data/theme/gnome-shell-high-contrast.css
+++ b/data/theme/gnome-shell-high-contrast.css
@@ -399,6 +399,34 @@ StScrollBar {
width: 48px;
height: 48px; }
+/* Audio selection dialog */
+.audio-device-selection-dialog {
+ spacing: 30px; }
+
+.audio-selection-content {
+ spacing: 20px;
+ padding: 24px; }
+
+.audio-selection-title {
+ font-weight: bold;
+ text-align: center; }
+
+.audio-selection-box {
+ spacing: 20px; }
+
+.audio-selection-device {
+ border: 1px solid rgba(238, 238, 236, 0.2);
+ border-radius: 12px; }
+ .audio-selection-device:active, .audio-selection-device:hover, .audio-selection-device:focus {
+ background-color: #215d9c; }
+
+.audio-selection-device-box {
+ padding: 20px;
+ spacing: 20px; }
+
+.audio-selection-device-icon {
+ icon-size: 64px; }
+
/* Network Agent Dialog */
.network-dialog-secret-table {
spacing-rows: 15px;
diff --git a/data/theme/gnome-shell-sass b/data/theme/gnome-shell-sass
index c67499686..9fb391883 160000
--- a/data/theme/gnome-shell-sass
+++ b/data/theme/gnome-shell-sass
@@ -1 +1 @@
-Subproject commit c67499686eb40b821784561fbb0596cc470d8017
+Subproject commit 9fb3918831459cd002f3d621494cf5eac70fe46a
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 0c94b1978..2e09c1c65 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -399,6 +399,34 @@ StScrollBar {
width: 48px;
height: 48px; }
+/* Audio selection dialog */
+.audio-device-selection-dialog {
+ spacing: 30px; }
+
+.audio-selection-content {
+ spacing: 20px;
+ padding: 24px; }
+
+.audio-selection-title {
+ font-weight: bold;
+ text-align: center; }
+
+.audio-selection-box {
+ spacing: 20px; }
+
+.audio-selection-device {
+ border: 1px solid rgba(238, 238, 236, 0.2);
+ border-radius: 12px; }
+ .audio-selection-device:active, .audio-selection-device:hover, .audio-selection-device:focus {
+ background-color: #215d9c; }
+
+.audio-selection-device-box {
+ padding: 20px;
+ spacing: 20px; }
+
+.audio-selection-device-icon {
+ icon-size: 64px; }
+
/* Network Agent Dialog */
.network-dialog-secret-table {
spacing-rows: 15px;
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index bd8f30708..169d952db 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -35,6 +35,7 @@
ui/animation.js
ui/appDisplay.js
ui/appFavorites.js
+ ui/audioDeviceSelection.js
ui/backgroundMenu.js
ui/background.js
ui/boxpointer.js
diff --git a/js/ui/audioDeviceSelection.js b/js/ui/audioDeviceSelection.js
new file mode 100644
index 000000000..c97184de9
--- /dev/null
+++ b/js/ui/audioDeviceSelection.js
@@ -0,0 +1,216 @@
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+const Meta = imports.gi.Meta;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+
+const Main = imports.ui.main;
+const ModalDialog = imports.ui.modalDialog;
+
+const AudioDevice = {
+ HEADPHONES: 1 << 0,
+ HEADSET: 1 << 1,
+ MICROPHONE: 1 << 2
+};
+
+const AudioDeviceSelectionIface = ' \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+';
+
+const AudioDeviceSelectionDialog = new Lang.Class({
+ Name: 'AudioDeviceSelectionDialog',
+ Extends: ModalDialog.ModalDialog,
+
+ _init: function(devices) {
+ this.parent({ styleClass: 'audio-device-selection-dialog' });
+
+ this._deviceItems = {};
+
+ this._buildLayout();
+
+ if (devices & AudioDevice.HEADPHONES)
+ this._addDevice(AudioDevice.HEADPHONES);
+ if (devices & AudioDevice.HEADSET)
+ this._addDevice(AudioDevice.HEADSET);
+ if (devices & AudioDevice.MICROPHONE)
+ this._addDevice(AudioDevice.MICROPHONE);
+
+ if (this._selectionBox.get_n_children() < 2)
+ throw new Error('Too few devices for a selection');
+ },
+
+ destroy: function() {
+ this.parent();
+ },
+
+ _buildLayout: function(devices) {
+ let title = new St.Label({ style_class: 'audio-selection-title',
+ text: _("Select Audio Device"),
+ x_align: Clutter.ActorAlign.CENTER });
+
+ this.contentLayout.style_class = 'audio-selection-content';
+ this.contentLayout.add(title);
+
+ this._selectionBox = new St.BoxLayout({ style_class: 'audio-selection-box' });
+ this.contentLayout.add(this._selectionBox, { expand: true });
+
+ this.addButton({ action: Lang.bind(this, this._openSettings),
+ label: _("Sound Settings") });
+ this.addButton({ action: Lang.bind(this, this.close),
+ label: _("Cancel"),
+ key: Clutter.Escape });
+ },
+
+ _getDeviceLabel: function(device) {
+ switch(device) {
+ case AudioDevice.HEADPHONES:
+ return _("Headphones");
+ case AudioDevice.HEADSET:
+ return _("Headset");
+ case AudioDevice.MICROPHONE:
+ return _("Microphone");
+ default:
+ return null;
+ }
+ },
+
+ _getDeviceIcon: function(device) {
+ switch(device) {
+ case AudioDevice.HEADPHONES:
+ return 'audio-headphones-symbolic';
+ case AudioDevice.HEADSET:
+ return 'audio-headset-symbolic';
+ case AudioDevice.MICROPHONE:
+ return 'audio-input-microphone-symbolic';
+ default:
+ return null;
+ }
+ },
+
+ _addDevice: function(device) {
+ let box = new St.BoxLayout({ style_class: 'audio-selection-device-box',
+ vertical: true });
+ box.connect('notify::height',
+ function() {
+ Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
+ function() {
+ box.width = box.height;
+ });
+ });
+
+ let icon = new St.Icon({ style_class: 'audio-selection-device-icon',
+ icon_name: this._getDeviceIcon(device) });
+ box.add(icon);
+
+ let label = new St.Label({ style_class: 'audio-selection-device-label',
+ text: this._getDeviceLabel(device),
+ x_align: Clutter.ActorAlign.CENTER });
+ box.add(label);
+
+ let button = new St.Button({ style_class: 'audio-selection-device',
+ can_focus: true,
+ child: box });
+ this._selectionBox.add(button);
+
+ button.connect('clicked', Lang.bind(this,
+ function() {
+ this.emit('device-selected', device);
+ this.close();
+ Main.overview.hide();
+ }));
+ },
+
+ _openSettings: function() {
+ let desktopFile = 'gnome-sound-panel.desktop'
+ let app = Shell.AppSystem.get_default().lookup_app(desktopFile);
+
+ if (!app) {
+ log('Settings panel for desktop file ' + desktopFile + ' could not be loaded!');
+ return;
+ }
+
+ this.close();
+ Main.overview.hide();
+ app.activate();
+ }
+});
+
+const AudioDeviceSelectionDBus = new Lang.Class({
+ Name: 'AudioDeviceSelectionDBus',
+
+ _init: function() {
+ this._audioSelectionDialog = null;
+
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(AudioDeviceSelectionIface, this);
+ this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/AudioDeviceSelection');
+
+ Gio.DBus.session.own_name('org.gnome.Shell.AudioDeviceSelection', Gio.BusNameOwnerFlags.REPLACE, null, null);
+ },
+
+ _onDialogClosed: function() {
+ this._audioSelectionDialog = null;
+ },
+
+ _onDeviceSelected: function(dialog, device) {
+ let connection = this._dbusImpl.get_connection();
+ let info = this._dbusImpl.get_info();
+ let deviceName = Object.keys(AudioDevice).filter(
+ function(dev) {
+ return AudioDevice[dev] == device;
+ })[0].toLowerCase();
+ connection.emit_signal(this._audioSelectionDialog._sender,
+ this._dbusImpl.get_object_path(),
+ info ? info.name : null,
+ 'DeviceSelected',
+ GLib.Variant.new('(s)', [deviceName]));
+ },
+
+ OpenAsync: function(params, invocation) {
+ if (this._audioSelectionDialog) {
+ invocation.return_value(null);
+ return;
+ }
+
+ let [deviceNames] = params;
+ let devices = 0;
+ deviceNames.forEach(function(n) {
+ devices |= AudioDevice[n.toUpperCase()];
+ });
+
+ let dialog;
+ try {
+ dialog = new AudioDeviceSelectionDialog(devices);
+ } catch(e) {
+ invocation.return_value(null);
+ return;
+ }
+ dialog._sender = invocation.get_sender();
+
+ dialog.connect('closed', Lang.bind(this, this._onDialogClosed));
+ dialog.connect('device-selected',
+ Lang.bind(this, this._onDeviceSelected));
+ dialog.open();
+
+ this._audioSelectionDialog = dialog;
+ invocation.return_value(null);
+ },
+
+ CloseAsync: function(params, invocation) {
+ if (this._audioSelectionDialog &&
+ this._audioSelectionDialog._sender == invocation.get_sender())
+ this._audioSelectionDialog.close();
+
+ invocation.return_value(null);
+ }
+});
diff --git a/js/ui/main.js b/js/ui/main.js
index 377040342..ad95ce0d4 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -11,6 +11,7 @@ const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
+const AudioDeviceSelection = imports.ui.audioDeviceSelection;
const Components = imports.ui.components;
const CtrlAltTab = imports.ui.ctrlAltTab;
const EndSessionDialog = imports.ui.endSessionDialog;
@@ -62,6 +63,7 @@ let ctrlAltTabManager = null;
let osdWindowManager = null;
let osdMonitorLabeler = null;
let sessionMode = null;
+let shellAudioSelectionDBusService = null;
let shellDBusService = null;
let shellMountOpDBusService = null;
let screenSaverDBus = null;
@@ -120,6 +122,7 @@ function start() {
_loadDefaultStylesheet);
_initializeUI();
+ shellAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus();
shellDBusService = new ShellDBus.GnomeShell();
shellMountOpDBusService = new ShellMountOperation.GnomeShellMountOpHandler();