diff options
-rwxr-xr-x | .extract.sh | 29 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 42 | ||||
-rw-r--r-- | Settings.ui | 126 | ||||
-rw-r--r-- | bluetooth.js | 121 | ||||
-rw-r--r-- | convenience.js | 93 | ||||
-rw-r--r-- | debian/changelog | 12 | ||||
-rw-r--r-- | debian/control | 6 | ||||
-rw-r--r-- | debian/copyright | 33 | ||||
-rw-r--r-- | debian/install | 2 | ||||
-rw-r--r-- | debian/patches/metadata-Declare-compatibility-with-GNOME-Shell-3.34-3.36.patch | 24 | ||||
-rw-r--r-- | debian/patches/series | 1 | ||||
-rw-r--r-- | extension.js | 246 | ||||
-rw-r--r-- | metadata.json | 5 | ||||
-rw-r--r-- | prefs.js | 21 | ||||
-rw-r--r-- | schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml | 12 | ||||
-rw-r--r-- | settings.js | 64 | ||||
-rw-r--r-- | ui.js | 164 | ||||
-rw-r--r-- | utils.js | 68 |
19 files changed, 763 insertions, 307 deletions
diff --git a/.extract.sh b/.extract.sh new file mode 100755 index 0000000..a4578fe --- /dev/null +++ b/.extract.sh @@ -0,0 +1,29 @@ +#!/bin/bash +if [[ $# -ne 1 ]]; then + echo "usage $0 dir" >&2 + exit 1 +fi + +dir="$1" + +if [[ -e $dir ]]; then + echo "Error: $dir already exists" >&2 + exit 1 +fi + +mkdir -p "$dir" +cd "$dir" + +GS=/usr/lib/gnome-shell/libgnome-shell.so + +for r in $(gresource list $GS); do + t="${r/#\/org\/gnome\/shell\/}" + mkdir -p $(dirname $t) + echo Extracting $t + gresource extract $GS $r >$t +done + +echo +echo "Now add the following to /etc/environment and restart gnome-shell" +echo "if you want to run with these extracted source files." +echo "GNOME_SHELL_JS=$PWD" @@ -1,2 +1,3 @@ /.idea /schemas/gschemas.compiled +/.gnome-shell @@ -1,9 +1,13 @@ # Bluetooth quick connect This extension allows paired Bluetooth devices to be connected and -disconnected via the GNOME system menu, without having to enter the +disconnected via the GNOME system menu, without need to enter the Settings app every time. +# Requirements + + * bluez (on ubuntu: `sudo apt install bluez`) + ## Installation from extensions.gnome.org https://extensions.gnome.org/extension/1401/bluetooth-quick-connect/ @@ -17,3 +21,39 @@ make rm -r ~/.local/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com cp -r gnome-bluetooth-quick-connect ~/.local/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com ``` + +## Troubleshooting + +### Connecting and disconnecting does not work + +This extensions calls `bluetoothctl` under the hood. If something does not work +you can try to execute `bluetoothctl` command in terminal and see what is wrong. + +#### Paired devices +```bash +bluetoothctl -- paired-devices +``` + +#### Connecting +```bash +bluetoothctl -- connect <mac address> +``` + +#### Disconnecting +```bash +bluetoothctl -- disconnect <mac address> +``` + +#### Reconnecting +```bash +bluetoothctl -- disconnect <mac> && bluetoothctl -- connect <mac> +``` + +### Reconnecting does not work + +Not sure why, but sometimes bluetoothctl does not want to connect +device after it was disconnected. Reinstalling bluez and rebooting system helped on my ubuntu. +``` +$ sudo apt purge bluez gnome-bluetooth pulseaudio-module-bluetooth +$ sudo apt install bluez gnome-bluetooth pulseaudio-module-bluetooth +```
\ No newline at end of file diff --git a/Settings.ui b/Settings.ui index 047b045..d8acbe8 100644 --- a/Settings.ui +++ b/Settings.ui @@ -232,6 +232,132 @@ SPDX-License-Identifier: GPL-2.0-or-later </child> </object> </child> + <child> + <object class="GtkListBoxRow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">12</property> + <property name="margin_right">12</property> + <property name="margin_top">12</property> + <property name="margin_bottom">12</property> + <property name="column_spacing">32</property> + <child> + <object class="GtkSwitch" id="keep_menu_on_toggle"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="keep_menu_on_toggle_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Keep the menu open after toggling the connection (restart required)</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">12</property> + <property name="margin_right">12</property> + <property name="margin_top">12</property> + <property name="margin_bottom">12</property> + <property name="column_spacing">32</property> + <child> + <object class="GtkSwitch" id="refresh_button_on"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="refresh_button_on_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Show reconnect button (restart required)</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">12</property> + <property name="margin_right">12</property> + <property name="margin_top">12</property> + <property name="margin_bottom">12</property> + <property name="column_spacing">32</property> + <child> + <object class="GtkSwitch" id="debug_mode_on"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="debug_mode_on_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Debug mode (restart required)</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> </object> </child> <child type="label_item"> diff --git a/bluetooth.js b/bluetooth.js new file mode 100644 index 0000000..231578f --- /dev/null +++ b/bluetooth.js @@ -0,0 +1,121 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// 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, see <https://www.gnu.org/licenses/>. + +const GnomeBluetooth = imports.gi.GnomeBluetooth; +const Signals = imports.signals; +const GLib = imports.gi.GLib; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +var BluetoothController = class { + constructor() { + this._client = new GnomeBluetooth.Client(); + this._model = this._client.get_model(); + } + + enable() { + this._connectSignal(this._model, 'row-changed', (arg0, arg1, iter) => { + if (iter) { + let device = this._buildDevice(iter); + this.emit('device-changed', device); + } + + }); + this._connectSignal(this._model, 'row-deleted', () => { + this.emit('device-deleted'); + }); + this._connectSignal(this._model, 'row-inserted', (arg0, arg1, iter) => { + if (iter) { + let device = this._buildDevice(iter); + this.emit('device-inserted', device); + } + }); + } + + getDevices() { + let adapter = this._getDefaultAdapter(); + if (!adapter) + return []; + + let devices = []; + + let [ret, iter] = this._model.iter_children(adapter); + while (ret) { + let device = this._buildDevice(iter); + devices.push(device); + ret = this._model.iter_next(iter); + } + + return devices; + } + + getConnectedDevices() { + return this.getDevices().filter((device) => { + return device.isConnected; + }); + } + + destroy() { + this._disconnectSignals(); + } + + _getDefaultAdapter() { + let [ret, iter] = this._model.get_iter_first(); + while (ret) { + let isDefault = this._model.get_value(iter, GnomeBluetooth.Column.DEFAULT); + let isPowered = this._model.get_value(iter, GnomeBluetooth.Column.POWERED); + if (isDefault && isPowered) + return iter; + ret = this._model.iter_next(iter); + } + return null; + } + + _buildDevice(iter) { + return new BluetoothDevice(this._model, iter); + } +} + +Signals.addSignalMethods(BluetoothController.prototype); +Utils.addSignalsHelperMethods(BluetoothController.prototype); + +var BluetoothDevice = class { + constructor(model, iter) { + this._model = model; + this.update(iter); + } + + update(iter) { + this.name = this._model.get_value(iter, GnomeBluetooth.Column.NAME); + this.isConnected = this._model.get_value(iter, GnomeBluetooth.Column.CONNECTED); + this.isPaired = this._model.get_value(iter, GnomeBluetooth.Column.PAIRED); + this.mac = this._model.get_value(iter, GnomeBluetooth.Column.ADDRESS); + this.isDefault = this._model.get_value(iter, GnomeBluetooth.Column.DEFAULT); + } + + disconnect(callback) { + Utils.spawn(`bluetoothctl -- disconnect ${this.mac}`, callback) + } + + connect(callback) { + Utils.spawn(`bluetoothctl -- connect ${this.mac}`, callback) + } + + reconnect(callback) { + Utils.spawn(`bluetoothctl -- disconnect ${this.mac} && bluetoothctl -- connect ${this.mac}`, callback) + } +} diff --git a/convenience.js b/convenience.js deleted file mode 100644 index 1c866b2..0000000 --- a/convenience.js +++ /dev/null @@ -1,93 +0,0 @@ -/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ -/* - Copyright (c) 2011-2012, Giovanni Campagna <scampa.giovanni@gmail.com> - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the GNOME nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -const Gettext = imports.gettext; -const Gio = imports.gi.Gio; - -const Config = imports.misc.config; -const ExtensionUtils = imports.misc.extensionUtils; - -/** - * initTranslations: - * @domain: (optional): the gettext domain to use - * - * Initialize Gettext to load translations from extensionsdir/locale. - * If @domain is not provided, it will be taken from metadata['gettext-domain'] - */ -function initTranslations(domain) { - let extension = ExtensionUtils.getCurrentExtension(); - - domain = domain || extension.metadata['gettext-domain']; - - // check if this extension was built with "make zip-file", and thus - // has the locale files in a subfolder - // otherwise assume that extension has been installed in the - // same prefix as gnome-shell - let localeDir = extension.dir.get_child('locale'); - if (localeDir.query_exists(null)) - Gettext.bindtextdomain(domain, localeDir.get_path()); - else - Gettext.bindtextdomain(domain, Config.LOCALEDIR); -} - -/** - * getSettings: - * @schema: (optional): the GSettings schema id - * - * Builds and return a GSettings schema for @schema, using schema files - * in extensionsdir/schemas. If @schema is not provided, it is taken from - * metadata['settings-schema']. - */ -function getSettings(schema) { - let extension = ExtensionUtils.getCurrentExtension(); - - schema = schema || extension.metadata['settings-schema']; - - const GioSSS = Gio.SettingsSchemaSource; - - // check if this extension was built with "make zip-file", and thus - // has the schema files in a subfolder - // otherwise assume that extension has been installed in the - // same prefix as gnome-shell (and therefore schemas are available - // in the standard folders) - let schemaDir = extension.dir.get_child('schemas'); - let schemaSource; - - if (schemaDir.query_exists(null)) - schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), - GioSSS.get_default(), - false); - else - schemaSource = GioSSS.get_default(); - - let schemaObj = schemaSource.lookup(schema, true); - if (!schemaObj) - throw new Error('Schema ' + schema + ' could not be found for extension ' - + extension.metadata.uuid + '. Please check your installation.'); - - return new Gio.Settings({ settings_schema: schemaObj }); -} diff --git a/debian/changelog b/debian/changelog index 335952e..6d2a65b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +gnome-shell-extension-bluetooth-quick-connect (13-1) unstable; urgency=medium + + * New upstream release + - d/copyright: Update + - d/control: Adjust dependencies. + This version drops compatibility with GNOME Shell older than 3.36. + - Drop patch to declare compatibility with GNOME 3.36, no longer required + - Install schemas to expected location. + The new version is more specific about where to find them. + + -- Simon McVittie <smcv@debian.org> Mon, 01 Jun 2020 11:55:44 +0100 + gnome-shell-extension-bluetooth-quick-connect (10-3) unstable; urgency=medium * Bump maximum GNOME Shell version to 3.36 diff --git a/debian/control b/debian/control index 164f315..b3f7123 100644 --- a/debian/control +++ b/debian/control @@ -17,14 +17,14 @@ Package: gnome-shell-extension-bluetooth-quick-connect Architecture: all Depends: gnome-shell (<< 3.37), - gnome-shell (>= 3.28), + gnome-shell (>= 3.36), ${misc:Depends}, Recommends: - gnome-tweaks, + gnome-shell-extension-prefs, Description: GNOME Shell extension to connect paired Bluetooth devices This GNOME Shell extension adds entries to the shell's System menu to provide a quick way to connect and disconnect Bluetooth devices that were previously paired with the computer. . Please note that each user will need to enable the extension manually, for - example using the gnome-tweaks application. + example using the gnome-shell-extension-prefs application. diff --git a/debian/copyright b/debian/copyright index c92cff3..bbdc14d 100644 --- a/debian/copyright +++ b/debian/copyright @@ -4,19 +4,13 @@ Source: https://github.com/bjarosze/gnome-bluetooth-quick-connect Files: * Copyright: - © 2018 Bartosz Jaroszewski + © 2018-2020 Bartosz Jaroszewski License: GPL-2+ -Files: - convenience.js -Copyright: - © 2011-2012 Giovanni Campagna -License: BSD-3-clause - Files: debian/* Copyright: © 2014-2015 Tobias Frost - © 2016-2019 Simon McVittie + © 2016-2020 Simon McVittie License: GPL-2+ License: GPL-2+ @@ -32,26 +26,3 @@ License: GPL-2+ Comment: On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". - -License: BSD-3-clause - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the GNOME nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - . - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/install b/debian/install index 4f2d4c2..40f17fd 100644 --- a/debian/install +++ b/debian/install @@ -1,4 +1,4 @@ *.js usr/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com *.json usr/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com *.ui usr/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com -schemas/*.xml usr/share/glib-2.0/schemas +schemas/* usr/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com/schemas diff --git a/debian/patches/metadata-Declare-compatibility-with-GNOME-Shell-3.34-3.36.patch b/debian/patches/metadata-Declare-compatibility-with-GNOME-Shell-3.34-3.36.patch deleted file mode 100644 index 73dfb4a..0000000 --- a/debian/patches/metadata-Declare-compatibility-with-GNOME-Shell-3.34-3.36.patch +++ /dev/null @@ -1,24 +0,0 @@ -From: Simon McVittie <smcv@debian.org> -Date: Tue, 24 Mar 2020 13:10:00 +0000 -Subject: metadata: Declare compatibility with GNOME Shell 3.34, 3.36 - -Signed-off-by: Simon McVittie <smcv@debian.org> -Bug: https://github.com/bjarosze/gnome-bluetooth-quick-connect/issues/15 ---- - metadata.json | 4 +++- - 1 file changed, 3 insertions(+), 1 deletion(-) - -diff --git a/metadata.json b/metadata.json -index dea519a..1f56b90 100644 ---- a/metadata.json -+++ b/metadata.json -@@ -9,6 +9,8 @@ - "3.26.2", - "3.28", - "3.30", -- "3.32" -+ "3.32", -+ "3.34", -+ "3.36" - ] - } diff --git a/debian/patches/series b/debian/patches/series deleted file mode 100644 index 5cadb1d..0000000 --- a/debian/patches/series +++ /dev/null @@ -1 +0,0 @@ -metadata-Declare-compatibility-with-GNOME-Shell-3.34-3.36.patch diff --git a/extension.js b/extension.js index 403d956..b08d77b 100644 --- a/extension.js +++ b/extension.js @@ -15,219 +15,181 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. const Main = imports.ui.main; -const GnomeBluetooth = imports.gi.GnomeBluetooth; -const PopupMenu = imports.ui.popupMenu; -const Util = imports.misc.util; const GLib = imports.gi.GLib; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const Convenience = Me.imports.convenience; - -class BluetoothDevice { - constructor(model, device) { - this._name = model.get_value(device, GnomeBluetooth.Column.NAME); - this._isConnected = model.get_value(device, GnomeBluetooth.Column.CONNECTED); - this._isPaired = model.get_value(device, GnomeBluetooth.Column.PAIRED); - this._mac = model.get_value(device, GnomeBluetooth.Column.ADDRESS); - } - - get name() { - return this._name; - } - - get isConnected() { - return this._isConnected; - } - - get isPaired() { - return this._isPaired; - } - - get mac() { - return this._mac; - } - - get item() { - if (!this._item) - this._buildMenuItem(); - - return this._item; - } - - _buildMenuItem() { - this._item = new PopupMenu.PopupSwitchMenuItem(this.name, this.isConnected); - this._item.isDeviceSwitcher = true; - this._item.connect('toggled', (item, state) => { - if (state) - this._connect(); - else - this._disconnect(); - }); - } - - _disconnect() { - this._call_bluetoothctl(`disconnect ${this.mac}`) - } - - _connect() { - this._call_bluetoothctl(`connect ${this.mac}`) - } +const UiExtension = Me.imports.ui; +const Bluetooth = Me.imports.bluetooth; +const Utils = Me.imports.utils; +const Settings = Me.imports.settings.Settings; - _call_bluetoothctl(command) { - let btctl_command = `echo -e "${command}\\n" | bluetoothctl`; - Util.spawn(['/usr/bin/env', 'bash', '-c', btctl_command]); - } -} class BluetoothQuickConnect { constructor(bluetooth, settings) { + this._logger = new Utils.Logger(settings); + this._logger.info('Initializing extension'); this._menu = bluetooth._item.menu; this._proxy = bluetooth._proxy; - this._settings = settings; + this._controller = new Bluetooth.BluetoothController(); + this._settings = settings - this._signals = []; + this._items = {}; } enable() { - this._loadBluetoothModel(); + this._logger.info('Enabling extension'); + this._controller.enable(); + this._refresh(); + this._connectControllerSignals(); + this._connectIdleMonitor(); + this._connectMenuSignals(); + } + + _connectMenuSignals() { this._connectSignal(this._menu, 'open-state-changed', (menu, isOpen) => { - if (isOpen && this._autoPowerOnEnabled()) + this._logger.info(`Menu toggled: ${isOpen}`); + if (isOpen) + this._disconnectIdleMonitor() + else + this._connectIdleMonitor(); + + if (isOpen && this._settings.isAutoPowerOnEnabled() && this._proxy.BluetoothAirplaneMode) { + this._logger.info('Disabling airplane mode'); this._proxy.BluetoothAirplaneMode = false; + } }); - - this._connectSignal(this._model, 'row-changed', () => this._sync()); - this._connectSignal(this._model, 'row-deleted', () => this._sync()); - this._connectSignal(this._model, 'row-inserted', () => this._sync()); - - this._idleMonitor(); - if (!this._proxy.BluetoothAirplaneMode) { - this._sync(); - } } disable() { + this._logger.info('Disabling extension'); this._destroy(); } test() { try { + this._logger.info('Testing bluetoothctl'); GLib.spawn_command_line_sync("bluetoothctl --version"); - } catch(error) { - Main.notifyError(`Bluetooth quick connect: error trying to execute "bluetoothctl"`); + this._logger.info('Test succeeded'); + } catch (error) { + Main.notifyError(_('Bluetooth quick connect'), _(`Error trying to execute "bluetoothctl"`)); + this._logger.info('Test failed'); } } - _idleMonitor() { - this._idleMonitorId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._autoPowerOffCheckingInterval() * 1000, () => { - if (this._autoPowerOffEnabled() && this._getConnectedDevices().length === 0) - this._proxy.BluetoothAirplaneMode = true; + _connectControllerSignals() { + this._logger.info('Connecting bluetooth controller signals'); - return true; + this._connectSignal(this._controller, 'device-inserted', (ctrl, device) => { + this._logger.info(`Device inserted event: ${device.name}`); + this._addMenuItem(device); + }); + this._connectSignal(this._controller, 'device-changed', (ctrl, device) => { + this._logger.info(`Device changed event: ${device.name}`); + if (device.isDefault) + this._refresh(); + else + this._syncMenuItem(device); + }); + this._connectSignal(this._controller, 'device-deleted', () => { + this._logger.info(`Device deleted event`); + this._refresh(); }); - } - _connectSignal(subject, signal_name, method) { - let signal_id = subject.connect(signal_name, method); - this._signals.push({ - subject: subject, - signal_id: signal_id + this._connectSignal(Main.sessionMode, 'updated', () => { + this._refresh() }); } - _loadBluetoothModel() { - this._client = new GnomeBluetooth.Client(); - this._model = this._client.get_model(); + _syncMenuItem(device) { + this._logger.info(`Synchronizing device menu item: ${device.name}`); + let item = this._items[device.mac] || this._addMenuItem(device); + item.sync(device); } - _getDefaultAdapter() { - let [ret, iter] = this._model.get_iter_first(); - while (ret) { - let isDefault = this._model.get_value(iter, GnomeBluetooth.Column.DEFAULT); - let isPowered = this._model.get_value(iter, GnomeBluetooth.Column.POWERED); - if (isDefault && isPowered) - return iter; - ret = this._model.iter_next(iter); - } - return null; + _addMenuItem(device) { + this._logger.info(`Adding device menu item: ${device.name}`); + let menuItem = new UiExtension.PopupBluetoothDeviceMenuItem( + device, + { + showRefreshButton: this._settings.isShowRefreshButtonEnabled(), + closeMenuOnAction: !this._settings.isKeepMenuOnToggleEnabled() + } + ); + this._items[device.mac] = menuItem; + this._menu.addMenuItem(menuItem, 1); + + return menuItem; } - _getDevices() { - let adapter = this._getDefaultAdapter(); - if (!adapter) - return []; + _connectIdleMonitor() { + if (this._idleMonitorId) return; - let devices = []; + this._logger.info('Connecting idle monitor'); - let [ret, iter] = this._model.iter_children(adapter); - while (ret) { - devices.push(new BluetoothDevice(this._model, iter)); - ret = this._model.iter_next(iter); - } + this._idleMonitorId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._settings.autoPowerOffCheckingInterval() * 1000, () => { + if (this._settings.isAutoPowerOffEnabled() && this._controller.getConnectedDevices().length === 0) + this._proxy.BluetoothAirplaneMode = true; - return devices; + return true; + }); } - _getPairedDevices() { - return this._getDevices().filter((device) => { - return device.isPaired || device.isConnected; - }); + _disconnectIdleMonitor() { + if (!this._idleMonitorId) return; + + this._logger.info('Disconnecting idle monitor'); + + GLib.Source.remove(this._idleMonitorId); + this._idleMonitorId = null; } - _getConnectedDevices() { - return this._getDevices().filter((device) => { - return device.isConnected; + _connectSignal(subject, signal_name, method) { + let signal_id = subject.connect(signal_name, method); + this._signals.push({ + subject: subject, + signal_id: signal_id }); } - _sync() { + _refresh() { this._removeDevicesFromMenu(); this._addDevicesToMenu(); + + this._logger.info('Refreshing devices list'); } _addDevicesToMenu() { - this._getPairedDevices().forEach((device) => { - this._menu.addMenuItem(device.item, 1); + this._controller.getDevices().forEach((device) => { + this._addMenuItem(device); }); } _removeDevicesFromMenu() { - this._menu._getMenuItems().forEach((item) => { - if (item.isDeviceSwitcher) { - item.destroy(); - } + Object.values(this._items).forEach((item) => { + item.destroy(); }); + + this._items = {}; } _destroy() { - this._signals.forEach((signal) => { - signal.subject.disconnect(signal.signal_id); - }); - this._signals = []; + this._disconnectSignals(); this._removeDevicesFromMenu(); - - if (this._idleMonitorId) - GLib.Source.remove(this._idleMonitorId); - } - - _autoPowerOnEnabled() { - return this._settings.get_boolean('bluetooth-auto-power-on'); + this._disconnectIdleMonitor(); + if (this._controller) + this._controller.destroy(); } +} - _autoPowerOffEnabled() { - return this._settings.get_boolean('bluetooth-auto-power-off'); - } +Utils.addSignalsHelperMethods(BluetoothQuickConnect.prototype); - _autoPowerOffCheckingInterval() { - return this._settings.get_int('bluetooth-auto-power-off-interval'); - } -} let bluetoothQuickConnect = null; function init() { let bluetooth = Main.panel.statusArea.aggregateMenu._bluetooth; - let settings = Convenience.getSettings(); + let settings = new Settings(); bluetoothQuickConnect = new BluetoothQuickConnect(bluetooth, settings); } diff --git a/metadata.json b/metadata.json index 1f56b90..bc3f83f 100644 --- a/metadata.json +++ b/metadata.json @@ -6,11 +6,6 @@ "settings-schema": "org.gnome.shell.extensions.bluetooth-quick-connect", "gettext-domain": "bluetooth-quick-connect", "shell-version": [ - "3.26.2", - "3.28", - "3.30", - "3.32", - "3.34", "3.36" ] } @@ -10,13 +10,12 @@ const _ = Gettext.gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const Convenience = Me.imports.convenience; +const Settings = Me.imports.settings.Settings; class SettingsBuilder { - constructor() { - this._settings = new Convenience.getSettings(); + this._settings = new Settings().settings; this._builder = new Gtk.Builder(); } @@ -32,10 +31,12 @@ class SettingsBuilder { this._builder.get_object('auto_power_off_settings_button').connect('clicked', () => { - let dialog = new Gtk.Dialog({ title: 'Auto power off settings', + let dialog = new Gtk.Dialog({ + title: 'Auto power off settings', transient_for: this._widget.get_toplevel(), use_header_bar: true, - modal: true }); + modal: true + }); let box = this._builder.get_object('auto_power_off_settings'); @@ -64,12 +65,20 @@ class SettingsBuilder { let autoPowerOffInterval = this._builder.get_object('auto_power_off_interval'); this._settings.bind('bluetooth-auto-power-off-interval', autoPowerOffInterval, 'value', Gio.SettingsBindFlags.DEFAULT); + + let keepMenuOnToggleSwitch = this._builder.get_object('keep_menu_on_toggle'); + this._settings.bind('keep-menu-on-toggle', keepMenuOnToggleSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + + let refreshButtonOnSwitch = this._builder.get_object('refresh_button_on'); + this._settings.bind('refresh-button-on', refreshButtonOnSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + + let debugModeOnSwitch = this._builder.get_object('debug_mode_on'); + this._settings.bind('debug-mode-on', debugModeOnSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); } } function init() { - // Convenience.initTranslations(); } function buildPrefsWidget() { diff --git a/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml b/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml index 684af11..ea5f674 100644 --- a/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml +++ b/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml @@ -18,5 +18,17 @@ SPDX-License-Identifier: GPL-2.0-or-later <default>60</default> <summary>Checking interval for power off idling bluetooth</summary> </key> + <key name="keep-menu-on-toggle" type="b"> + <default>false</default> + <summary>Keep the menu open after toggling the connection</summary> + </key> + <key name="refresh-button-on" type="b"> + <default>false</default> + <summary>Show reconnect button</summary> + </key> + <key name="debug-mode-on" type="b"> + <default>false</default> + <summary>Debug mode</summary> + </key> </schema> </schemalist> diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..e7040df --- /dev/null +++ b/settings.js @@ -0,0 +1,64 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// 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, see <https://www.gnu.org/licenses/>. + +const ExtensionUtils = imports.misc.extensionUtils; +const Gio = imports.gi.Gio; +const GioSSS = Gio.SettingsSchemaSource; + +class Settings { + constructor() { + this.settings = this._loadSettings(); + } + + isAutoPowerOnEnabled() { + return this.settings.get_boolean('bluetooth-auto-power-on'); + } + + isAutoPowerOffEnabled() { + return this.settings.get_boolean('bluetooth-auto-power-off'); + } + + autoPowerOffCheckingInterval() { + return this.settings.get_int('bluetooth-auto-power-off-interval'); + } + + isKeepMenuOnToggleEnabled() { + return this.settings.get_boolean('keep-menu-on-toggle'); + } + + isShowRefreshButtonEnabled() { + return this.settings.get_boolean('refresh-button-on'); + } + + isDebugModeEnabled() { + return this.settings.get_boolean('debug-mode-on'); + } + + _loadSettings() { + let extension = ExtensionUtils.getCurrentExtension(); + let schema = extension.metadata['settings-schema']; + + let schemaSource = GioSSS.new_from_directory( + extension.dir.get_child('schemas').get_path(), + GioSSS.get_default(), + false + ); + + let schemaObj = schemaSource.lookup(schema, true); + + return new Gio.Settings({settings_schema: schemaObj}); + } +}
\ No newline at end of file @@ -0,0 +1,164 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// 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, see <https://www.gnu.org/licenses/>. + + +const Clutter = imports.gi.Clutter; +const GObject = imports.gi.GObject; +const St = imports.gi.St; +const Tweener = imports.ui.tweener; +const PopupMenu = imports.ui.popupMenu; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +var PopupBluetoothDeviceMenuItem = GObject.registerClass( + class PopupSwitchWithButtonMenuItem extends PopupMenu.PopupSwitchMenuItem { + _init(device, params) { + super._init(device.name, device.isConnected, {}); + + this._device = device; + this._showRefreshButton = params.showRefreshButton; + this._closeMenuOnAction = params.closeMenuOnAction; + + this.label.x_expand = true; + this._statusBin.x_expand = false; + + this._refreshButton = this._buildRefreshButton(); + this._pendingLabel = this._buildPendingLabel(); + this._connectToggledEvent(); + + this.insert_child_at_index(this._refreshButton, this.get_n_children() - 1); + this.add_child(this._pendingLabel); + + this.sync(device); + } + + sync(device) { + this._device = device; + this._switch.state = device.isConnected; + this.visible = device.isPaired; + if (this._showRefreshButton && device.isConnected) + this._refreshButton.show(); + else + this._refreshButton.hide(); + } + + _buildRefreshButton() { + let icon = new St.Icon({ + icon_name: 'view-refresh', + style_class: 'popup-menu-icon', + opacity: 155 + }); + + let button = new St.Button({ + child: icon, + x_align: Clutter.ActorAlign.END + }); + + button.connect("enter-event", (widget) => { + Tweener.addTween( + widget.child, { + opacity: 255, + time: 0.05, + transition: 'linear' + } + ); + }); + + button.connect("leave-event", (widget) => { + Tweener.addTween( + widget.child, { + opacity: 155, + time: 0.05, + transition: 'linear' + } + ); + }); + + button.connect('clicked', () => { + this._enablePending(); + this._device.reconnect(() => { + this._disablePending() + }); + + if (this._closeMenuOnAction) + this.emit('activate', Clutter.get_current_event()); + }); + + return button; + } + + _buildPendingLabel() { + let label = new St.Label({text: _('Wait')}); + label.hide(); + + return label; + } + + _connectToggledEvent() { + this.connect('toggled', (item, state) => { + if (state) + this._device.connect(() => { + this._disablePending() + }); + else + this._device.disconnect(() => { + this._disablePending() + }); + }); + } + + activate(event) { + if (this._switch.mapped) { + this.toggle(); + this._switch.toggle(); // toggle back, state will be updated by signal + } + + // we allow pressing space to toggle the switch + // without closing the menu + if (event.type() == Clutter.EventType.KEY_PRESS && + event.get_key_symbol() == Clutter.KEY_space) + return; + + if (this._closeMenuOnAction) + this.emit('activate', event); + } + + toggle() { + super.toggle(); + this._enablePending(); + } + + hideRefreshButton() { + this._refreshButton.hide(); + } + + _enablePending() { + this._refreshButton.reactive = false; + this._switch.hide(); + this._pendingLabel.show(); + this.reactive = false; + } + + _disablePending() { + this._refreshButton.reactive = true; + this._switch.show(); + this._pendingLabel.hide(); + this.reactive = true; + } + } +); diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..f6676e7 --- /dev/null +++ b/utils.js @@ -0,0 +1,68 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// 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, see <https://www.gnu.org/licenses/>. + +const GLib = imports.gi.GLib; + +function spawn(command, callback) { + let [status, pid] = GLib.spawn_async( + null, + ['/usr/bin/env', 'bash', '-c', command], + null, + GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, + null + ); + + if (callback) + GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, callback); +} + + +function isDebugModeEnabled() { + return new Settings().isDebugModeEnabled(); +} + +class Logger { + constructor(settings) { + this._enabled = settings.isDebugModeEnabled(); + } + + info(message) { + if (!this._enabled) return; + + global.log(`[bluetooth-quick-connect] ${message}`); + } +} + +function addSignalsHelperMethods(prototype) { + prototype._connectSignal = function (subject, signal_name, method) { + if (!this._signals) this._signals = []; + + let signal_id = subject.connect(signal_name, method); + this._signals.push({ + subject: subject, + signal_id: signal_id + }); + } + + prototype._disconnectSignals = function () { + if (!this._signals) return; + + this._signals.forEach((signal) => { + signal.subject.disconnect(signal.signal_id); + }); + this._signals = []; + }; +} |