From af91ce4c4513ad8ef965b753e026650df8df983f Mon Sep 17 00:00:00 2001 From: Bartosz Jaroszewski Date: Sat, 23 May 2020 10:51:59 +0200 Subject: total rebuild --- README.md | 35 ++- Settings.ui | 47 +++- bluetooth.js | 121 +++++++++++ convenience.js | 93 -------- extension.js | 240 ++++++++------------- prefs.js | 15 +- ....extensions.bluetooth-quick-connect.gschema.xml | 4 + settings.js | 64 ++++++ ui.js | 70 ++++-- utils.js | 68 ++++++ 10 files changed, 484 insertions(+), 273 deletions(-) create mode 100644 bluetooth.js delete mode 100644 convenience.js create mode 100644 settings.js create mode 100644 utils.js 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 +``` + +#### Disconnecting +```bash +bluetoothctl -- disconnect +``` + +#### Reconnecting +```bash +bluetoothctl -- disconnect && bluetoothctl -- connect +``` + +### 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 True False True - Keep the menu open after toggling the connection + Keep the menu open after toggling the connection (restart required) 0 @@ -304,7 +304,49 @@ SPDX-License-Identifier: GPL-2.0-or-later True False True - Show reconnect button + Show reconnect button (restart required) + 0 + + + 0 + 0 + + + + + + + + + True + True + + + True + False + 12 + 12 + 12 + 12 + 32 + + + True + True + end + center + + + 1 + 0 + + + + + True + False + True + Debug mode (restart required) 0 @@ -316,7 +358,6 @@ SPDX-License-Identifier: GPL-2.0-or-later - 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 . + +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 - - 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 . 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 false Show reconnect button + + false + Debug mode + 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 . + +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 . -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 . + +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 = []; + }; +} -- cgit v1.2.3