// 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 . import BluetoothController from "./bluetooth.js"; import Settings from "./settings.js"; import { PopupBluetoothDeviceMenuItem } from "./ui.js"; import { Logger } from "./utils.js"; import GLib from "gi://GLib"; import { Extension } from "resource:///org/gnome/shell/extensions/extension.js"; import * as Main from "resource:///org/gnome/shell/ui/main.js"; import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js"; class BluetoothQuickConnect extends Extension { _settings; _logger; _controller; _menu; _proxy; _items = {}; _signals = []; _idleMonitorId = null; enable() { this._settings = new Settings(this); this._logger = new Logger(this._settings); this._controller = new BluetoothController(); this._logger.log("Enabling extension"); this._queueModify(); this._menu = new PopupMenu.PopupMenuSection(); this._items = {}; this._controller.enable(); this._refresh(); this._connectControllerSignals(); this._connectIdleMonitor(); this._connectMenuSignals(); } disable() { this._logger.log("Disabling extension"); this._removeDevicesFromMenu(); this._disconnectIdleMonitor(); this._settings = null; this._logger = null; this._controller?.destroy(); this._controller = null; this._menu = null; this._disconnectSignals(); } _queueModify() { GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { if (!Main.panel.statusArea.quickSettings._bluetooth) { return GLib.SOURCE_CONTINUE; } const btIndicator = Main.panel.statusArea.quickSettings._bluetooth; const bluetoothToggle = btIndicator.quickSettingsItems[0]; bluetoothToggle._updateDeviceVisibility = () => { bluetoothToggle._deviceSection.actor.visible = false; bluetoothToggle._placeholderItem.actor.visible = false; }; bluetoothToggle._updateDeviceVisibility(); this._proxy = bluetoothToggle._client._proxy; bluetoothToggle.menu.addMenuItem(this._menu, 0); return GLib.SOURCE_REMOVE; }); } _connectMenuSignals() { this._connectSignal(this._menu, "open-state-changed", (_menu, isOpen) => { this._logger.log(`Menu toggled: ${isOpen}`); if (isOpen) { this._disconnectIdleMonitor(); } else { this._connectIdleMonitor(); } if (isOpen && this._settings.isAutoPowerOnEnabled() && this._proxy.BluetoothAirplaneMode) { this._logger.log("Disabling airplane mode"); this._proxy.BluetoothAirplaneMode = false; } }); } _connectControllerSignals() { this._logger.log("Connecting bluetooth controller signals"); this._connectSignal(this._controller, "default-adapter-changed", (_ctrl) => { this._logger.log("Default adapter changed event"); this._refresh(); }); this._connectSignal(this._controller, "device-inserted", (_ctrl, device) => { this._logger.log(`Device inserted event: ${device.alias || device.name}`); if (device.paired) { this._addMenuItem(device); } else { this._logger.log(`Device ${device.alias || device.name} not paired, ignoring`); } }); this._connectSignal(this._controller, "device-changed", (_ctrl, device) => { this._logger.log(`Device changed event: ${device.alias || device.name}`); if (device.paired) this._syncMenuItem(device); else this._logger.log( `Skipping change event for unpaired device ${device.alias || device.name}` ); }); this._connectSignal(this._controller, "device-deleted", (_ctrl, skipDevicePath) => { this._logger.log("Device deleted event"); this._refresh(skipDevicePath); }); } _syncMenuItem(device) { this._logger.log(`Synchronizing device menu item: ${device.alias || device.name}`); const item = this._items[device.address || ""] || this._addMenuItem(device); item.sync(device); } _addMenuItem(device) { this._logger.log(`Adding device menu item: ${device.alias || device.name} ${device.address}`); const menuItem = new PopupBluetoothDeviceMenuItem( this._controller._client, device, this._logger, { showRefreshButton: this._settings.isShowRefreshButtonEnabled(), closeMenuOnAction: !this._settings.isKeepMenuOnToggleEnabled(), showBatteryValue: this._settings.isShowBatteryValueEnabled(), showBatteryIcon: this._settings.isShowBatteryIconEnabled() } ); this._items[device.address || ""] = menuItem; this._menu.addMenuItem(menuItem); return menuItem; } _connectIdleMonitor() { if (this._idleMonitorId) return; this._logger.log("Connecting idle monitor"); this._idleMonitorId = GLib.timeout_add( GLib.PRIORITY_DEFAULT, this._settings.autoPowerOffCheckingInterval() * 1e3, () => { if (this._settings.isAutoPowerOffEnabled() && this._controller.getConnectedDevices().length === 0) { this._proxy.BluetoothAirplaneMode = true; } return true; } ); } _disconnectIdleMonitor() { if (!this._idleMonitorId) return; this._logger.log("Disconnecting idle monitor"); GLib.Source.remove(this._idleMonitorId); this._idleMonitorId = null; } _connectSignal(subject, signal_name, method) { if (!this._signals) this._signals = []; const signal_id = subject.connect(signal_name, method); this._signals.push({ subject, signal_id }); } _disconnectSignals() { if (!this._signals) return; for (const signal of this._signals) { signal.subject.disconnect(signal.signal_id); } this._signals = []; } _refresh(skipDevice = null) { this._removeDevicesFromMenu(); this._addDevicesToMenu(skipDevice); this._logger.log("Refreshing devices list"); } _addDevicesToMenu(skipDevice = null) { for (const device of this._controller.getDevices().sort((a, b) => { if (!a.name) return 0; return a.name.localeCompare(b.name || ""); })) { if (device.paired && device.get_object_path() !== skipDevice) { this._addMenuItem(device); } else { this._logger.log(`skipping adding device ${device.alias || device.name}`); } } } _removeDevicesFromMenu() { for (const item of Object.values(this._items)) { item.destroy(); } this._items = {}; } } export { BluetoothQuickConnect as default };