From 9e99a8c25a60183b9a7985bff2c77b3270c65ab2 Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Sat, 24 Jul 2010 13:57:53 +0200 Subject: [PATCH] Bluetooth status indicator Introduce the new Bluetooth indicator in the System Status area. It is written in JS with St and uses the new GnomeBluetoothApplet library. https://bugzilla.gnome.org/show_bug.cgi?id=618312 --- .gitignore | 1 + configure.ac | 14 +- data/theme/gnome-shell.css | 4 +- js/Makefile.am | 2 + js/misc/config.js.in | 3 + js/ui/main.js | 1 + js/ui/panel.js | 38 +-- js/ui/status/bluetooth.js | 446 ++++++++++++++++++++++++++++++++ src/Makefile.am | 2 + src/gnome-shell-plugin.c | 4 + tools/build/gnome-shell.modules | 10 + 11 files changed, 506 insertions(+), 19 deletions(-) create mode 100644 js/misc/config.js.in create mode 100644 js/ui/status/bluetooth.js diff --git a/.gitignore b/.gitignore index f717e24bb..5a035eb4c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ data/org.gnome.shell.gschema.xml data/org.gnome.shell.gschema.valid data/org.gnome.accessibility.magnifier.gschema.xml data/org.gnome.accessibility.magnifier.gschema.valid +js/misc/config.js intltool-extract.in intltool-merge.in intltool-update.in diff --git a/configure.ac b/configure.ac index daf833cfb..353ac946c 100644 --- a/configure.ac +++ b/configure.ac @@ -92,9 +92,20 @@ PKG_CHECK_MODULES(ST, clutter-1.0 gtk+-3.0 libcroco-0.6 gnome-desktop-3.0 >= 2.9 PKG_CHECK_MODULES(GDMUSER, dbus-glib-1 gtk+-3.0) PKG_CHECK_MODULES(TRAY, gtk+-3.0) PKG_CHECK_MODULES(GVC, libpulse libpulse-mainloop-glib gobject-2.0) - PKG_CHECK_MODULES(JS_TEST, clutter-x11-1.0 gjs-1.0 gobject-introspection-1.0 gtk+-3.0) +AC_MSG_CHECKING([for bluetooth support]) +PKG_CHECK_EXISTS([gnome-bluetooth-1.0 >= 2.90.0], + [BLUETOOTH_DIR=`$PKG_CONFIG --variable=libdir gnome-bluetooth-1.0`/gnome-bluetooth + BLUETOOTH_LIBS="-L'$BLUETOOTH_DIR' -lgnome-bluetooth-applet" + AC_SUBST([BLUETOOTH_LIBS],["$BLUETOOTH_LIBS"]) + AC_DEFINE_UNQUOTED([BLUETOOTH_DIR],["$BLUETOOTH_DIR"],[Path to installed GnomeBluetooth typelib and library]) + AC_DEFINE([HAVE_BLUETOOTH],[1],[Define if you have libgnome-bluetooth-applet]) + AC_SUBST([HAVE_BLUETOOTH],[1]) + AC_MSG_RESULT([yes])], + [AC_DEFINE([HAVE_BLUETOOTH],[0]) + AC_MSG_RESULT([no])]) + MUTTER_BIN_DIR=`$PKG_CONFIG --variable=exec_prefix mutter-plugins`/bin # FIXME: metacity-plugins.pc should point directly to its .gir file MUTTER_LIB_DIR=`$PKG_CONFIG --variable=libdir mutter-plugins` @@ -165,6 +176,7 @@ AC_CONFIG_FILES([ Makefile data/Makefile js/Makefile + js/misc/config.js src/Makefile tests/Makefile po/Makefile.in diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 1eaefbfdd..e6b483b8e 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -883,7 +883,7 @@ StTooltip StLabel { color: #bbbbbb; } -.chat-response { +#notification StEntry { padding: 4px; border-radius: 4px; border: 1px solid #565656; @@ -893,7 +893,7 @@ StTooltip StLabel { caret-size: 1px; } -.chat-response:focus { +#notification StEntry:focus { border: 1px solid #3a3a3a; color: #545454; background-color: #e8e8e8; diff --git a/js/Makefile.am b/js/Makefile.am index 98ec73751..64c9ef97e 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -2,6 +2,7 @@ jsdir = $(pkgdatadir)/js nobase_dist_js_DATA = \ + misc/config.js \ misc/docInfo.js \ misc/fileUtils.js \ misc/format.js \ @@ -47,6 +48,7 @@ nobase_dist_js_DATA = \ ui/status/accessibility.js \ ui/status/power.js \ ui/status/volume.js \ + ui/status/bluetooth.js \ ui/telepathyClient.js \ ui/tweener.js \ ui/viewSelector.js \ diff --git a/js/misc/config.js.in b/js/misc/config.js.in new file mode 100644 index 000000000..db8c6da5e --- /dev/null +++ b/js/misc/config.js.in @@ -0,0 +1,3 @@ +/* mode: js2; indent-tabs-mode: nil; tab-size: 4 */ +const HAVE_BLUETOOTH = @HAVE_BLUETOOTH@; + diff --git a/js/ui/main.js b/js/ui/main.js index 82172cf0f..056baab31 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -135,6 +135,7 @@ function start() { notificationDaemon = new NotificationDaemon.NotificationDaemon(); windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler(); telepathyClient = new TelepathyClient.Client(); + panel.startStatusArea(); _startDate = new Date(); diff --git a/js/ui/panel.js b/js/ui/panel.js index 690d992c7..fc4614dc5 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -12,6 +12,7 @@ const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; const Calendar = imports.ui.calendar; +const Config = imports.misc.config; const Overview = imports.ui.overview; const PopupMenu = imports.ui.popupMenu; const PanelMenu = imports.ui.panelMenu; @@ -36,6 +37,9 @@ const STANDARD_TRAY_ICON_SHELL_IMPLEMENTATION = { 'battery': imports.ui.status.power.Indicator }; +if (Config.HAVE_BLUETOOTH) + STANDARD_TRAY_ICON_SHELL_IMPLEMENTATION['bluetooth'] = imports.ui.status.bluetooth.Indicator; + const CLOCK_FORMAT_KEY = 'format'; const CLOCK_CUSTOM_FORMAT_KEY = 'custom-format'; const CLOCK_SHOW_DATE_KEY = 'show-date'; @@ -815,25 +819,9 @@ Panel.prototype = { this._rightBox.add(trayBox); this._rightBox.add(statusBox); - for (let i = 0; i < STANDARD_TRAY_ICON_ORDER.length; i++) { - let role = STANDARD_TRAY_ICON_ORDER[i]; - let constructor = STANDARD_TRAY_ICON_SHELL_IMPLEMENTATION[role]; - if (!constructor) { - // This icon is not implemented (this is a bug) - continue; - } - let indicator = new constructor(); - statusBox.add(indicator.actor); - this._menus.addMenu(indicator.menu); - } - Main.statusIconDispatcher.connect('status-icon-added', Lang.bind(this, this._onTrayIconAdded)); Main.statusIconDispatcher.connect('status-icon-removed', Lang.bind(this, this._onTrayIconRemoved)); - this._statusmenu = new StatusMenu.StatusMenuButton(); - this._menus.addMenu(this._statusmenu.menu); - this._rightBox.add(this._statusmenu.actor); - // TODO: decide what to do with the rest of the panel in the Overview mode (make it fade-out, become non-reactive, etc.) // We get into the Overview mode on button-press-event as opposed to button-release-event because eventually we'll probably // have the Overview act like a menu that allows the user to release the mouse on the activity the user wants @@ -859,6 +847,24 @@ Panel.prototype = { Main.chrome.addActor(this.actor, { visibleInOverview: true }); }, + startStatusArea: function() { + for (let i = 0; i < STANDARD_TRAY_ICON_ORDER.length; i++) { + let role = STANDARD_TRAY_ICON_ORDER[i]; + let constructor = STANDARD_TRAY_ICON_SHELL_IMPLEMENTATION[role]; + if (!constructor) { + // This icon is not implemented (this is a bug) + continue; + } + let indicator = new constructor(); + this._statusBox.add(indicator.actor); + this._menus.addMenu(indicator.menu); + } + + this._statusmenu = new StatusMenu.StatusMenuButton(); + this._menus.addMenu(this._statusmenu.menu); + this._rightBox.add(this._statusmenu.actor); + }, + hideCalendar: function() { this._clockButton.closeCalendar(); }, diff --git a/js/ui/status/bluetooth.js b/js/ui/status/bluetooth.js new file mode 100644 index 000000000..ead3b1f88 --- /dev/null +++ b/js/ui/status/bluetooth.js @@ -0,0 +1,446 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Clutter = imports.gi.Clutter; +const Gdk = imports.gi.Gdk; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const GnomeBluetoothApplet = imports.gi.GnomeBluetoothApplet; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const St = imports.gi.St; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; + +const Gettext = imports.gettext.domain('gnome-shell'); +const _ = Gettext.gettext; + +const ConnectionState = { + DISCONNECTED: 0, + CONNECTED: 1, + DISCONNECTING: 2, + CONNECTING: 3 +} + +function Indicator() { + this._init.apply(this, arguments); +} + +Indicator.prototype = { + __proto__: PanelMenu.SystemStatusButton.prototype, + + _init: function() { + PanelMenu.SystemStatusButton.prototype._init.call(this, 'bluetooth-disabled', null); + + GLib.spawn_command_line_sync ('pkill -f "^bluetooth-applet$"'); + this._applet = new GnomeBluetoothApplet.Applet(); + + this._killswitch = new PopupMenu.PopupSwitchMenuItem(_("Bluetooth"), false); + this._applet.connect('notify::killswitch-state', Lang.bind(this, this._updateKillswitch)); + this._killswitch.connect('toggled', Lang.bind(this, function() { + let current_state = this._applet.killswitch_state; + if (current_state != GnomeBluetoothApplet.KillswitchState.HARD_BLOCKED && + current_state != GnomeBluetoothApplet.KillswitchState.NO_ADAPTER) { + this._applet.killswitch_state = this._killswitch.state ? + GnomeBluetoothApplet.KillswitchState.UNBLOCKED: + GnomeBluetoothApplet.KillswitchState.SOFT_BLOCKED; + } else + this._killswitch.setToggleState(false); + })); + + this._discoverable = new PopupMenu.PopupSwitchMenuItem(_("Visibility"), this._applet.discoverable); + this._applet.connect('notify::discoverable', Lang.bind(this, function() { + this._discoverable.setToggleState(this._applet.discoverable); + })); + this._discoverable.connect('toggled', Lang.bind(this, function() { + this._applet.discoverable = this._discoverable.state; + })); + + this._updateKillswitch(); + this.menu.addMenuItem(this._killswitch); + this.menu.addMenuItem(this._discoverable); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + this._fullMenuItems = [new PopupMenu.PopupMenuItem(_("Send Files to Device...")), + new PopupMenu.PopupSeparatorMenuItem(), + new PopupMenu.PopupSeparatorMenuItem(), + new PopupMenu.PopupMenuItem(_("Setup a New Device..."))]; + this._deviceSep = this._fullMenuItems[1]; // hidden if no device exists + + this._fullMenuItems[0].connect('activate', function() { + GLib.spawn_command_line_async('bluetooth-sendto'); + }); + this._fullMenuItems[3].connect('activate', function() { + GLib.spawn_command_line_async('bluetooth-wizard'); + }); + + for (let i = 0; i < this._fullMenuItems.length; i++) { + let item = this._fullMenuItems[i]; + this.menu.addMenuItem(item); + } + + this._deviceItemPosition = 5; + this._deviceItems = []; + this._applet.connect('devices-changed', Lang.bind(this, this._updateDevices)); + this._updateDevices(); + + this._applet.connect('notify::show-full-menu', Lang.bind(this, this._updateFullMenu)); + this._updateFullMenu(); + + this.menu.addAction(_("Bluetooth Settings"), function() { + GLib.spawn_command_line_async('gnome-control-center bluetooth'); + }); + + this._applet.connect('pincode-request', Lang.bind(this, this._pinRequest)); + this._applet.connect('confirm-request', Lang.bind(this, this._confirmRequest)); + this._applet.connect('auth-request', Lang.bind(this, this._authRequest)); + this._applet.connect('cancel-request', Lang.bind(this, this._cancelRequest)); + }, + + _updateKillswitch: function() { + let current_state = this._applet.killswitch_state; + let on = current_state == GnomeBluetoothApplet.KillswitchState.UNBLOCKED; + let can_toggle = current_state != GnomeBluetoothApplet.KillswitchState.NO_ADAPTER && + current_state != GnomeBluetoothApplet.KillswitchState.HARD_BLOCKED; + + this._killswitch.setToggleState(on); + this._killswitch.actor.reactive = can_toggle; + + if (on) { + this._discoverable.actor.show(); + this.setIcon('bluetooth-active'); + } else { + this._discoverable.actor.hide(); + this.setIcon('bluetooth-disabled'); + } + }, + + _updateDevices: function() { + this._destroyAll(this._deviceItems); + this._deviceItems = []; + + let devices = this._applet.get_devices(); + if (devices.length == 0) + this._deviceSep.actor.hide(); + else + this._deviceSep.actor.show(); + for (let i = 0; i < devices.length; i++) { + let d = devices[i]; + let item = this._createDeviceItem(d); + if (item) { + this.menu.addMenuItem(item, this._deviceItemPosition + this._deviceItems.length); + this._deviceItems.push(item); + } + } + }, + + _createDeviceItem: function(device) { + if (!device.can_connect && device.capabilities == GnomeBluetoothApplet.Capabilities.NONE) + return null; + let item = new PopupMenu.PopupSubMenuMenuItem(device.alias); + item._device = device; + + if (device.can_connect) { + item._connected = device.connected; + let menuitem = new PopupMenu.PopupSwitchMenuItem(_("Connection"), device.connected); + + menuitem.connect('toggled', Lang.bind(this, function() { + if (item._connected > ConnectionState.CONNECTED) { + // operation already in progress, revert + menuitem.setToggleState(menuitem.state); + } + if (item._connected) { + item._connected = ConnectionState.DISCONNECTING; + this._applet.disconnect_device(item._device.device_path, function(applet, success) { + if (success) { // apply + item._connected = ConnectionState.DISCONNECTED; + menuitem.setToggleState(false); + } else { // revert + item._connected = ConnectionState.CONNECTED; + menuitem.setToggleState(true); + } + }); + } else { + item._connected = ConnectionState.CONNECTING; + this._applet.connect_device(item._device.device_path, function(applet, success) { + if (success) { // apply + item._connected = ConnectionState.CONNECTED; + menuitem.setToggleState(true); + } else { // revert + item._connected = ConnectionState.DISCONNECTED; + menuitem.setToggleState(false); + } + }); + } + })); + + item.menu.addMenuItem(menuitem); + } + + if (device.capabilities & GnomeBluetoothApplet.Capabilities.OBEX_PUSH) { + item.menu.addAction(_("Send Files..."), Lang.bind(this, function() { + this._applet.send_to_address(device.bdaddr, device.alias); + })); + } + if (device.capabilities & GnomeBluetoothApplet.Capabilities.OBEX_FILE_TRANSFER) { + item.menu.addAction(_("Browse Files..."), Lang.bind(this, function(event) { + this._applet.browse_address(device.bdaddr, event.get_time(), + Lang.bind(this, function(applet, result) { + try { + applet.browse_address_finish(result); + } catch (e) { + this._ensureSource(); + this._source.notify(new MessageTray.Notification(this._source, + _("Bluetooth"), + _("Error browsing device"), + { body: _("The requested device cannot be browsed, error is '%s'").format(e) })); + } + })); + })); + } + + switch (device.type) { + case GnomeBluetoothApplet.Type.KEYBOARD: + item.menu.addAction(_("Keyboard Settings"), function() { + GLib.spawn_command_line_async('gnome-control-center keyboard'); + }); + break; + case GnomeBluetoothApplet.Type.MOUSE: + item.menu.addAction(_("Mouse Settings"), function() { + GLib.spawn_command_line_async('gnome-control-center mouse'); + }); + break; + case GnomeBluetoothApplet.Type.HEADSET: + case GnomeBluetoothApplet.Type.HEADPHONES: + case GnomeBluetoothApplet.Type.OTHER_AUDIO: + item.menu.addAction(_("Sound Settings"), function() { + GLib.spawn_command_line_async('gnome-control-center sound'); + }); + break; + default: + break; + } + + return item; + }, + + _updateFullMenu: function() { + if (this._applet.show_full_menu) { + this._showAll(this._fullMenuItems); + this._showAll(this._deviceItems); + } else { + this._hideAll(this._fullMenuItems); + this._hideAll(this._deviceItems); + } + }, + + _showAll: function(items) { + for (let i = 0; i < items.length; i++) + items[i].actor.show(); + }, + + _hideAll: function(items) { + for (let i = 0; i < items.length; i++) + items[i].actor.hide(); + }, + + _destroyAll: function(items) { + for (let i = 0; i < items.length; i++) + items[i].destroy(); + }, + + _ensureSource: function() { + if (!this._source) { + this._source = new Source(); + Main.messageTray.add(this._source); + } + }, + + _authRequest: function(applet, device_path, name, long_name, uuid) { + this._ensureSource(); + this._source.notify(new AuthNotification(this._source, this._applet, device_path, name, long_name, uuid)); + }, + + _confirmRequest: function(applet, device_path, name, long_name, pin) { + this._ensureSource(); + this._source.notify(new ConfirmNotification(this._source, this._applet, device_path, name, long_name, pin)); + }, + + _pinRequest: function(applet, device_path, name, long_name, numeric) { + this._ensureSource(); + this._source.notify(new PinNotification(this._source, this._applet, device_path, name, long_name, numeric)); + }, + + _cancelRequest: function() { + this._source.destroy(); + } +} + +function Source() { + this._init.apply(this, arguments); +} + +Source.prototype = { + __proto__: MessageTray.Source.prototype, + + _init: function() { + MessageTray.Source.prototype._init.call(this, _("Bluetooth Agent")); + + this._setSummaryIcon(this.createNotificationIcon()); + }, + + notify: function(notification) { + this._private_destroyId = notification.connect('destroy', Lang.bind(this, function(notification) { + if (this.notification == notification) { + // the destroyed notification is the last for this source + this.notification.disconnect(this._private_destroyId); + this.destroy(); + } + })); + + MessageTray.Source.prototype.notify.call(this, notification); + }, + + createNotificationIcon: function() { + return new St.Icon({ icon_name: 'bluetooth-active', + icon_type: St.IconType.SYMBOLIC, + icon_size: this.ICON_SIZE }); + } +} + +function AuthNotification() { + this._init.apply(this, arguments); +} + +AuthNotification.prototype = { + __proto__: MessageTray.Notification.prototype, + + _init: function(source, applet, device_path, name, long_name, uuid) { + MessageTray.Notification.prototype._init.call(this, + source, + _("Bluetooth Agent"), + _("Authorization request from %s").format(name), + { customContent: true }); + this.setResident(true); + + this._applet = applet; + this._devicePath = device_path; + this.addBody(_("Device %s wants access to the service '%s'").format(long_name, uuid)); + + this.addButton('always-grant', _("Always grant access")); + this.addButton('grant', _("Grant this time only")); + this.addButton('reject', _("Reject")); + + this.connect('action-invoked', Lang.bind(this, function(self, action) { + switch (action) { + case 'always-grant': + this._applet.agent_reply_auth(this._devicePath, true, true); + break; + case 'grant': + this._applet.agent_reply_auth(this._devicePath, true, false); + break; + case 'reject': + default: + this._applet.agent_reply_auth(this._devicePath, false, false); + } + this.destroy(); + })); + } +} + +function ConfirmNotification() { + this._init.apply(this, arguments); +} + +ConfirmNotification.prototype = { + __proto__: MessageTray.Notification.prototype, + + _init: function(source, applet, device_path, name, long_name, pin) { + MessageTray.Notification.prototype._init.call(this, + source, + _("Bluetooth Agent"), + _("Pairing confirmation for %s").format(name), + { customContent: true }); + this.setResident(true); + + this._applet = applet; + this._devicePath = device_path; + this.addBody(_("Device %s wants to pair with this computer").format(long_name)); + this.addBody(_("Please confirm whether the PIN '%s' matches the one on the device.").format(pin)); + + this.addButton('matches', _("Matches")); + this.addButton('does-not-match', _("Does not match")); + + this.connect('action-invoked', Lang.bind(this, function(self, action) { + if (action == 'matches') + this._applet.agent_reply_confirm(this._devicePath, true); + else + this._applet.agent_reply_confirm(this._devicePath, false); + this.destroy(); + })); + } +} + +function PinNotification() { + this._init.apply(this, arguments); +} + +PinNotification.prototype = { + __proto__: MessageTray.Notification.prototype, + + _init: function(source, applet, device_path, name, long_name, numeric) { + MessageTray.Notification.prototype._init.call(this, + source, + _("Bluetooth Agent"), + _("Pairing request for %s").format(name), + { customContent: true }); + this.setResident(true); + + this._applet = applet; + this._devicePath = device_path; + this._numeric = numeric; + this.addBody(_("Device %s wants to pair with this computer").format(long_name)); + this.addBody(_("Please enter the PIN mentioned on the device.")); + + this._entry = new St.Entry(); + this._entry.connect('key-release-event', Lang.bind(this, function(entry, event) { + let key = event.get_key_symbol(); + if (key == Clutter.KEY_Return) { + this.emit('action-invoked', 'ok'); + return true; + } else if (key == Clutter.KEY_Escape) { + this.emit('action-invoked', 'cancel'); + return true; + } + return false; + })); + this.addActor(this._entry); + + this.addButton('ok', _("Ok")); + this.addButton('cancel', _("Cancel")); + + this.connect('action-invoked', Lang.bind(this, function(self, action) { + if (action == 'ok') { + if (this._numeric) + this._applet.agent_reply_passkey(this._devicePath, parseInt(this._entry.text)); + else + this._applet.agent_reply_pincode(this._devicePath, this._entry.text); + } else { + if (this._numeric) + this._applet.agent_reply_passkey(this._devicePath, -1); + else + this._applet.agent_reply_pincode(this._devicePath, null); + } + this.destroy(); + })); + }, + + grabFocus: function(lockTray) { + MessageTray.Notification.prototype.grabFocus.call(this, lockTray); + global.stage.set_key_focus(this._entry); + } +} diff --git a/src/Makefile.am b/src/Makefile.am index ee42b33b9..61b58260a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -218,10 +218,12 @@ libgnome_shell_la_LIBADD = \ -lm \ $(MUTTER_PLUGIN_LIBS) \ $(LIBGNOMEUI_LIBS) \ + $(BLUETOOTH_LIBS) \ libst-1.0.la \ libgdmuser-1.0.la \ libtray.la \ libgvc.la + libgnome_shell_la_CPPFLAGS = $(gnome_shell_cflags) typelibdir = $(pkglibdir) diff --git a/src/gnome-shell-plugin.c b/src/gnome-shell-plugin.c index 69e63a272..ec23bf317 100644 --- a/src/gnome-shell-plugin.c +++ b/src/gnome-shell-plugin.c @@ -357,6 +357,10 @@ gnome_shell_plugin_start (MetaPlugin *plugin) "GL buffer swap complete event received (with timestamp of completion)", "x"); +#if HAVE_BLUETOOTH + g_irepository_prepend_search_path (BLUETOOTH_DIR); +#endif + g_irepository_prepend_search_path (GNOME_SHELL_PKGLIBDIR); shell_js = g_getenv("GNOME_SHELL_JS"); diff --git a/tools/build/gnome-shell.modules b/tools/build/gnome-shell.modules index 109cc6ac2..94828aa98 100644 --- a/tools/build/gnome-shell.modules +++ b/tools/build/gnome-shell.modules @@ -230,6 +230,7 @@ + @@ -251,6 +252,15 @@ + + + + + + + + +