123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- 'use strict';
- import Clutter from 'gi://Clutter';
- import St from 'gi://St';
- import GObject from 'gi://GObject';
- import GLib from 'gi://GLib';
- import Gio from 'gi://Gio';
- import {gettext as _, ngettext} from 'resource:///org/gnome/shell/extensions/extension.js';
- import * as Main from 'resource:///org/gnome/shell/ui/main.js';
- import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
- import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
- import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js';
- import * as Config from 'resource://org/gnome/shell/misc/config.js';
- import { ThinkPad } from './driver.js';
- const ICONS_PATH = GLib.Uri.resolve_relative(import.meta.url, '../icons', GLib.UriFlags.NONE);
- const SHELL_VERSION = Number(Config.PACKAGE_VERSION.split('.', 1));
- /**
- * Get icon
- *
- * @param {string} iconName Icon name
- * @param {boolean} colorMode Color mode or symbolic
- * @returns {Gio.Icon}
- */
- function getIcon(iconName, colorMode = false) {
- return Gio.icon_new_for_string(`${ICONS_PATH}/${iconName}${colorMode ? '' : '-symbolic'}.svg`);
- }
- const BatteryItem = GObject.registerClass({
- GTypeName: 'BatteryItem',
- }, class BatteryItem extends PopupMenu.PopupImageMenuItem {
- constructor(battery, settings) {
- super('', null);
- this._settings = settings;
- this._battery = battery;
-
- const box = new St.BoxLayout({
- 'opacity': 128,
- 'x_expand': true,
- 'x_align': Clutter.ActorAlign.END,
- 'style': 'spacing: 5px;',
- });
- this.add_child(box);
- // Reload icon
- this._reload = new St.Icon({
- 'icon-size': 16,
- 'reactive': true,
- 'icon-name': 'view-refresh-symbolic',
- });
- this._reload.connectObject(
- 'button-press-event', () => {
- this._battery.enable();
- return true;
- },
- this
- );
- box.add_child(this._reload);
- this._valuesLabel = new St.Label({
- 'y_align': Clutter.ActorAlign.CENTER,
- 'style': 'font-size: 0.75em;',
- });
- box.add_child(this._valuesLabel);
- // Battery signals
- this._battery.connectObject(
- 'notify', () => {
- this._update();
- },
- this
- );
- // Settings changes
- this._settings.connectObject(
- 'changed', () => {
- this._update();
- },
- this
- );
- // Menu item action
- this.connectObject(
- 'activate', () => {
- this._battery.toggle();
- },
- 'destroy', () => {
- this._settings.disconnectObject(this);
- this._battery.disconnectObject(this);
- this._reload.disconnectObject(this);
- this.disconnectObject(this);
- this._valuesLabel.destroy();
- this._valuesLabel = null;
- this._reload.destroy();
- this._reload = null;
- this._battery = null;
- this._settings = null;
- },
- this
- );
- this._update();
- }
- /**
- * Update UI
- */
- _update() {
- const colorMode = this._settings.get_boolean('color-mode');
- // Menu text and icon
- if (this._battery.isActive) {
- // TRANSLATORS: %s is the name of the battery.
- this.label.text = _('Disable thresholds (%s)').format(this._battery.name);
- if (this._battery.pendingChanges) {
- this.setIcon(getIcon('threshold-active-warning', colorMode));
- } else {
- this.setIcon(getIcon('threshold-active', colorMode));
- }
- // Status text
- const showCurrentValues = this._settings.get_boolean('show-current-values');
- if (showCurrentValues) {
- // TRANSLATORS: %d/%d are the [start/end] threshold values. The string %% is the percent symbol (may need to be escaped depending on the language)
- this._valuesLabel.text = _('%d/%d %%').format(this._battery.startValue || 0, this._battery.endValue || 100);
- this._valuesLabel.visible = true;
- } else {
- this._valuesLabel.visible = false;
- }
- } else {
- // TRANSLATORS: %s is the name of the battery.
- this.label.text = _('Enable thresholds (%s)').format(this._battery.name);
- this.setIcon(getIcon('threshold-inactive', colorMode ));
- this._valuesLabel.visible = false;
- }
- // Reload 'button'
- this._reload.visible = this._battery.pendingChanges && this._battery.isActive;
- // Menu item visibility
- this.visible = this._battery.isAvailable;
- }
- });
- const ThresholdToggle = GObject.registerClass({
- GTypeName: 'ThresholdToggle',
- }, class ThresholdToggle extends QuickSettings.QuickMenuToggle {
- constructor(driver, extensionObject) {
- super({
- 'title': _('Thresholds'),
- 'gicon': getIcon('threshold-app'),
- 'toggle-mode': false,
- //'subtitle': 'subtitle'
- });
- // Header
- this.menu.setHeader(
- getIcon('threshold-app'), // Icon
- _('Battery Threshold'), // Title
- driver.environment.productVersion ? driver.environment.productVersion : _('Unknown model')// Subtitle
- );
- // Unavailable
- this.unavailableMenuItem = new PopupMenu.PopupImageMenuItem(_('Thresholds not available'), getIcon('threshold-unknown'));
- this.unavailableMenuItem.sensitive = false;
- this.unavailableMenuItem.visible = false;
- this.menu.addMenuItem(this.unavailableMenuItem);
-
- // Batteries
- driver.batteries.forEach(battery => {
- // Battery menu item
- const item = new BatteryItem(battery, extensionObject.getSettings());
- this.menu.addMenuItem(item);
- });
- // Unavailable status
- this.unavailableMenuItem.visible = !driver.isAvailable;
- // Checked status
- this.checked = driver.isActive;
- // Driver signals
- driver.connectObject(
- 'notify::is-active', () => {
- this.checked = driver.isActive;
- },
- 'notify::is-available', () => {
- this.unavailableMenuItem.visible = !driver.isAvailable;
- },
- this
- );
- // Signals
- this.connectObject(
- 'clicked', () => {
- if (driver.isActive) {
- driver.disableAll();
- } else {
- driver.enableAll();
- }
- },
- 'destroy', () => {
- this.disconnectObject(this);
- driver.disconnectObject(this);
- this.menu.removeAll();
- },
- this
- );
- // Add an entry-point for more getSettings()
- this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
- const settingsItem = this.menu.addAction(_('Thresholds settings'),
- () => extensionObject.openPreferences());
-
- // Ensure the getSettings() are unavailable when the screen is locked
- settingsItem.visible = Main.sessionMode.allowSettings;
- this.menu._settingsActions[extensionObject.uuid] = settingsItem;
- }
- });
- export const ThresholdIndicator = GObject.registerClass({
- GTypeName: 'ThresholdIndicator',
- }, class ThresholdIndicator extends QuickSettings.SystemIndicator {
- constructor(extensionObject) {
- super();
- this._settings = extensionObject.getSettings();
- this._driver = new ThinkPad({'settings': this._settings})
- this._name = extensionObject.metadata.name;
- this._indicator = this._addIndicator();
- this._indicator.gicon = getIcon('threshold-unknown');
- this.quickSettingsItems.push(new ThresholdToggle(this._driver, extensionObject));
- Main.panel.statusArea.quickSettings.addExternalIndicator(this);
- this._updateIndicator();
- // Driver signals
- this._driver.connectObject(
- 'notify::is-available', () => {
- this._updateIndicator();
- },
- 'notify::is-active', () => {
- this._updateIndicator();
- },
- 'notify::pending-changes', () => {
- this._updateIndicator();
- },
- 'enable-battery-completed', (driver, battery, error) => {
- if (!error) {
- this._notifyEnabled(
- // TRANSLATORS: %s is the name of the battery. %d/%d are the [start/end] threshold values. The string %% is the percent symbol (may need to be escaped depending on the language)
- _('Battery (%s) charge thresholds enabled at %d/%d %%').format(
- battery.name, battery.startValue || 0, battery.endValue || 100
- )
- );
- } else {
- this._notifyError(
- // TRANSLATORS: The first %s is the name of the battery. The second %s is the error message. \n is new line.
- _('Failed to enable thresholds on battery %s. \nError: %s').format(
- battery.name, error.message
- )
- );
- }
- },
- 'disable-battery-completed', (driver, battery, error) => {
- if (!error) {
- this._notifyDisabled(
- // TRANSLATORS: %s is the name of the battery.
- _('Battery (%s) charge thresholds disabled').format(
- battery.name
- )
- );
- } else {
- this._notifyError(
- // TRANSLATORS: The first %s is the name of the battery. The second %s is the error message. \n is new line.
- _('Failed to disable thresholds on battery %s. \nError: %s').format(
- battery.name, error.message
- )
- );
- }
- },
- 'enable-all-completed', (driver, error) => {
- if (!error) {
- this._notifyEnabled(_('Thresholds enabled for all batteries'))
- } else {
- this._notifyError(
- // TRANSLATORS: %s is the error message. \n is new line.
- _('Failed to enable thresholds for all batteries. \nError: %s').format(
- error.message
- )
- );
- }
- },
- 'disable-all-completed', (driver, error) => {
- if (!error) {
- this._notifyDisabled(_('Thresholds disabled for all batteries'));
- } else {
- this._notifyError(
- // TRANSLATORS: %s is the error message. \n is new line.
- _('Failed to disable thresholds for all batteries. \nError: %s').format(
- error.message
- )
- );
- }
- },
- this
- );
- // Settings signals
- this._settings.connectObject(
- 'changed::color-mode', () => {
- this._updateIndicator();
- },
- 'changed::indicator-mode', () => {
- this._updateIndicator();
- },
- this
- );
- this.connect('destroy', () => {
- this.quickSettingsItems.forEach(item => item.destroy());
- this._settings.disconnectObject(this);
- this._settings = null;
- this._driver.disconnectObject(this);
- this._driver.destroy();
- this._driver = null;
- this._extension = null;
- });
- // Pending changes alert
- this._driver.batteries.every(battery => {
- if (battery.pendingChanges && battery.isActive) {
- this._notify(_('Battery Threshold'), _('The currently set thresholds do not match the configured ones'), 'threshold-active-warning', false);
- return false;
- }
- return true;
- });
- }
- /**
- * Update indicator (tray-icon)
- */
- _updateIndicator() {
- const colorMode = this._settings.get_boolean('color-mode');
- if (this._driver.isAvailable) {
- if (this._driver.isActive) {
- if (this._driver.pendingChanges) {
- this._indicator.gicon = getIcon('threshold-active-warning', colorMode);
- } else {
- this._indicator.gicon = getIcon('threshold-active', colorMode);
- }
- } else {
- this._indicator.gicon = getIcon('threshold-inactive', colorMode);
- }
- } else {
- this._indicator.gicon = getIcon('threshold-unknown', colorMode);
- }
- const indicatorMode = this._settings.get_enum('indicator-mode');
- switch (indicatorMode) {
- case 0: // Active
- this._indicator.visible = this._driver.isActive;
- break;
- case 1: // Inactive (or pending changes)
- this._indicator.visible = !this._driver.isActive || this._driver.pendingChanges;
- break;
- case 2: // Always
- this._indicator.visible = true;
- break;
- case 3: // Never (or pending changes)
- this._indicator.visible = this._driver.pendingChanges;
- break;
- default:
- this._indicator.visible = true;
- break;
- }
- }
- /**
- * Show notification.
- *
- * @param {string} msg Title
- * @param {string} details Message
- * @param {string} iconName Icon name
- * @param {boolean} [transient=true] Transient notification
- */
- _notify(msg, details, iconName, transient=true) {
- if (!this._settings.get_boolean('show-notifications')) return;
- if (SHELL_VERSION === 45) {
- const source = new MessageTray.Source(this._name);
- Main.messageTray.add(source);
- const notification = new MessageTray.Notification(
- source,
- msg,
- details,
- {gicon: getIcon(iconName, true)}
- );
- notification.setTransient(transient);
- source.showNotification(notification);
- } else {
- const source = new MessageTray.Source({'title': this._name});
- Main.messageTray.add(source);
- const notification = new MessageTray.Notification({
- source: source,
- title: msg,
- body: details,
- isTransient: transient,
- gicon: getIcon(iconName, true)
- });
- source.addNotification(notification);
- }
- }
- /**
- * Show error notification
- *
- * @param {string} message Message
- */
- _notifyError(message) {
- this._notify(_('Battery Threshold'), message, 'threshold-error', false);
- }
- /**
- * Show enabled notification
- *
- * @param {string} message Message
- */
- _notifyEnabled(message) {
- this._notify(_('Battery Threshold'), message, 'threshold-active');
- }
- /**
- * Show disabled notification
- *
- * @param {string} message Message
- */
- _notifyDisabled(message) {
- this._notify(_('Battery Threshold'), message, 'threshold-inactive');
- }
- });
|