// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect // // SPDX-License-Identifier: GPL-2.0-or-later import Clutter from 'gi://Clutter'; import GObject from 'gi://GObject'; import St from 'gi://St'; import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; import {getIcon} from './utils.js'; import * as GMenu from './gmenu.js'; import Tooltip from './tooltip.js'; /** * A battery widget with an icon, text percentage and time estimate tooltip */ export const Battery = GObject.registerClass({ GTypeName: 'GSConnectShellDeviceBattery', }, class Battery extends St.BoxLayout { _init(params) { super._init({ reactive: true, style_class: 'gsconnect-device-battery', track_hover: true, }); Object.assign(this, params); // Percent Label this.label = new St.Label({ y_align: Clutter.ActorAlign.CENTER, }); this.label.clutter_text.ellipsize = 0; this.add_child(this.label); // Battery Icon this.icon = new St.Icon({ fallback_icon_name: 'battery-missing-symbolic', icon_size: 16, }); this.add_child(this.icon); // Battery Estimate this.tooltip = new Tooltip({ parent: this, text: null, }); // Battery GAction this._actionAddedId = this.device.action_group.connect( 'action-added', this._onActionChanged.bind(this) ); this._actionRemovedId = this.device.action_group.connect( 'action-removed', this._onActionChanged.bind(this) ); this._actionStateChangedId = this.device.action_group.connect( 'action-state-changed', this._onStateChanged.bind(this) ); this._onActionChanged(this.device.action_group, 'battery'); // Cleanup on destroy this.connect('destroy', this._onDestroy); } _onActionChanged(action_group, action_name) { if (action_name !== 'battery') return; if (action_group.has_action('battery')) { const value = action_group.get_action_state('battery'); const [charging, icon_name, level, time] = value.deepUnpack(); this._state = { charging: charging, icon_name: icon_name, level: level, time: time, }; } else { this._state = null; } this._sync(); } _onStateChanged(action_group, action_name, value) { if (action_name !== 'battery') return; const [charging, icon_name, level, time] = value.deepUnpack(); this._state = { charging: charging, icon_name: icon_name, level: level, time: time, }; this._sync(); } _getBatteryLabel() { if (!this._state) return null; const {charging, level, time} = this._state; if (level === 100) // TRANSLATORS: When the battery level is 100% return _('Fully Charged'); if (time === 0) // TRANSLATORS: When no time estimate for the battery is available // EXAMPLE: 42% (Estimating…) return _('%d%% (Estimating…)').format(level); const total = time / 60; const minutes = Math.floor(total % 60); const hours = Math.floor(total / 60); if (charging) { // TRANSLATORS: Estimated time until battery is charged // EXAMPLE: 42% (1:15 Until Full) return _('%d%% (%d\u2236%02d Until Full)').format( level, hours, minutes ); } else { // TRANSLATORS: Estimated time until battery is empty // EXAMPLE: 42% (12:15 Remaining) return _('%d%% (%d\u2236%02d Remaining)').format( level, hours, minutes ); } } _onDestroy(actor) { actor.device.action_group.disconnect(actor._actionAddedId); actor.device.action_group.disconnect(actor._actionRemovedId); actor.device.action_group.disconnect(actor._actionStateChangedId); } _sync() { this.visible = !!this._state; if (!this.visible) return; this.icon.icon_name = this._state.icon_name; this.label.text = (this._state.level > -1) ? `${this._state.level}%` : ''; this.tooltip.text = this._getBatteryLabel(); } }); /** * A cell signal strength widget with two icons */ export const SignalStrength = GObject.registerClass({ GTypeName: 'GSConnectShellDeviceSignalStrength', }, class SignalStrength extends St.BoxLayout { _init(params) { super._init({ reactive: true, style_class: 'gsconnect-device-signal-strength', track_hover: true, }); Object.assign(this, params); // Network Type Icon this.networkTypeIcon = new St.Icon({ fallback_icon_name: 'network-cellular-symbolic', icon_size: 16, }); this.add_child(this.networkTypeIcon); // Signal Strength Icon this.signalStrengthIcon = new St.Icon({ fallback_icon_name: 'network-cellular-offline-symbolic', icon_size: 16, }); this.add_child(this.signalStrengthIcon); // Network Type Text this.tooltip = new Tooltip({ parent: this, text: null, }); // ConnectivityReport GAction this._actionAddedId = this.device.action_group.connect( 'action-added', this._onActionChanged.bind(this) ); this._actionRemovedId = this.device.action_group.connect( 'action-removed', this._onActionChanged.bind(this) ); this._actionStateChangedId = this.device.action_group.connect( 'action-state-changed', this._onStateChanged.bind(this) ); this._onActionChanged(this.device.action_group, 'connectivityReport'); // Cleanup on destroy this.connect('destroy', this._onDestroy); } _onActionChanged(action_group, action_name) { if (action_name !== 'connectivityReport') return; if (action_group.has_action('connectivityReport')) { const value = action_group.get_action_state('connectivityReport'); const [ cellular_network_type, cellular_network_type_icon, cellular_network_strength, cellular_network_strength_icon, hotspot_name, hotspot_bssid, ] = value.deepUnpack(); this._state = { cellular_network_type: cellular_network_type, cellular_network_type_icon: cellular_network_type_icon, cellular_network_strength: cellular_network_strength, cellular_network_strength_icon: cellular_network_strength_icon, hotspot_name: hotspot_name, hotspot_bssid: hotspot_bssid, }; } else { this._state = null; } this._sync(); } _onStateChanged(action_group, action_name, value) { if (action_name !== 'connectivityReport') return; const [ cellular_network_type, cellular_network_type_icon, cellular_network_strength, cellular_network_strength_icon, hotspot_name, hotspot_bssid, ] = value.deepUnpack(); this._state = { cellular_network_type: cellular_network_type, cellular_network_type_icon: cellular_network_type_icon, cellular_network_strength: cellular_network_strength, cellular_network_strength_icon: cellular_network_strength_icon, hotspot_name: hotspot_name, hotspot_bssid: hotspot_bssid, }; this._sync(); } _onDestroy(actor) { actor.device.action_group.disconnect(actor._actionAddedId); actor.device.action_group.disconnect(actor._actionRemovedId); actor.device.action_group.disconnect(actor._actionStateChangedId); } _sync() { this.visible = !!this._state; if (!this.visible) return; this.networkTypeIcon.icon_name = this._state.cellular_network_type_icon; this.signalStrengthIcon.icon_name = this._state.cellular_network_strength_icon; this.tooltip.text = this._state.cellular_network_type; } }); /** * A PopupMenu used as an information and control center for a device */ export class Menu extends PopupMenu.PopupMenuSection { constructor(params) { super(); Object.assign(this, params); this.actor.add_style_class_name('gsconnect-device-menu'); // Title this._title = new PopupMenu.PopupSeparatorMenuItem(this.device.name); this.addMenuItem(this._title); // Title -> Name this._title.label.style_class = 'gsconnect-device-name'; this._title.label.clutter_text.ellipsize = 0; this.device.bind_property( 'name', this._title.label, 'text', GObject.BindingFlags.SYNC_CREATE ); // Title -> Cellular Signal Strength this._signalStrength = new SignalStrength({device: this.device}); this._title.actor.add_child(this._signalStrength); // Title -> Battery this._battery = new Battery({device: this.device}); this._title.actor.add_child(this._battery); // Actions let actions; if (this.menu_type === 'icon') { actions = new GMenu.IconBox({ action_group: this.device.action_group, model: this.device.menu, }); } else if (this.menu_type === 'list') { actions = new GMenu.ListBox({ action_group: this.device.action_group, model: this.device.menu, }); } this.addMenuItem(actions); } isEmpty() { return false; } } /** * An indicator representing a Device in the Status Area */ export const Indicator = GObject.registerClass({ GTypeName: 'GSConnectDeviceIndicator', }, class Indicator extends PanelMenu.Button { _init(params) { super._init(0.0, `${params.device.name} Indicator`, false); Object.assign(this, params); // Device Icon this._icon = new St.Icon({ gicon: getIcon(this.device.icon_name), style_class: 'system-status-icon gsconnect-device-indicator', }); this.add_child(this._icon); // Menu const menu = new Menu({ device: this.device, menu_type: 'icon', }); this.menu.addMenuItem(menu); } });