summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md35
-rw-r--r--Settings.ui47
-rw-r--r--bluetooth.js121
-rw-r--r--convenience.js93
-rw-r--r--extension.js240
-rw-r--r--prefs.js15
-rw-r--r--schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml4
-rw-r--r--settings.js64
-rw-r--r--ui.js70
-rw-r--r--utils.js68
10 files changed, 484 insertions, 273 deletions
diff --git a/README.md b/README.md
index ed4289c..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/
@@ -20,10 +24,35 @@ cp -r gnome-bluetooth-quick-connect ~/.local/share/gnome-shell/extensions/blueto
## Troubleshooting
-### Reconnect does not work
+### 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 helped on my ubuntu.
+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
diff --git a/Settings.ui b/Settings.ui
index 06d7424..d8acbe8 100644
--- a/Settings.ui
+++ b/Settings.ui
@@ -262,7 +262,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<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</property>
+ <property name="label" translatable="yes">Keep the menu open after toggling the connection (restart required)</property>
<property name="xalign">0</property>
</object>
<packing>
@@ -304,7 +304,49 @@ SPDX-License-Identifier: GPL-2.0-or-later
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
- <property name="label" translatable="yes">Show reconnect button</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>
@@ -316,7 +358,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
</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 cff5c84..b08d77b 100644
--- a/extension.js
+++ b/extension.js
@@ -15,113 +15,120 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Main = imports.ui.main;
-const GnomeBluetooth = imports.gi.GnomeBluetooth;
-const Util = imports.misc.util;
const GLib = imports.gi.GLib;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
-const Convenience = Me.imports.convenience;
const UiExtension = Me.imports.ui;
+const Bluetooth = Me.imports.bluetooth;
+const Utils = Me.imports.utils;
+const Settings = Me.imports.settings.Settings;
-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._item = new UiExtension.PopupBluetoothDeviceMenuItem(this);
-
- return this._item;
- }
-
- disconnect() {
- this._call_cmd(`bluetoothctl -- disconnect ${this.mac}`)
- }
-
- connect() {
- this._call_cmd(`bluetoothctl -- connect ${this.mac}`)
- }
-
- reconnect() {
- this._call_cmd(`bluetoothctl -- disconnect ${this.mac} && bluetoothctl -- connect ${this.mac}`)
- }
-
- _call_cmd(command) {
- Util.spawn(['/usr/bin/env', 'bash', '-c', 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) => {
+ this._logger.info(`Menu toggled: ${isOpen}`);
if (isOpen)
this._disconnectIdleMonitor()
else
this._connectIdleMonitor();
- if (isOpen && this._autoPowerOnEnabled())
+ if (isOpen && this._settings.isAutoPowerOnEnabled() && this._proxy.BluetoothAirplaneMode) {
+ this._logger.info('Disabling airplane mode');
this._proxy.BluetoothAirplaneMode = false;
-
- this._sync();
+ }
});
-
- 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._connectIdleMonitor();
- 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");
+ this._logger.info('Test succeeded');
} catch (error) {
- Main.notifyError(_('Bluetooth quick connect'), _(`Error trying to execute "bluetoothctl"`));
+ Main.notifyError(_('Bluetooth quick connect'), _(`Error trying to execute "bluetoothctl"`));
+ this._logger.info('Test failed');
}
}
+ _connectControllerSignals() {
+ this._logger.info('Connecting bluetooth controller signals');
+
+ 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();
+ });
+
+ this._connectSignal(Main.sessionMode, 'updated', () => {
+ this._refresh()
+ });
+ }
+
+ _syncMenuItem(device) {
+ this._logger.info(`Synchronizing device menu item: ${device.name}`);
+ let item = this._items[device.mac] || this._addMenuItem(device);
+ item.sync(device);
+ }
+
+ _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;
+ }
+
_connectIdleMonitor() {
if (this._idleMonitorId) return;
- this._idleMonitorId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._autoPowerOffCheckingInterval() * 1000, () => {
- if (this._autoPowerOffEnabled() && this._getConnectedDevices().length === 0)
+ this._logger.info('Connecting idle monitor');
+
+ 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 true;
@@ -131,6 +138,8 @@ class BluetoothQuickConnect {
_disconnectIdleMonitor() {
if (!this._idleMonitorId) return;
+ this._logger.info('Disconnecting idle monitor');
+
GLib.Source.remove(this._idleMonitorId);
this._idleMonitorId = null;
}
@@ -143,109 +152,44 @@ class BluetoothQuickConnect {
});
}
- _loadBluetoothModel() {
- this._client = new GnomeBluetooth.Client();
- this._model = this._client.get_model();
- }
-
- _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;
- }
-
- _getDevices() {
- let adapter = this._getDefaultAdapter();
- if (!adapter)
- return [];
-
- let devices = [];
-
- let [ret, iter] = this._model.iter_children(adapter);
- while (ret) {
- devices.push(new BluetoothDevice(this._model, iter));
- ret = this._model.iter_next(iter);
- }
-
- return devices;
- }
-
- _getPairedDevices() {
- return this._getDevices().filter((device) => {
- return device.isPaired || device.isConnected;
- });
- }
-
- _getConnectedDevices() {
- return this._getDevices().filter((device) => {
- return device.isConnected;
- });
- }
-
- _sync() {
+ _refresh() {
this._removeDevicesFromMenu();
this._addDevicesToMenu();
+
+ this._logger.info('Refreshing devices list');
}
_addDevicesToMenu() {
- this._getPairedDevices().forEach((device) => {
- device.item.isEmitActivatedEnabled = !this._keepMenuOnToggle();
- if (!this._showRefreshButton())
- device.item.hideRefreshButton();
-
- this._menu.addMenuItem(device.item, 1);
+ this._controller.getDevices().forEach((device) => {
+ this._addMenuItem(device);
});
}
_removeDevicesFromMenu() {
- this._menu._getMenuItems().forEach((item) => {
- if (item.isBluetoothDeviceSwitcher) {
- 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();
this._disconnectIdleMonitor();
+ if (this._controller)
+ this._controller.destroy();
}
+}
- _autoPowerOnEnabled() {
- return this._settings.get_boolean('bluetooth-auto-power-on');
- }
-
- _autoPowerOffEnabled() {
- return this._settings.get_boolean('bluetooth-auto-power-off');
- }
-
- _autoPowerOffCheckingInterval() {
- return this._settings.get_int('bluetooth-auto-power-off-interval');
- }
-
- _keepMenuOnToggle() {
- return this._settings.get_boolean('keep-menu-on-toggle');
- }
+Utils.addSignalsHelperMethods(BluetoothQuickConnect.prototype);
- _showRefreshButton() {
- return this._settings.get_boolean('refresh-button-on');
- }
-}
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/prefs.js b/prefs.js
index 235a1b9..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');
@@ -70,12 +71,14 @@ class SettingsBuilder {
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 1a62336..ea5f674 100644
--- a/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml
+++ b/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml
@@ -26,5 +26,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<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
index b18f3df..86deae4 100644
--- a/ui.js
+++ b/ui.js
@@ -15,18 +15,24 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-const {Atk, Clutter, Gio, GObject, Graphene, Shell, St} = imports.gi;
+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(bluetoothDevice, params) {
- super._init(bluetoothDevice.name, bluetoothDevice.isConnected, params);
+ _init(device, params) {
+ super._init(device.name, device.isConnected, {});
- this._bluetoothDevice = bluetoothDevice
- this.isBluetoothDeviceSwitcher = true;
- this.isEmitActivatedEnabled = true;
+ this._device = device;
+ this._showRefreshButton = params.showRefreshButton;
+ this._closeMenuOnAction = params.closeMenuOnAction;
this.label.x_expand = true;
this._statusBin.x_expand = false;
@@ -37,6 +43,18 @@ var PopupBluetoothDeviceMenuItem = GObject.registerClass(
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() {
@@ -72,21 +90,20 @@ var PopupBluetoothDeviceMenuItem = GObject.registerClass(
});
button.connect('clicked', () => {
- this._pending();
- this._bluetoothDevice.reconnect();
+ this._enablePending();
+ this._device.reconnect(() => {
+ this._disablePending()
+ });
- if (this.isEmitActivatedEnabled)
+ if (this._closeMenuOnAction)
this.emit('activate', Clutter.get_current_event());
});
- if (!this._bluetoothDevice.isConnected)
- button.hide();
-
return button;
}
_buildPendingLabel() {
- let label = new St.Label({ text: _('Wait') });
+ let label = new St.Label({text: _('Wait')});
label.hide();
return label;
@@ -95,15 +112,21 @@ var PopupBluetoothDeviceMenuItem = GObject.registerClass(
_connectToggledEvent() {
this.connect('toggled', (item, state) => {
if (state)
- this._bluetoothDevice.connect();
+ this._device.connect(() => {
+ this._disablePending()
+ });
else
- this._bluetoothDevice.disconnect();
+ this._device.disconnect(() => {
+ this._disablePending()
+ });
});
}
activate(event) {
- if (this._switch.mapped)
+ 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
@@ -111,24 +134,31 @@ var PopupBluetoothDeviceMenuItem = GObject.registerClass(
event.get_key_symbol() == Clutter.KEY_space)
return;
- if (this.isEmitActivatedEnabled)
+ if (this._closeMenuOnAction)
this.emit('activate', event);
}
toggle() {
super.toggle();
- this._pending();
+ this._enablePending();
}
hideRefreshButton() {
this._refreshButton.hide();
}
- _pending() {
- 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 = [];
+ };
+}