indicator.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. 'use strict';
  2. import Clutter from 'gi://Clutter';
  3. import St from 'gi://St';
  4. import GObject from 'gi://GObject';
  5. import GLib from 'gi://GLib';
  6. import Gio from 'gi://Gio';
  7. import {gettext as _, ngettext} from 'resource:///org/gnome/shell/extensions/extension.js';
  8. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  9. import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
  10. import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
  11. import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js';
  12. import * as Config from 'resource://org/gnome/shell/misc/config.js';
  13. import { ThinkPad } from './driver.js';
  14. const ICONS_PATH = GLib.Uri.resolve_relative(import.meta.url, '../icons', GLib.UriFlags.NONE);
  15. const SHELL_VERSION = Number(Config.PACKAGE_VERSION.split('.', 1));
  16. /**
  17. * Get icon
  18. *
  19. * @param {string} iconName Icon name
  20. * @param {boolean} colorMode Color mode or symbolic
  21. * @returns {Gio.Icon}
  22. */
  23. function getIcon(iconName, colorMode = false) {
  24. return Gio.icon_new_for_string(`${ICONS_PATH}/${iconName}${colorMode ? '' : '-symbolic'}.svg`);
  25. }
  26. const BatteryItem = GObject.registerClass({
  27. GTypeName: 'BatteryItem',
  28. }, class BatteryItem extends PopupMenu.PopupImageMenuItem {
  29. constructor(battery, settings) {
  30. super('', null);
  31. this._settings = settings;
  32. this._battery = battery;
  33. // Flag to prevent the PopupImageMenuItem from being activated when the reload icon is clicked
  34. this._reloading = false;
  35. const box = new St.BoxLayout({
  36. 'opacity': 128,
  37. 'x_expand': true,
  38. 'x_align': Clutter.ActorAlign.END,
  39. 'style': 'spacing: 5px;',
  40. });
  41. this.add_child(box);
  42. // Reload icon
  43. this._reload = new St.Icon({
  44. 'icon-size': 16,
  45. 'reactive': true,
  46. 'icon-name': 'view-refresh-symbolic',
  47. });
  48. this._reload.connectObject(
  49. 'button-press-event', () => {
  50. this._reloading = true;
  51. this._battery.enable();
  52. return true; // Does not prevent event propagation???
  53. },
  54. this
  55. );
  56. box.add_child(this._reload);
  57. this._valuesLabel = new St.Label({
  58. 'y_align': Clutter.ActorAlign.CENTER,
  59. 'style': 'font-size: 0.75em;',
  60. });
  61. box.add_child(this._valuesLabel);
  62. // Battery signals
  63. this._battery.connectObject(
  64. 'notify', () => {
  65. this._update();
  66. },
  67. this
  68. );
  69. // Settings changes
  70. this._settings.connectObject(
  71. 'changed', () => {
  72. this._update();
  73. },
  74. this
  75. );
  76. // Menu item action
  77. this.connectObject(
  78. 'activate', () => {
  79. if (!this._reloading) {
  80. this._battery.toggle();
  81. }
  82. this._reloading = false;
  83. },
  84. 'destroy', () => {
  85. this._settings.disconnectObject(this);
  86. this._battery.disconnectObject(this);
  87. this._reload.disconnectObject(this);
  88. this.disconnectObject(this);
  89. this._valuesLabel.destroy();
  90. this._valuesLabel = null;
  91. this._reload.destroy();
  92. this._reload = null;
  93. this._battery = null;
  94. this._settings = null;
  95. },
  96. this
  97. );
  98. this._update();
  99. }
  100. /**
  101. * Update UI
  102. */
  103. _update() {
  104. const colorMode = this._settings.get_boolean('color-mode');
  105. // Menu text and icon
  106. if (this._battery.isActive) {
  107. // TRANSLATORS: %s is the name of the battery.
  108. this.label.text = _('Disable thresholds (%s)').format(this._battery.name);
  109. this.setIcon(getIcon('threshold-active', colorMode));
  110. // Status text
  111. const showCurrentValues = this._settings.get_boolean('show-current-values');
  112. if (showCurrentValues) {
  113. // TRANSLATORS: %d/%d are the [start/end] threshold values. The string %% is the percent symbol (may need to be escaped depending on the language)
  114. this._valuesLabel.text = _('%d/%d %%').format(this._battery.startValue || 0, this._battery.endValue || 100);
  115. this._valuesLabel.visible = true;
  116. } else {
  117. this._valuesLabel.visible = false;
  118. }
  119. } else {
  120. // TRANSLATORS: %s is the name of the battery.
  121. this.label.text = _('Enable thresholds (%s)').format(this._battery.name);
  122. this.setIcon(getIcon('threshold-inactive', colorMode ));
  123. this._valuesLabel.visible = false;
  124. }
  125. // Reload 'button'
  126. this._reload.visible = this._battery.pendingChanges && this._battery.isActive;
  127. // Menu item visibility
  128. this.visible = this._battery.isAvailable;
  129. }
  130. });
  131. const ThresholdToggle = GObject.registerClass({
  132. GTypeName: 'ThresholdToggle',
  133. }, class ThresholdToggle extends QuickSettings.QuickMenuToggle {
  134. constructor(driver, extensionObject) {
  135. super({
  136. 'title': _('Thresholds'),
  137. 'gicon': getIcon('threshold-app'),
  138. 'toggle-mode': false,
  139. //'subtitle': 'subtitle'
  140. });
  141. // Header
  142. this.menu.setHeader(
  143. getIcon('threshold-app'), // Icon
  144. _('Battery Threshold'), // Title
  145. driver.environment.productVersion ? driver.environment.productVersion : _('Unknown model')// Subtitle
  146. );
  147. // Unavailable
  148. this.unavailableMenuItem = new PopupMenu.PopupImageMenuItem(_('Thresholds not available'), getIcon('threshold-unknown'));
  149. this.unavailableMenuItem.sensitive = false;
  150. this.unavailableMenuItem.visible = false;
  151. this.menu.addMenuItem(this.unavailableMenuItem);
  152. // Batteries
  153. driver.batteries.forEach(battery => {
  154. // Battery menu item
  155. const item = new BatteryItem(battery, extensionObject.getSettings());
  156. this.menu.addMenuItem(item);
  157. });
  158. // Unavailable status
  159. this.unavailableMenuItem.visible = !driver.isAvailable;
  160. // Checked status
  161. this.checked = driver.isActive;
  162. // Driver signals
  163. driver.connectObject(
  164. 'notify::is-active', () => {
  165. this.checked = driver.isActive;
  166. },
  167. 'notify::is-available', () => {
  168. this.unavailableMenuItem.visible = !driver.isAvailable;
  169. },
  170. this
  171. );
  172. // Signals
  173. this.connectObject(
  174. 'clicked', () => {
  175. if (driver.isActive) {
  176. driver.disableAll();
  177. } else {
  178. driver.enableAll();
  179. }
  180. },
  181. 'destroy', () => {
  182. this.disconnectObject(this);
  183. driver.disconnectObject(this);
  184. this.menu.removeAll();
  185. },
  186. this
  187. );
  188. // Add an entry-point for more getSettings()
  189. this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
  190. const settingsItem = this.menu.addAction(_('Thresholds settings'),
  191. () => extensionObject.openPreferences());
  192. // Ensure the getSettings() are unavailable when the screen is locked
  193. settingsItem.visible = Main.sessionMode.allowSettings;
  194. this.menu._settingsActions[extensionObject.uuid] = settingsItem;
  195. }
  196. });
  197. export const ThresholdIndicator = GObject.registerClass({
  198. GTypeName: 'ThresholdIndicator',
  199. }, class ThresholdIndicator extends QuickSettings.SystemIndicator {
  200. constructor(extensionObject) {
  201. super();
  202. this._settings = extensionObject.getSettings();
  203. this._driver = new ThinkPad({'settings': this._settings})
  204. this._name = extensionObject.metadata.name;
  205. this._indicator = this._addIndicator();
  206. this._indicator.gicon = getIcon('threshold-unknown');
  207. this.quickSettingsItems.push(new ThresholdToggle(this._driver, extensionObject));
  208. Main.panel.statusArea.quickSettings.addExternalIndicator(this);
  209. this._updateIndicator();
  210. // Driver signals
  211. this._driver.connectObject(
  212. 'notify::is-available', () => {
  213. this._updateIndicator();
  214. },
  215. 'notify::is-active', () => {
  216. this._updateIndicator();
  217. },
  218. 'enable-battery-completed', (driver, battery, error) => {
  219. if (!error) {
  220. this._notifyEnabled(
  221. // 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)
  222. _('Battery (%s) charge thresholds enabled at %d/%d %%').format(
  223. battery.name, battery.startValue || 0, battery.endValue || 100
  224. )
  225. );
  226. } else {
  227. this._notifyError(
  228. // TRANSLATORS: The first %s is the name of the battery. The second %s is the error message. \n is new line.
  229. _('Failed to enable thresholds on battery %s. \nError: %s').format(
  230. battery.name, error.message
  231. )
  232. );
  233. }
  234. },
  235. 'disable-battery-completed', (driver, battery, error) => {
  236. if (!error) {
  237. this._notifyDisabled(
  238. // TRANSLATORS: %s is the name of the battery.
  239. _('Battery (%s) charge thresholds disabled').format(
  240. battery.name
  241. )
  242. );
  243. } else {
  244. this._notifyError(
  245. // TRANSLATORS: The first %s is the name of the battery. The second %s is the error message. \n is new line.
  246. _('Failed to disable thresholds on battery %s. \nError: %s').format(
  247. battery.name, error.message
  248. )
  249. );
  250. }
  251. },
  252. 'enable-all-completed', (driver, error) => {
  253. if (!error) {
  254. this._notifyEnabled(_('Thresholds enabled for all batteries'))
  255. } else {
  256. this._notifyError(
  257. // TRANSLATORS: %s is the error message. \n is new line.
  258. _('Failed to enable thresholds for all batteries. \nError: %s').format(
  259. error.message
  260. )
  261. );
  262. }
  263. },
  264. 'disable-all-completed', (driver, error) => {
  265. if (!error) {
  266. this._notifyDisabled(_('Thresholds disabled for all batteries'));
  267. } else {
  268. this._notifyError(
  269. // TRANSLATORS: %s is the error message. \n is new line.
  270. _('Failed to disable thresholds for all batteries. \nError: %s').format(
  271. error.message
  272. )
  273. );
  274. }
  275. },
  276. this
  277. );
  278. // Settings signals
  279. this._settings.connectObject(
  280. 'changed::color-mode', () => {
  281. this._updateIndicator();
  282. },
  283. 'changed::indicator-mode', () => {
  284. this._updateIndicator();
  285. },
  286. this
  287. );
  288. this.connect('destroy', () => {
  289. this.quickSettingsItems.forEach(item => item.destroy());
  290. this._settings.disconnectObject(this);
  291. this._settings = null;
  292. this._driver.disconnectObject(this);
  293. this._driver.destroy();
  294. this._driver = null;
  295. this._extension = null;
  296. });
  297. }
  298. /**
  299. * Update indicator (tray-icon)
  300. */
  301. _updateIndicator() {
  302. const colorMode = this._settings.get_boolean('color-mode');
  303. if (this._driver.isAvailable) {
  304. if (this._driver.isActive) {
  305. this._indicator.gicon = getIcon('threshold-active', colorMode);
  306. } else {
  307. this._indicator.gicon = getIcon('threshold-inactive', colorMode);
  308. }
  309. } else {
  310. this._indicator.gicon = getIcon('threshold-unknown', colorMode);
  311. }
  312. const indicatorMode = this._settings.get_enum('indicator-mode');
  313. switch (indicatorMode) {
  314. case 0: // Active
  315. this._indicator.visible = this._driver.isActive;
  316. break;
  317. case 1: // Inactive
  318. this._indicator.visible = !this._driver.isActive;
  319. break;
  320. case 2: // Always
  321. this._indicator.visible = true;
  322. break;
  323. case 3: // Never
  324. this._indicator.visible = false;
  325. break;
  326. default:
  327. this._indicator.visible = true;
  328. break;
  329. }
  330. }
  331. /**
  332. * Show notificaion.
  333. *
  334. * @param {string} msg Title
  335. * @param {string} details Message
  336. * @param {string} iconName Icon name
  337. */
  338. _notify(msg, details, iconName) {
  339. if (!this._settings.get_boolean('show-notifications')) return;
  340. const colorMode = this._settings.get_boolean('color-mode');
  341. if (SHELL_VERSION === 45) {
  342. const source = new MessageTray.Source(this._name);
  343. Main.messageTray.add(source);
  344. const notification = new MessageTray.Notification(
  345. source,
  346. msg,
  347. details,
  348. {gicon: getIcon(iconName, colorMode)}
  349. );
  350. notification.setTransient(true);
  351. source.showNotification(notification);
  352. } else {
  353. const source = new MessageTray.Source({'title': this._name});
  354. Main.messageTray.add(source);
  355. const notification = new MessageTray.Notification({
  356. source: source,
  357. title: msg,
  358. body: details,
  359. isTransient: true,
  360. gicon: getIcon(iconName, colorMode)
  361. });
  362. source.addNotification(notification);
  363. }
  364. }
  365. /**
  366. * Show error notification
  367. *
  368. * @param {string} message Message
  369. */
  370. _notifyError(message) {
  371. this._notify(_('Battery Threshold'), message, 'threshold-error');
  372. }
  373. /**
  374. * Show enabled notification
  375. *
  376. * @param {string} message Message
  377. */
  378. _notifyEnabled(message) {
  379. this._notify(_('Battery Threshold'), message, 'threshold-active');
  380. }
  381. /**
  382. * Show disabled notification
  383. *
  384. * @param {string} message Message
  385. */
  386. _notifyDisabled(message) {
  387. this._notify(_('Battery Threshold'), message, 'threshold-inactive');
  388. }
  389. });