2025-05-09 09:48:24 +02:00

227 lines
7.6 KiB
JavaScript

// 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/>.
import Clutter from "gi://Clutter";
import GLib from "gi://GLib";
import GObject from "gi://GObject";
import St from "gi://St";
import { gettext as _ } from "resource:///org/gnome/shell/extensions/extension.js";
import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js";
class PopupSwitchWithButtonMenuItem extends PopupMenu.PopupSwitchMenuItem {
_client;
_logger;
_device;
_showRefreshButton;
_closeMenuOnAction;
_refreshButton;
_pendingLabel;
_batteryInfo;
_afterReconnectTimeout = null;
_afterToggleTimeout = null;
_reconnectTimeout = null;
constructor(client, device, logger, params) {
const label = device.alias || device.name || "(unknown)";
super(label, device.connected);
this._handleIcon(device);
this._client = client;
this._logger = logger;
this._device = device;
this._showRefreshButton = params.showRefreshButton;
this._closeMenuOnAction = params.closeMenuOnAction;
this.actor.labelActor.xExpand = true;
this.actor._statusBin.xExpand = false;
this._refreshButton = this._buildRefreshButton();
this._pendingLabel = this._buildPendingLabel();
this._connectToggledEvent();
this._batteryInfo = new BatteryInfoWidget(params.showBatteryValue, params.showBatteryIcon);
this.insert_child_at_index(this._batteryInfo, this.get_n_children() - 1);
this.insert_child_at_index(this._refreshButton, this.get_n_children() - 1);
this.add_child(this._pendingLabel);
this.sync(device);
}
_handleIcon(device) {
if (!device.icon) return;
const deviceIcon = new St.Icon({
styleClass: "popup-menu-icon",
iconName: device.icon
});
this.insert_child_at_index(deviceIcon, 1);
}
disconnectSignals() {
if (this._afterReconnectTimeout != null) {
GLib.Source.remove(this._afterReconnectTimeout);
this._afterReconnectTimeout = null;
}
if (this._afterToggleTimeout != null) {
GLib.Source.remove(this._afterToggleTimeout);
this._afterToggleTimeout = null;
}
}
sync(device) {
this.disconnectSignals();
this._batteryInfo.visible = false;
this._device = device;
this.setToggleState(device.connected);
this.visible = device.paired;
if (this._showRefreshButton && device.connected) this._refreshButton.show();
else this._refreshButton.hide();
this._disablePending();
if (device.connected) {
if (device.batteryPercentage && device.batteryPercentage > 0) {
this._batteryInfo.show();
this._logger.log(
`Battery percentage ${device.alias || device.name}: ${device.batteryPercentage}`
);
this._batteryInfo.setPercentage(device.batteryPercentage);
}
}
}
_buildRefreshButton() {
const icon = new St.Icon({
iconName: "view-refresh",
styleClass: "popup-menu-icon",
opacity: 155
});
const button = new St.Button({
child: icon,
xAlign: 2
// Align.END
});
button.connect("enter-event", (widget) => {
widget.child.ease({
opacity: 255,
time: 0.05,
transition: Clutter.AnimationMode.LINEAR
});
});
button.connect("leave-event", (widget) => {
widget.child.ease({
opacity: 155,
time: 0.05,
transition: Clutter.AnimationMode.LINEAR
});
});
button.connect("clicked", () => {
this._enablePending();
this._logger.log(`Reconnecting to ${this._device.alias || this._device.name}`);
this._client.connect_service(this._device.get_object_path(), false, null, () => {
this._reconnectTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 7e3, () => {
this._logger.log(`Trying to Reconnect to ${this._device.alias || this._device.name}`);
this._client.connect_service(this._device.get_object_path(), true, null, () => {
});
this._logger.log(`Reconnected to ${this._device.alias || this._device.name}`);
this.sync(this._device);
return GLib.SOURCE_REMOVE;
});
});
this._afterReconnectTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1e4, () => {
this._disablePending();
this._afterReconnectTimeout = null;
return GLib.SOURCE_REMOVE;
});
if (this._closeMenuOnAction) this.emit("activate", Clutter.get_current_event());
});
return button;
}
_buildPendingLabel() {
const label = new St.Label({ text: _("Wait") });
label.hide();
return label;
}
_connectToggledEvent() {
this.connect("toggled", (item, state) => {
this._client.connect_service(this._device.get_object_path(), state, null, () => {
});
this._afterToggleTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1e4, () => {
this._disablePending();
this._afterToggleTimeout = null;
return GLib.SOURCE_REMOVE;
});
});
}
activate(event) {
if (this.actor._switch.mapped) {
this.toggle();
this.actor._switch.toggle();
}
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();
}
_enablePending() {
this._refreshButton.reactive = false;
this.actor._switch.hide();
this._pendingLabel.show();
this.reactive = false;
}
_disablePending() {
this._refreshButton.reactive = true;
this.actor._switch.show();
this._pendingLabel.hide();
this.reactive = true;
}
destroy() {
if (this._reconnectTimeout) {
GLib.Source.remove(this._reconnectTimeout);
this._reconnectTimeout = null;
}
this.disconnectSignals();
super.destroy();
}
}
const PopupBluetoothDeviceMenuItem = GObject.registerClass(PopupSwitchWithButtonMenuItem);
class BatteryInfoWidgetClass extends St.BoxLayout {
_icon;
_label;
constructor(showBatteryValue, showBatteryIcon) {
super({ visible: false, style: "spacing: 3px;" });
this._icon = new St.Icon({ styleClass: "popup-menu-icon" });
this.add_child(this._icon);
this._icon.iconName = "battery-missing-symbolic";
this._label = new St.Label({
xAlign: Clutter.ActorAlign.START,
yAlign: Clutter.ActorAlign.CENTER,
text: "100%",
styleClass: "monospace"
});
this._label.naturalWidth = this._label.width;
this._label.text = "";
this.add_child(this._label);
if (!showBatteryValue) this._label.hide();
if (!showBatteryIcon) this._icon.hide();
}
setPercentage(value) {
if (value == null) {
this._label.text = "";
this._icon.iconName = "battery-missing-symbolic";
} else {
this._label.text = `${value}%`;
const fillLevel = 10 * Math.floor(value / 10);
const iconName = `battery-level-${fillLevel}-symbolic`;
this._icon.iconName = iconName;
}
}
}
const BatteryInfoWidget = GObject.registerClass(BatteryInfoWidgetClass);
export {
PopupBluetoothDeviceMenuItem,
PopupSwitchWithButtonMenuItem
};