summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon McVittie <smcv@debian.org>2020-06-01 10:22:42 +0100
committerSimon McVittie <smcv@debian.org>2020-06-01 10:22:42 +0100
commitcbbadb6030fd13ef3a6c2c65eca2d9f1432f020b (patch)
tree8d1912520cf07dc672e3636050ea5ce0adbbbd15
parentdd20fa78315df4d2d9b28975edb6e097485a5f52 (diff)
parent4ebc314e354eb46cc84c566ec60a1b921902f16f (diff)
Update upstream source from tag 'upstream/13'
Update to upstream version '13' with Debian dir 2d356bc9a4a4c64b5c808d911a4fe9bcea175b1d
-rwxr-xr-x.extract.sh29
-rw-r--r--.gitignore1
-rw-r--r--README.md42
-rw-r--r--Settings.ui126
-rw-r--r--bluetooth.js121
-rw-r--r--convenience.js93
-rw-r--r--extension.js246
-rw-r--r--metadata.json5
-rw-r--r--prefs.js21
-rw-r--r--schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml12
-rw-r--r--settings.js64
-rw-r--r--ui.js164
-rw-r--r--utils.js68
13 files changed, 746 insertions, 246 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"
diff --git a/.gitignore b/.gitignore
index e666a92..2586ac4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/.idea
/schemas/gschemas.compiled
+/.gnome-shell
diff --git a/README.md b/README.md
index 5a6d8cb..12b32e5 100644
--- a/README.md
+++ b/README.md
@@ -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/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 dea519a..bc3f83f 100644
--- a/metadata.json
+++ b/metadata.json
@@ -6,9 +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.36"
]
}
diff --git a/prefs.js b/prefs.js
index 2a94e8d..62b4bec 100644
--- a/prefs.js
+++ b/prefs.js
@@ -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
diff --git a/ui.js b/ui.js
new file mode 100644
index 0000000..86deae4
--- /dev/null
+++ b/ui.js
@@ -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 = [];
+ };
+}