appIndicator.js 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600
  1. // This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
  2. //
  3. // This program is free software; you can redistribute it and/or
  4. // modify it under the terms of the GNU General Public License
  5. // as published by the Free Software Foundation; either version 2
  6. // of the License, or (at your option) any later version.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program; if not, write to the Free Software
  15. // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. import Clutter from 'gi://Clutter';
  17. import GLib from 'gi://GLib';
  18. import GObject from 'gi://GObject';
  19. import GdkPixbuf from 'gi://GdkPixbuf';
  20. import Gio from 'gi://Gio';
  21. import St from 'gi://St';
  22. import * as Params from 'resource:///org/gnome/shell/misc/params.js';
  23. import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
  24. import * as IconCache from './iconCache.js';
  25. import * as Util from './util.js';
  26. import * as Interfaces from './interfaces.js';
  27. import * as PixmapsUtils from './pixmapsUtils.js';
  28. import * as PromiseUtils from './promiseUtils.js';
  29. import * as SettingsManager from './settingsManager.js';
  30. import {DBusProxy} from './dbusProxy.js';
  31. Gio._promisify(Gio.File.prototype, 'read_async');
  32. Gio._promisify(GdkPixbuf.Pixbuf, 'get_file_info_async');
  33. Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_at_scale_async',
  34. 'new_from_stream_finish');
  35. Gio._promisify(St.IconInfo.prototype, 'load_symbolic_async');
  36. Gio._promisify(Gio.DBusConnection.prototype, 'call');
  37. const MAX_UPDATE_FREQUENCY = 30; // In ms
  38. const FALLBACK_ICON_NAME = 'image-loading-symbolic';
  39. const PIXMAPS_FORMAT = imports.gi.Cogl.PixelFormat.ARGB_8888;
  40. export const SNICategory = Object.freeze({
  41. APPLICATION: 'ApplicationStatus',
  42. COMMUNICATIONS: 'Communications',
  43. SYSTEM: 'SystemServices',
  44. HARDWARE: 'Hardware',
  45. });
  46. export const SNIStatus = Object.freeze({
  47. PASSIVE: 'Passive',
  48. ACTIVE: 'Active',
  49. NEEDS_ATTENTION: 'NeedsAttention',
  50. });
  51. const SNIconType = Object.freeze({
  52. NORMAL: 0,
  53. ATTENTION: 1,
  54. OVERLAY: 2,
  55. toPropertyName: (iconType, params = {isPixbuf: false}) => {
  56. let propertyName = 'Icon';
  57. if (iconType === SNIconType.OVERLAY)
  58. propertyName = 'OverlayIcon';
  59. else if (iconType === SNIconType.ATTENTION)
  60. propertyName = 'AttentionIcon';
  61. return `${propertyName}${params.isPixbuf ? 'Pixmap' : 'Name'}`;
  62. },
  63. });
  64. export const AppIndicatorProxy = GObject.registerClass(
  65. class AppIndicatorProxy extends DBusProxy {
  66. static get interfaceInfo() {
  67. if (!this._interfaceInfo) {
  68. this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(
  69. Interfaces.StatusNotifierItem);
  70. }
  71. return this._interfaceInfo;
  72. }
  73. static get OPTIONAL_PROPERTIES() {
  74. return [
  75. 'XAyatanaLabel',
  76. 'XAyatanaLabelGuide',
  77. 'XAyatanaOrderingIndex',
  78. 'IconAccessibleDesc',
  79. 'AttentionAccessibleDesc',
  80. ];
  81. }
  82. static get TUPLE_TYPE() {
  83. if (!this._tupleType)
  84. this._tupleType = new GLib.VariantType('()');
  85. return this._tupleType;
  86. }
  87. static destroy() {
  88. delete this._interfaceInfo;
  89. delete this._tupleType;
  90. }
  91. _init(busName, objectPath) {
  92. const {interfaceInfo} = AppIndicatorProxy;
  93. super._init(busName, objectPath, interfaceInfo,
  94. Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES);
  95. this.set_cached_property('Status',
  96. new GLib.Variant('s', SNIStatus.PASSIVE));
  97. this._accumulatedProperties = new Set();
  98. this._cancellables = new Map();
  99. this._changedProperties = Object.create(null);
  100. }
  101. async initAsync(cancellable) {
  102. await super.initAsync(cancellable);
  103. this._setupProxyPropertyList();
  104. }
  105. destroy() {
  106. const cachedProperties = this.get_cached_property_names();
  107. if (cachedProperties) {
  108. cachedProperties.forEach(propertyName =>
  109. this.set_cached_property(propertyName, null));
  110. }
  111. super.destroy();
  112. }
  113. _onNameOwnerChanged() {
  114. this._resetNeededProperties();
  115. if (!this.gNameOwner)
  116. this._cancelRefreshProperties();
  117. else
  118. this._setupProxyPropertyList();
  119. }
  120. _setupProxyPropertyList() {
  121. this._propertiesList =
  122. (this.get_cached_property_names() || []).filter(p =>
  123. this.gInterfaceInfo.properties.some(pInfo => pInfo.name === p));
  124. if (this._propertiesList.length) {
  125. AppIndicatorProxy.OPTIONAL_PROPERTIES.forEach(
  126. p => this._addExtraProperty(p));
  127. }
  128. }
  129. _addExtraProperty(name) {
  130. if (this._propertiesList.includes(name))
  131. return;
  132. if (!(name in this)) {
  133. Object.defineProperty(this, name, {
  134. configurable: false,
  135. enumerable: true,
  136. get: () => {
  137. const v = this.get_cached_property(name);
  138. return v ? v.deep_unpack() : null;
  139. },
  140. });
  141. }
  142. this._propertiesList.push(name);
  143. }
  144. _signalToPropertyName(signal) {
  145. if (signal.startsWith('New'))
  146. return signal.substr(3);
  147. else if (signal.startsWith('XAyatanaNew'))
  148. return `XAyatana${signal.substr(11)}`;
  149. return null;
  150. }
  151. // The Author of the spec didn't like the PropertiesChanged signal, so he invented his own
  152. async _refreshOwnProperties(prop) {
  153. await Promise.all(
  154. [prop, `${prop}Name`, `${prop}Pixmap`, `${prop}AccessibleDesc`].filter(p =>
  155. this._propertiesList.includes(p)).map(async p => {
  156. try {
  157. await this.refreshProperty(p, {
  158. skipEqualityCheck: p.endsWith('Pixmap'),
  159. });
  160. } catch (e) {
  161. if (!AppIndicatorProxy.OPTIONAL_PROPERTIES.includes(p) ||
  162. !(e instanceof Gio.DBusError))
  163. logError(e);
  164. }
  165. }));
  166. }
  167. _onSignal(sender, signal, ...args) {
  168. this._onSignalAsync(sender, signal, ...args).catch(e => {
  169. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  170. logError(e, `Error while processing signal '${signal}'`);
  171. });
  172. }
  173. async _onSignalAsync(_sender, signal, params) {
  174. const property = this._signalToPropertyName(signal);
  175. if (!property)
  176. return;
  177. if (this.status === SNIStatus.PASSIVE &&
  178. ![...AppIndicator.NEEDED_PROPERTIES, 'Status'].includes(property)) {
  179. this._accumulatedProperties.add(property);
  180. return;
  181. }
  182. if (!params.get_type().equal(AppIndicatorProxy.TUPLE_TYPE)) {
  183. // If the property includes arguments, we can just queue the signal emission
  184. const [value] = params.unpack();
  185. try {
  186. await this._queuePropertyUpdate(property, value);
  187. } catch (e) {
  188. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  189. throw e;
  190. }
  191. if (!this._accumulatedProperties.size)
  192. return;
  193. } else {
  194. this._accumulatedProperties.add(property);
  195. }
  196. if (this._signalsAccumulator)
  197. return;
  198. this._signalsAccumulator = new PromiseUtils.TimeoutPromise(
  199. MAX_UPDATE_FREQUENCY, GLib.PRIORITY_DEFAULT_IDLE, this._cancellable);
  200. try {
  201. await this._signalsAccumulator;
  202. const refreshPropertiesPromises =
  203. [...this._accumulatedProperties].map(p =>
  204. this._refreshOwnProperties(p));
  205. this._accumulatedProperties.clear();
  206. await Promise.all(refreshPropertiesPromises);
  207. } catch (e) {
  208. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  209. throw e;
  210. } finally {
  211. delete this._signalsAccumulator;
  212. }
  213. }
  214. _resetNeededProperties() {
  215. AppIndicator.NEEDED_PROPERTIES.forEach(p =>
  216. this.set_cached_property(p, null));
  217. }
  218. async refreshAllProperties() {
  219. const cancellableName = 'org.freedesktop.DBus.Properties.GetAll';
  220. const cancellable = this._cancelRefreshProperties({
  221. propertyName: cancellableName,
  222. addNew: true,
  223. });
  224. try {
  225. const [valuesVariant] = (await this.getProperties(
  226. cancellable)).deep_unpack();
  227. this._cancellables.delete(cancellableName);
  228. await Promise.all(
  229. Object.entries(valuesVariant).map(([propertyName, valueVariant]) =>
  230. this._queuePropertyUpdate(propertyName, valueVariant, {
  231. skipEqualityCheck: true,
  232. cancellable,
  233. })));
  234. } catch (e) {
  235. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
  236. // the property may not even exist, silently ignore it
  237. Util.Logger.debug(`While refreshing all properties: ${e}`);
  238. this.get_cached_property_names().forEach(propertyName =>
  239. this.set_cached_property(propertyName, null));
  240. this._cancellables.delete(cancellableName);
  241. throw e;
  242. }
  243. }
  244. }
  245. async refreshProperty(propertyName, params) {
  246. params = Params.parse(params, {
  247. skipEqualityCheck: false,
  248. });
  249. const cancellable = this._cancelRefreshProperties({
  250. propertyName,
  251. addNew: true,
  252. });
  253. try {
  254. const [valueVariant] = (await this.getProperty(
  255. propertyName, cancellable)).deep_unpack();
  256. this._cancellables.delete(propertyName);
  257. await this._queuePropertyUpdate(propertyName, valueVariant,
  258. Object.assign(params, {cancellable}));
  259. } catch (e) {
  260. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
  261. // the property may not even exist, silently ignore it
  262. Util.Logger.debug(`Error when calling 'Get(${propertyName})' ` +
  263. `in ${this.gName}, ${this.gObjectPath}, ` +
  264. `org.freedesktop.DBus.Properties, ${this.gInterfaceName} ` +
  265. `while refreshing property ${propertyName}: ${e}\n` +
  266. `${e.stack}`);
  267. this.set_cached_property(propertyName, null);
  268. this._cancellables.delete(propertyName);
  269. delete this._changedProperties[propertyName];
  270. throw e;
  271. }
  272. }
  273. }
  274. async _queuePropertyUpdate(propertyName, value, params) {
  275. params = Params.parse(params, {
  276. skipEqualityCheck: false,
  277. cancellable: null,
  278. });
  279. if (!params.skipEqualityCheck) {
  280. const cachedProperty = this.get_cached_property(propertyName);
  281. if (value && cachedProperty &&
  282. value.equal(this.get_cached_property(propertyName)))
  283. return;
  284. }
  285. this.set_cached_property(propertyName, value);
  286. // synthesize a batched property changed event
  287. this._changedProperties[propertyName] = value;
  288. if (!this._propertiesEmitTimeout || !this._propertiesEmitTimeout.pending()) {
  289. if (!params.cancellable) {
  290. params.cancellable = this._cancelRefreshProperties({
  291. propertyName,
  292. addNew: true,
  293. });
  294. }
  295. this._propertiesEmitTimeout = new PromiseUtils.TimeoutPromise(
  296. MAX_UPDATE_FREQUENCY * 2, GLib.PRIORITY_DEFAULT_IDLE, params.cancellable);
  297. await this._propertiesEmitTimeout;
  298. if (Object.keys(this._changedProperties).length) {
  299. this.emit('g-properties-changed', GLib.Variant.new('a{sv}',
  300. this._changedProperties), []);
  301. this._changedProperties = Object.create(null);
  302. }
  303. }
  304. }
  305. _cancelRefreshProperties(params) {
  306. params = Params.parse(params, {
  307. propertyName: undefined,
  308. addNew: false,
  309. });
  310. if (!this._cancellables.size && !params.addNew)
  311. return null;
  312. if (params.propertyName !== undefined) {
  313. let cancellable = this._cancellables.get(params.propertyName);
  314. if (cancellable) {
  315. cancellable.cancel();
  316. if (!params.addNew)
  317. this._cancellables.delete(params.propertyName);
  318. }
  319. if (params.addNew) {
  320. cancellable = new Util.CancellableChild(this._cancellable);
  321. this._cancellables.set(params.propertyName, cancellable);
  322. return cancellable;
  323. }
  324. } else {
  325. this._cancellables.forEach(c => c.cancel());
  326. this._cancellables.clear();
  327. this._changedProperties = Object.create(null);
  328. }
  329. return null;
  330. }
  331. });
  332. /**
  333. * the AppIndicator class serves as a generic container for indicator information and functions common
  334. * for every displaying implementation (IndicatorMessageSource and IndicatorStatusIcon)
  335. */
  336. export class AppIndicator extends Signals.EventEmitter {
  337. static get NEEDED_PROPERTIES() {
  338. return ['Id', 'Menu'];
  339. }
  340. constructor(service, busName, object) {
  341. super();
  342. this.isReady = false;
  343. this.busName = busName;
  344. this._uniqueId = Util.indicatorId(service, busName, object);
  345. this._cancellable = new Gio.Cancellable();
  346. this._proxy = new AppIndicatorProxy(busName, object);
  347. this._invalidatedPixmapsIcons = new Set();
  348. this._setupProxy().catch(logError);
  349. Util.connectSmart(this._proxy, 'g-properties-changed', this, this._onPropertiesChanged);
  350. Util.connectSmart(this._proxy, 'notify::g-name-owner', this, this._nameOwnerChanged);
  351. if (this.uniqueId === service) {
  352. this._nameWatcher = new Util.NameWatcher(service);
  353. Util.connectSmart(this._nameWatcher, 'changed', this, this._nameOwnerChanged);
  354. }
  355. }
  356. async _setupProxy() {
  357. const cancellable = this._cancellable;
  358. try {
  359. await this._proxy.initAsync(cancellable);
  360. this._checkIfReady();
  361. await this._checkNeededProperties();
  362. } catch (e) {
  363. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
  364. logError(e, `While initalizing proxy for ${this._uniqueId}`);
  365. this.destroy();
  366. }
  367. }
  368. // We try to lookup the activate method to see if the app supports it
  369. try {
  370. const introspectionVariant = await this._proxy.gConnection.call(
  371. this._proxy.gNameOwner, this._proxy.gObjectPath,
  372. 'org.freedesktop.DBus.Introspectable', 'Introspect', null, null,
  373. Gio.DBusCallFlags.NONE, -1, cancellable);
  374. const [introspectionXml] = introspectionVariant.deep_unpack();
  375. const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspectionXml);
  376. const interfaceInfo = nodeInfo.lookup_interface(this._proxy.gInterfaceName);
  377. this.supportsActivation = !!interfaceInfo.lookup_method('Activate');
  378. this._hasAyatanaSecondaryActivate =
  379. !!interfaceInfo.lookup_method('XAyatanaSecondaryActivate');
  380. } catch (e) {
  381. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
  382. Util.Logger.debug(
  383. `${this.uniqueId}, check for Activation support: ${e.message}`);
  384. }
  385. }
  386. try {
  387. this._commandLine = await Util.getProcessName(this.busName,
  388. cancellable, GLib.PRIORITY_LOW);
  389. } catch (e) {
  390. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
  391. Util.Logger.debug(
  392. `${this.uniqueId}, failed getting command line: ${e.message}`);
  393. }
  394. }
  395. }
  396. _checkIfReady() {
  397. const wasReady = this.isReady;
  398. let isReady = false;
  399. if (this.hasNameOwner && this.id && this.menuPath)
  400. isReady = true;
  401. this.isReady = isReady;
  402. if (this.isReady && !wasReady) {
  403. if (this._delayCheck) {
  404. this._delayCheck.cancel();
  405. delete this._delayCheck;
  406. }
  407. this.emit('ready');
  408. return true;
  409. }
  410. return false;
  411. }
  412. async _checkNeededProperties() {
  413. if (this.id && this.menuPath)
  414. return true;
  415. const MAX_RETRIES = 3;
  416. const cancellable = this._cancellable;
  417. for (let checks = 0; checks < MAX_RETRIES; ++checks) {
  418. this._delayCheck = new PromiseUtils.TimeoutSecondsPromise(1,
  419. GLib.PRIORITY_DEFAULT_IDLE, cancellable);
  420. // eslint-disable-next-line no-await-in-loop
  421. await this._delayCheck;
  422. try {
  423. // eslint-disable-next-line no-await-in-loop
  424. await Promise.all(AppIndicator.NEEDED_PROPERTIES.map(p =>
  425. this._proxy.refreshProperty(p)));
  426. } catch (e) {
  427. if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  428. throw e;
  429. if (checks < MAX_RETRIES - 1)
  430. continue;
  431. throw e;
  432. }
  433. if (this.id && this.menuPath)
  434. break;
  435. }
  436. return this.id && this.menuPath;
  437. }
  438. async _nameOwnerChanged() {
  439. if (!this.hasNameOwner) {
  440. this._checkIfReady();
  441. } else {
  442. try {
  443. await this._checkNeededProperties();
  444. } catch (e) {
  445. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
  446. Util.Logger.warn(`${this.uniqueId}, Impossible to get basic properties: ${e}`);
  447. this.checkAlive();
  448. }
  449. }
  450. }
  451. this.emit('name-owner-changed');
  452. }
  453. // public property getters
  454. get title() {
  455. return this._proxy.Title;
  456. }
  457. get id() {
  458. return this._proxy.Id;
  459. }
  460. get uniqueId() {
  461. return this._uniqueId;
  462. }
  463. get status() {
  464. return this._proxy.Status;
  465. }
  466. get label() {
  467. return this._proxy.XAyatanaLabel || null;
  468. }
  469. get accessibleName() {
  470. const accessibleDesc = this.status === SNIStatus.NEEDS_ATTENTION
  471. ? this._proxy.AccessibleDesc : this._proxy.IconAccessibleDesc;
  472. return accessibleDesc || this.title;
  473. }
  474. get menuPath() {
  475. if (this._proxy.Menu === '/NO_DBUSMENU')
  476. return null;
  477. return this._proxy.Menu;
  478. }
  479. get attentionIcon() {
  480. return {
  481. theme: this._proxy.IconThemePath,
  482. name: this._proxy.AttentionIconName,
  483. pixmap: this._getPixmapProperty(SNIconType.ATTENTION),
  484. };
  485. }
  486. get icon() {
  487. return {
  488. theme: this._proxy.IconThemePath,
  489. name: this._proxy.IconName,
  490. pixmap: this._getPixmapProperty(SNIconType.NORMAL),
  491. };
  492. }
  493. get overlayIcon() {
  494. return {
  495. theme: this._proxy.IconThemePath,
  496. name: this._proxy.OverlayIconName,
  497. pixmap: this._getPixmapProperty(SNIconType.OVERLAY),
  498. };
  499. }
  500. get hasOverlayIcon() {
  501. const {name, pixmap} = this.overlayIcon;
  502. return name || (pixmap && pixmap.n_children());
  503. }
  504. get hasNameOwner() {
  505. if (this._nameWatcher && !this._nameWatcher.nameOnBus)
  506. return false;
  507. return !!this._proxy.g_name_owner;
  508. }
  509. get cancellable() {
  510. return this._cancellable;
  511. }
  512. async checkAlive() {
  513. // Some applications (hey electron!) just remove the indicator object
  514. // from bus after hiding it, without closing its bus name, so we are
  515. // not able to understand whe they're gone.
  516. // Thus we just kill it when an expected well-known method is failing.
  517. if (this.status !== SNIStatus.PASSIVE && this._checkIfReady()) {
  518. if (this._checkAliveTimeout) {
  519. this._checkAliveTimeout.cancel();
  520. delete this._checkAliveTimeout;
  521. }
  522. return;
  523. }
  524. if (this._checkAliveTimeout)
  525. return;
  526. try {
  527. const cancellable = this._cancellable;
  528. this._checkAliveTimeout = new PromiseUtils.TimeoutSecondsPromise(10,
  529. GLib.PRIORITY_DEFAULT_IDLE, cancellable);
  530. Util.Logger.debug(`${this.uniqueId}: may not respond, checking...`);
  531. await this._checkAliveTimeout;
  532. // We should call the Ping method instead but in some containers
  533. // such as snaps that's not accessible, so let's just use our own
  534. await this._proxy.getProperty('Status', cancellable);
  535. } catch (e) {
  536. if (e.matches(Gio.DBusError, Gio.DBusError.NAME_HAS_NO_OWNER) ||
  537. e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN) ||
  538. e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_OBJECT) ||
  539. e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_INTERFACE) ||
  540. e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD) ||
  541. e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_PROPERTY)) {
  542. Util.Logger.warn(`${this.uniqueId}: not on bus anymore, removing it`);
  543. this.destroy();
  544. return;
  545. }
  546. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  547. logError(e);
  548. } finally {
  549. delete this._checkAliveTimeout;
  550. }
  551. }
  552. _onPropertiesChanged(_proxy, changed, _invalidated) {
  553. const props = Object.keys(changed.unpack());
  554. const signalsToEmit = new Set();
  555. const checkIfReadyChanged = () => {
  556. if (checkIfReadyChanged.value === undefined)
  557. checkIfReadyChanged.value = this._checkIfReady();
  558. return checkIfReadyChanged.value;
  559. };
  560. props.forEach(property => {
  561. // some property changes require updates on our part,
  562. // a few need to be passed down to the displaying code
  563. if (property === 'Id')
  564. checkIfReadyChanged();
  565. // all these can mean that the icon has to be changed
  566. if (property.startsWith('Icon') ||
  567. property.startsWith('AttentionIcon'))
  568. signalsToEmit.add('icon');
  569. // same for overlays
  570. if (property.startsWith('OverlayIcon'))
  571. signalsToEmit.add('overlay-icon');
  572. // this may make all of our icons invalid
  573. if (property === 'IconThemePath') {
  574. signalsToEmit.add('icon');
  575. signalsToEmit.add('overlay-icon');
  576. }
  577. // the label will be handled elsewhere
  578. if (property === 'XAyatanaLabel')
  579. signalsToEmit.add('label');
  580. if (property === 'Menu') {
  581. if (!checkIfReadyChanged() && this.isReady)
  582. signalsToEmit.add('menu');
  583. }
  584. if (property === 'IconAccessibleDesc' ||
  585. property === 'AttentionAccessibleDesc' ||
  586. property === 'Title')
  587. signalsToEmit.add('accessible-name');
  588. // status updates may cause the indicator to be hidden
  589. if (property === 'Status') {
  590. signalsToEmit.add('icon');
  591. signalsToEmit.add('overlay-icon');
  592. signalsToEmit.add('status');
  593. signalsToEmit.add('accessible-name');
  594. }
  595. });
  596. signalsToEmit.forEach(s => this.emit(s));
  597. }
  598. reset() {
  599. this.emit('reset');
  600. }
  601. destroy() {
  602. this.emit('destroy');
  603. this.disconnectAll();
  604. this._proxy.destroy();
  605. this._cancellable.cancel();
  606. this._invalidatedPixmapsIcons.clear();
  607. if (this._nameWatcher)
  608. this._nameWatcher.destroy();
  609. delete this._cancellable;
  610. delete this._proxy;
  611. delete this._nameWatcher;
  612. }
  613. _getPixmapProperty(iconType) {
  614. const propertyName = SNIconType.toPropertyName(iconType,
  615. {isPixbuf: true});
  616. const pixmap = this._proxy.get_cached_property(propertyName);
  617. const wasInvalidated = this._invalidatedPixmapsIcons.delete(iconType);
  618. if (!pixmap && wasInvalidated) {
  619. this._proxy.refreshProperty(propertyName, {
  620. skipEqualityCheck: true,
  621. }).catch(e => {
  622. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  623. logError(e);
  624. });
  625. }
  626. return pixmap;
  627. }
  628. invalidatePixmapProperty(iconType) {
  629. this._invalidatedPixmapsIcons.add(iconType);
  630. this._proxy.set_cached_property(
  631. SNIconType.toPropertyName(iconType, {isPixbuf: true}), null);
  632. }
  633. _getActivationToken(timestamp) {
  634. const launchContext = global.create_app_launch_context(timestamp, -1);
  635. const fakeAppInfo = Gio.AppInfo.create_from_commandline(
  636. this._commandLine || 'true', this.id,
  637. Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION);
  638. return [launchContext, launchContext.get_startup_notify_id(fakeAppInfo, [])];
  639. }
  640. async provideActivationToken(timestamp) {
  641. if (this._hasProvideXdgActivationToken === false)
  642. return;
  643. const [launchContext, activationToken] = this._getActivationToken(timestamp);
  644. try {
  645. await this._proxy.ProvideXdgActivationTokenAsync(activationToken,
  646. this._cancellable);
  647. this._hasProvideXdgActivationToken = true;
  648. } catch (e) {
  649. launchContext.launch_failed(activationToken);
  650. if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD))
  651. this._hasProvideXdgActivationToken = false;
  652. else
  653. Util.Logger.warn(`${this.id}, failed to provide activation token: ${e.message}`);
  654. }
  655. }
  656. async open(x, y, timestamp) {
  657. const cancellable = this._cancellable;
  658. // we can't use WindowID because we're not able to get the x11 window id from a MetaWindow
  659. // nor can we call any X11 functions. Luckily, the Activate method usually works fine.
  660. // parameters are "an hint to the item where to show eventual windows" [sic]
  661. // ... and don't seem to have any effect.
  662. try {
  663. await this.provideActivationToken(timestamp);
  664. await this._proxy.ActivateAsync(x, y, cancellable);
  665. this.supportsActivation = true;
  666. } catch (e) {
  667. if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD)) {
  668. this.supportsActivation = false;
  669. Util.Logger.warn(`${this.id}, does not support activation: ${e.message}`);
  670. return;
  671. }
  672. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  673. Util.Logger.critical(`${this.id}, failed to activate: ${e.message}`);
  674. }
  675. }
  676. async secondaryActivate(timestamp, x, y) {
  677. const cancellable = this._cancellable;
  678. try {
  679. await this.provideActivationToken(timestamp);
  680. if (this._hasAyatanaSecondaryActivate !== false) {
  681. try {
  682. await this._proxy.XAyatanaSecondaryActivateAsync(timestamp, cancellable);
  683. this._hasAyatanaSecondaryActivate = true;
  684. } catch (e) {
  685. if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD))
  686. this._hasAyatanaSecondaryActivate = false;
  687. else
  688. throw e;
  689. }
  690. }
  691. if (!this._hasAyatanaSecondaryActivate)
  692. await this._proxy.SecondaryActivateAsync(x, y, cancellable);
  693. } catch (e) {
  694. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  695. Util.Logger.critical(`${this.id}, failed to secondary activate: ${e.message}`);
  696. }
  697. }
  698. async scroll(dx, dy) {
  699. const cancellable = this._cancellable;
  700. try {
  701. const actions = [];
  702. if (dx !== 0) {
  703. actions.push(this._proxy.ScrollAsync(Math.floor(dx),
  704. 'horizontal', cancellable));
  705. }
  706. if (dy !== 0) {
  707. actions.push(this._proxy.ScrollAsync(Math.floor(dy),
  708. 'vertical', cancellable));
  709. }
  710. await Promise.all(actions);
  711. } catch (e) {
  712. Util.Logger.critical(`${this.id}, failed to scroll: ${e.message}`);
  713. }
  714. }
  715. }
  716. const StTextureCacheSkippingFileIcon = GObject.registerClass({
  717. Implements: [Gio.Icon],
  718. }, class StTextureCacheSkippingFileIconImpl extends Gio.EmblemedIcon {
  719. _init(params) {
  720. // FIXME: We can't just inherit from Gio.FileIcon for some reason
  721. super._init({gicon: new Gio.FileIcon(params)});
  722. }
  723. vfunc_to_tokens() {
  724. // Disables the to_tokens() vfunc so that the icon to_string()
  725. // method won't work and thus can't be kept forever around by
  726. // StTextureCache, see the awesome debugging session in this thread:
  727. // https://twitter.com/mild_sunrise/status/1458739604098621443
  728. // upstream bug is at:
  729. // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/4944
  730. return [false, [], 0];
  731. }
  732. });
  733. export const IconActor = GObject.registerClass(
  734. class AppIndicatorsIconActor extends St.Icon {
  735. static get DEFAULT_STYLE() {
  736. return 'padding: 0';
  737. }
  738. static get USER_WRITABLE_PATHS() {
  739. if (!this._userWritablePaths) {
  740. this._userWritablePaths = [
  741. GLib.get_user_cache_dir(),
  742. GLib.get_user_data_dir(),
  743. GLib.get_user_config_dir(),
  744. GLib.get_user_runtime_dir(),
  745. GLib.get_home_dir(),
  746. GLib.get_tmp_dir(),
  747. ];
  748. this._userWritablePaths.push(Object.values(GLib.UserDirectory).slice(
  749. 0, -1).map(dirId => GLib.get_user_special_dir(dirId)));
  750. }
  751. return this._userWritablePaths;
  752. }
  753. _init(indicator, iconSize) {
  754. super._init({
  755. reactive: true,
  756. style_class: 'system-status-icon',
  757. fallbackIconName: FALLBACK_ICON_NAME,
  758. });
  759. this.name = this.constructor.name;
  760. this.add_style_class_name('appindicator-icon');
  761. this.add_style_class_name('status-notifier-icon');
  762. this.set_style(AppIndicatorsIconActor.DEFAULT_STYLE);
  763. const themeContext = St.ThemeContext.get_for_stage(global.stage);
  764. this.height = iconSize * themeContext.scale_factor;
  765. this._indicator = indicator;
  766. this._customIcons = new Map();
  767. this._iconSize = iconSize;
  768. this._iconCache = new IconCache.IconCache();
  769. this._cancellable = new Gio.Cancellable();
  770. this._loadingIcons = Object.create(null);
  771. Object.values(SNIconType).forEach(t => (this._loadingIcons[t] = new Map()));
  772. Util.connectSmart(this._indicator, 'icon', this, () => {
  773. if (this.is_mapped())
  774. this._updateIcon();
  775. });
  776. Util.connectSmart(this._indicator, 'overlay-icon', this, () => {
  777. if (this.is_mapped())
  778. this._updateIcon();
  779. });
  780. Util.connectSmart(this._indicator, 'reset', this,
  781. () => this._invalidateIconWhenFullyReady());
  782. const settings = SettingsManager.getDefaultGSettings();
  783. Util.connectSmart(settings, 'changed::icon-size', this, () =>
  784. this._updateWhenFullyReady());
  785. Util.connectSmart(settings, 'changed::custom-icons', this, () => {
  786. this._updateCustomIcons();
  787. this._invalidateIconWhenFullyReady();
  788. });
  789. if (GObject.signal_lookup('resource-scale-changed', this))
  790. this.connect('resource-scale-changed', () => this._invalidateIcon());
  791. else
  792. this.connect('notify::resource-scale', () => this._invalidateIcon());
  793. Util.connectSmart(themeContext, 'notify::scale-factor', this, tc => {
  794. this._updateIconSize();
  795. this.height = this._iconSize * tc.scale_factor;
  796. this.width = -1;
  797. this._invalidateIcon();
  798. });
  799. Util.connectSmart(Util.getDefaultTheme(), 'changed', this,
  800. () => this._invalidateIconWhenFullyReady());
  801. this.connect('notify::mapped', () => {
  802. if (!this.is_mapped())
  803. this._updateWhenFullyReady();
  804. });
  805. this._updateWhenFullyReady();
  806. this.connect('destroy', () => {
  807. this._iconCache.destroy();
  808. this._cancellable.cancel();
  809. this._cancellable = null;
  810. this._indicator = null;
  811. this._loadingIcons = null;
  812. this._iconTheme = null;
  813. });
  814. }
  815. get debugId() {
  816. return this._indicator ? this._indicator.id : this.toString();
  817. }
  818. async _waitForFullyReady() {
  819. const waitConditions = [];
  820. if (!this.is_mapped()) {
  821. waitConditions.push(new PromiseUtils.SignalConnectionPromise(
  822. this, 'notify::mapped', this._cancellable));
  823. }
  824. if (!this._indicator.isReady) {
  825. waitConditions.push(new PromiseUtils.SignalConnectionPromise(
  826. this._indicator, 'ready', this._cancellable));
  827. }
  828. if (!waitConditions.length)
  829. return true;
  830. await Promise.all(waitConditions);
  831. return this._waitForFullyReady();
  832. }
  833. async _updateWhenFullyReady() {
  834. if (this._waitingReady)
  835. return;
  836. try {
  837. this._waitingReady = true;
  838. await this._waitForFullyReady();
  839. this._updateIconSize();
  840. this._updateIconClass();
  841. this._updateCustomIcons();
  842. this._invalidateIcon();
  843. } catch (e) {
  844. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  845. logError(e);
  846. } finally {
  847. delete this._waitingReady;
  848. }
  849. }
  850. _updateIconClass() {
  851. if (!this._indicator)
  852. return;
  853. this.add_style_class_name(
  854. `appindicator-icon-${this._indicator.id.toLowerCase().replace(/_|\s/g, '-')}`);
  855. }
  856. _cancelLoadingByType(iconType) {
  857. this._loadingIcons[iconType].forEach(c => c.cancel());
  858. this._loadingIcons[iconType].clear();
  859. }
  860. _ensureNoIconIsLoading(iconType, id) {
  861. if (this._loadingIcons[iconType].has(id)) {
  862. Util.Logger.debug(`${this.debugId}, Icon ${id} Is still loading, ignoring the request`);
  863. throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING,
  864. 'Already in progress');
  865. } else if (this._loadingIcons[iconType].size > 0) {
  866. throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS,
  867. 'Another icon is already loading');
  868. }
  869. }
  870. _getIconLoadingCancellable(iconType, loadingId) {
  871. try {
  872. this._ensureNoIconIsLoading(iconType, loadingId);
  873. } catch (e) {
  874. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
  875. throw e;
  876. this._cancelLoadingByType(iconType);
  877. }
  878. const cancellable = new Util.CancellableChild(this._cancellable);
  879. this._loadingIcons[iconType].set(loadingId, cancellable);
  880. return cancellable;
  881. }
  882. _cleanupIconLoadingCancellable(iconType, loadingId) {
  883. if (this._loadingIcons)
  884. this._loadingIcons[iconType].delete(loadingId);
  885. }
  886. _getResourceScale() {
  887. // Remove this when we remove support for versions earlier than 3.38
  888. const resourceScale = this.get_resource_scale();
  889. if (Array.isArray(resourceScale))
  890. return resourceScale[0] ? resourceScale[1] : 1.0;
  891. return resourceScale;
  892. }
  893. // Will look the icon up in the cache, if it's found
  894. // it will return it. Otherwise, it will create it and cache it.
  895. async _cacheOrCreateIconByName(iconType, iconSize, iconScaling, iconName, themePath) {
  896. const id = `${iconType}:${iconName}@${iconSize * iconScaling}:${themePath || ''}`;
  897. let gicon = this._iconCache.get(id);
  898. if (gicon)
  899. return gicon;
  900. const iconData = this._getIconData(iconName, themePath, iconSize, iconScaling);
  901. const loadingId = iconData.file ? iconData.file.get_path() : id;
  902. const cancellable = await this._getIconLoadingCancellable(iconType, id);
  903. try {
  904. gicon = await this._createIconByIconData(iconData, iconSize,
  905. iconScaling, cancellable);
  906. } finally {
  907. this._cleanupIconLoadingCancellable(iconType, loadingId);
  908. }
  909. if (gicon)
  910. gicon = this._iconCache.add(id, gicon);
  911. return gicon;
  912. }
  913. _getIconLookupFlags(themeNode) {
  914. let lookupFlags = 0;
  915. if (!themeNode)
  916. return lookupFlags;
  917. const lookupFlagsEnum = St.IconLookupFlags;
  918. const iconStyle = themeNode.get_icon_style();
  919. if (iconStyle === St.IconStyle.REGULAR)
  920. lookupFlags |= lookupFlagsEnum.FORCE_REGULAR;
  921. else if (iconStyle === St.IconStyle.SYMBOLIC)
  922. lookupFlags |= lookupFlagsEnum.FORCE_SYMBOLIC;
  923. if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
  924. lookupFlags |= lookupFlagsEnum.DIR_RTL;
  925. else
  926. lookupFlags |= lookupFlagsEnum.DIR_LTR;
  927. return lookupFlags;
  928. }
  929. async _createIconByIconData(iconData, iconSize, iconScaling, cancellable) {
  930. const {file, name} = iconData;
  931. if (!file && !name) {
  932. if (this._createIconIdle) {
  933. throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING,
  934. 'Already in progress');
  935. }
  936. try {
  937. this._createIconIdle = new PromiseUtils.IdlePromise(GLib.PRIORITY_DEFAULT_IDLE,
  938. cancellable);
  939. await this._createIconIdle;
  940. } catch (e) {
  941. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  942. logError(e);
  943. throw e;
  944. } finally {
  945. delete this._createIconIdle;
  946. }
  947. return this.gicon;
  948. } else if (this._createIconIdle) {
  949. this._createIconIdle.cancel();
  950. delete this._createIconIdle;
  951. }
  952. if (name)
  953. return new Gio.ThemedIcon({name});
  954. if (!file)
  955. throw new Error('Neither file or name are set');
  956. if (!this._isFileInWritableArea(file))
  957. return new Gio.FileIcon({file});
  958. try {
  959. const [format, width, height] = await GdkPixbuf.Pixbuf.get_file_info_async(
  960. file.get_path(), cancellable);
  961. if (!format) {
  962. Util.Logger.critical(`${this.debugId}, Invalid image format: ${file.get_path()}`);
  963. return null;
  964. }
  965. if (width >= height * 1.5) {
  966. /* Hello indicator-multiload! */
  967. await this._loadCustomImage(file,
  968. width, height, iconSize, iconScaling, cancellable);
  969. return null;
  970. } else {
  971. /* We'll wrap the icon so that it won't be cached forever by the shell */
  972. return new StTextureCacheSkippingFileIcon({file});
  973. }
  974. } catch (e) {
  975. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
  976. Util.Logger.warn(
  977. `${this.debugId}, Impossible to read image info from ` +
  978. `path '${file ? file.get_path() : null}' or name '${name}': ${e}`);
  979. }
  980. throw e;
  981. }
  982. }
  983. async _loadCustomImage(file, width, height, iconSize, iconScaling, cancellable) {
  984. const textureCache = St.TextureCache.get_default();
  985. const customImage = textureCache.load_file_async(file, -1,
  986. height, 1, iconScaling);
  987. const setCustomImageActor = imageActor => {
  988. const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
  989. const {content} = imageActor;
  990. imageActor.content = null;
  991. imageActor.destroy();
  992. this._setImageContent(content,
  993. width * scaleFactor, height * scaleFactor);
  994. };
  995. if (customImage.content) {
  996. setCustomImageActor(customImage);
  997. return;
  998. }
  999. const imageContentPromise = new PromiseUtils.SignalConnectionPromise(
  1000. customImage, 'notify::content', cancellable);
  1001. const waitPromise = new PromiseUtils.TimeoutSecondsPromise(
  1002. 1, GLib.PRIORITY_DEFAULT, cancellable);
  1003. const racingPromises = [imageContentPromise, waitPromise];
  1004. try {
  1005. await Promise.race(racingPromises);
  1006. if (!waitPromise.resolved())
  1007. setCustomImageActor(customImage);
  1008. } catch (e) {
  1009. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  1010. throw e;
  1011. } finally {
  1012. racingPromises.forEach(p => p.cancel());
  1013. }
  1014. }
  1015. _isFileInWritableArea(file) {
  1016. // No need to use IO here, we can just do some assumptions
  1017. // print('Writable paths', IconActor.USER_WRITABLE_PATHS)
  1018. const path = file.get_path();
  1019. return IconActor.USER_WRITABLE_PATHS.some(writablePath =>
  1020. path.startsWith(writablePath));
  1021. }
  1022. _createIconTheme(searchPath = []) {
  1023. const iconTheme = new St.IconTheme();
  1024. iconTheme.set_search_path(searchPath);
  1025. return iconTheme;
  1026. }
  1027. _getIconData(name, themePath, size, scale) {
  1028. const emptyIconData = {iconInfo: null, file: null, name: null};
  1029. if (!name) {
  1030. delete this._iconTheme;
  1031. return emptyIconData;
  1032. }
  1033. // HACK: icon is a path name. This is not specified by the API,
  1034. // but at least indicator-sensors uses it.
  1035. if (name[0] === '/') {
  1036. delete this._iconTheme;
  1037. const file = Gio.File.new_for_path(name);
  1038. return {file, iconInfo: null, name: null};
  1039. }
  1040. if (name.includes('.')) {
  1041. const splits = name.split('.');
  1042. if (['svg', 'png'].includes(splits[splits.length - 1]))
  1043. name = splits.slice(0, -1).join('');
  1044. }
  1045. if (themePath && Util.getDefaultTheme().get_search_path().includes(themePath))
  1046. themePath = null;
  1047. if (themePath) {
  1048. // If a theme path is provided, we need to lookup the icon ourself
  1049. // as St won't be able to do it unless we mess with default theme
  1050. // that is something we prefer not to do, as it would imply lots of
  1051. // St.TextureCache cleanups.
  1052. const newSearchPath = [themePath];
  1053. if (!this._iconTheme) {
  1054. this._iconTheme = this._createIconTheme(newSearchPath);
  1055. } else {
  1056. const currentSearchPath = this._iconTheme.get_search_path();
  1057. if (!currentSearchPath.includes(newSearchPath))
  1058. this._iconTheme.set_search_path(newSearchPath);
  1059. }
  1060. // try to look up the icon in the icon theme
  1061. const iconInfo = this._iconTheme.lookup_icon_for_scale(`${name}`,
  1062. size, scale, this._getIconLookupFlags(this.get_theme_node()) |
  1063. St.IconLookupFlags.GENERIC_FALLBACK);
  1064. if (iconInfo) {
  1065. return {
  1066. iconInfo,
  1067. file: Gio.File.new_for_path(iconInfo.get_filename()),
  1068. name: null,
  1069. };
  1070. }
  1071. const logger = this.gicon ? Util.Logger.debug : Util.Logger.warn;
  1072. logger(`${this.debugId}, Impossible to lookup icon ` +
  1073. `for '${name}' in ${themePath}`);
  1074. return emptyIconData;
  1075. }
  1076. delete this._iconTheme;
  1077. return {name, iconInfo: null, file: null};
  1078. }
  1079. _setImageContent(content, width, height) {
  1080. this.set({
  1081. content,
  1082. width,
  1083. height,
  1084. contentGravity: Clutter.ContentGravity.RESIZE_ASPECT,
  1085. fallbackIconName: null,
  1086. });
  1087. }
  1088. async _createIconFromPixmap(iconType, iconSize, iconScaling, scaleFactor, pixmapsVariant) {
  1089. const {pixmapVariant, width, height, rowStride} =
  1090. PixmapsUtils.getBestPixmap(pixmapsVariant, iconSize * iconScaling);
  1091. const id = `__PIXMAP_ICON_${width}x${height}`;
  1092. const imageContent = new St.ImageContent({
  1093. preferredWidth: width,
  1094. preferredHeight: height,
  1095. });
  1096. // Remove this dynamic check when we depend on GNOME 48.
  1097. const coglContext = [];
  1098. const mutterBackend = global.stage?.context?.get_backend?.();
  1099. if (imageContent.set_bytes.length === 6 && mutterBackend?.get_cogl_context)
  1100. coglContext.push(mutterBackend.get_cogl_context());
  1101. imageContent.set_bytes(...coglContext, pixmapVariant.get_data_as_bytes(),
  1102. PIXMAPS_FORMAT, width, height, rowStride);
  1103. if (iconType !== SNIconType.OVERLAY && !this._indicator.hasOverlayIcon) {
  1104. const scaledSize = iconSize * scaleFactor;
  1105. this._setImageContent(imageContent, scaledSize, scaledSize);
  1106. return null;
  1107. }
  1108. const cancellable = this._getIconLoadingCancellable(iconType, id);
  1109. try {
  1110. // FIXME: async API results in a gray icon for some reason
  1111. const [inputStream] = imageContent.load(iconSize, cancellable);
  1112. return await GdkPixbuf.Pixbuf.new_from_stream_at_scale_async(
  1113. inputStream, -1, iconSize * iconScaling, true, cancellable);
  1114. } catch (e) {
  1115. // the image data was probably bogus. We don't really know why, but it _does_ happen.
  1116. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  1117. Util.Logger.warn(`${this.debugId}, Impossible to create image from data: ${e}`);
  1118. throw e;
  1119. } finally {
  1120. this._cleanupIconLoadingCancellable(iconType, id);
  1121. }
  1122. }
  1123. // The icon cache Active flag will be set to true if the used gicon matches
  1124. // the cached one (as in some cases it may be equal, but not the same object).
  1125. // So when it's not need anymore we make sure to check the active state
  1126. // and set it to false so that it can be picked up by the garbage collector.
  1127. _setGicon(iconType, gicon) {
  1128. if (iconType !== SNIconType.OVERLAY) {
  1129. if (gicon) {
  1130. if (this.gicon === gicon ||
  1131. (this.gicon && this.gicon.get_icon() === gicon))
  1132. return;
  1133. if (gicon instanceof Gio.EmblemedIcon)
  1134. this.gicon = gicon;
  1135. else
  1136. this.gicon = new Gio.EmblemedIcon({gicon});
  1137. this._iconCache.updateActive(SNIconType.NORMAL, gicon,
  1138. this.gicon.get_icon() === gicon);
  1139. } else {
  1140. this.gicon = null;
  1141. }
  1142. } else if (gicon) {
  1143. this._emblem = new Gio.Emblem({icon: gicon});
  1144. this._iconCache.updateActive(iconType, gicon, true);
  1145. } else {
  1146. this._emblem = null;
  1147. }
  1148. if (this.gicon) {
  1149. if (!this.gicon.get_emblems().some(e => e.equal(this._emblem))) {
  1150. this.gicon.clear_emblems();
  1151. if (this._emblem)
  1152. this.gicon.add_emblem(this._emblem);
  1153. }
  1154. }
  1155. }
  1156. async _updateIconByType(iconType, iconSize) {
  1157. let icon;
  1158. switch (iconType) {
  1159. case SNIconType.ATTENTION:
  1160. icon = this._indicator.attentionIcon;
  1161. break;
  1162. case SNIconType.NORMAL:
  1163. ({icon} = this._indicator);
  1164. break;
  1165. case SNIconType.OVERLAY:
  1166. icon = this._indicator.overlayIcon;
  1167. break;
  1168. }
  1169. const {theme, name, pixmap} = icon;
  1170. const commonArgs = [theme, iconType, iconSize];
  1171. if (this._customIcons.size) {
  1172. let customIcon = this._customIcons.get(iconType);
  1173. if (!await this._createAndSetIcon(customIcon, null, ...commonArgs)) {
  1174. if (iconType !== SNIconType.OVERLAY) {
  1175. customIcon = this._customIcons.get(SNIconType.NORMAL);
  1176. await this._createAndSetIcon(customIcon, null, ...commonArgs);
  1177. }
  1178. }
  1179. } else {
  1180. await this._createAndSetIcon(name, pixmap, ...commonArgs);
  1181. }
  1182. }
  1183. async _createAndSetIcon(name, pixmap, theme, iconType, iconSize) {
  1184. let gicon = null;
  1185. try {
  1186. gicon = await this._createIcon(name, pixmap, theme, iconType, iconSize);
  1187. } catch (e) {
  1188. if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) ||
  1189. e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING))
  1190. return null;
  1191. if (iconType === SNIconType.OVERLAY) {
  1192. logError(e, `${this.debugId} unable to update icon emblem`);
  1193. } else {
  1194. this.fallbackIconName = FALLBACK_ICON_NAME;
  1195. logError(e, `${this.debugId} unable to update icon`);
  1196. }
  1197. }
  1198. try {
  1199. this._setGicon(iconType, gicon);
  1200. if (pixmap && this.gicon) {
  1201. // The pixmap has been saved, we can free the variants memory
  1202. this._indicator.invalidatePixmapProperty(iconType);
  1203. }
  1204. return gicon;
  1205. } catch (e) {
  1206. logError(e, 'Setting GIcon failed');
  1207. return null;
  1208. }
  1209. }
  1210. // updates the base icon
  1211. async _createIcon(name, pixmap, theme, iconType, iconSize) {
  1212. const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
  1213. const resourceScale = this._getResourceScale();
  1214. const iconScaling = Math.ceil(resourceScale * scaleFactor);
  1215. // From now on we consider them the same thing, as one replaces the other
  1216. if (iconType === SNIconType.ATTENTION)
  1217. iconType = SNIconType.NORMAL;
  1218. if (name) {
  1219. const gicon = await this._cacheOrCreateIconByName(
  1220. iconType, iconSize, iconScaling, name, theme);
  1221. if (gicon)
  1222. return gicon;
  1223. }
  1224. if (pixmap && pixmap.n_children()) {
  1225. return this._createIconFromPixmap(iconType,
  1226. iconSize, iconScaling, scaleFactor, pixmap);
  1227. }
  1228. return null;
  1229. }
  1230. // updates the base icon
  1231. async _updateIcon() {
  1232. if (this._indicator.status === SNIStatus.PASSIVE)
  1233. return;
  1234. if (this.gicon instanceof Gio.EmblemedIcon) {
  1235. const {gicon} = this.gicon;
  1236. this._iconCache.updateActive(SNIconType.NORMAL, gicon, false);
  1237. }
  1238. // we might need to use the AttentionIcon*, which have precedence over the normal icons
  1239. const iconType = this._indicator.status === SNIStatus.NEEDS_ATTENTION
  1240. ? SNIconType.ATTENTION : SNIconType.NORMAL;
  1241. try {
  1242. await this._updateIconByType(iconType, this._iconSize);
  1243. } catch (e) {
  1244. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) &&
  1245. !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING))
  1246. logError(e, `${this.debugId}: Updating icon type ${iconType} failed`);
  1247. }
  1248. }
  1249. async _updateOverlayIcon() {
  1250. if (this._indicator.status === SNIStatus.PASSIVE)
  1251. return;
  1252. if (this._emblem) {
  1253. const {icon} = this._emblem;
  1254. this._iconCache.updateActive(SNIconType.OVERLAY, icon, false);
  1255. }
  1256. // KDE hardcodes the overlay icon size to 10px (normal icon size 16px)
  1257. // we approximate that ratio for other sizes, too.
  1258. // our algorithms will always pick a smaller one instead of stretching it.
  1259. const iconSize = Math.floor(this._iconSize / 1.6);
  1260. try {
  1261. await this._updateIconByType(SNIconType.OVERLAY, iconSize);
  1262. } catch (e) {
  1263. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) &&
  1264. !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING))
  1265. logError(e, `${this.debugId}: Updating overlay icon failed`);
  1266. }
  1267. }
  1268. async _invalidateIconWhenFullyReady() {
  1269. if (this._waitingInvalidation)
  1270. return;
  1271. try {
  1272. this._waitingInvalidation = true;
  1273. await this._waitForFullyReady();
  1274. this._invalidateIcon();
  1275. } catch (e) {
  1276. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  1277. logError(e);
  1278. } finally {
  1279. delete this._waitingInvalidation;
  1280. }
  1281. }
  1282. // called when the icon theme changes
  1283. _invalidateIcon() {
  1284. this._iconCache.clear();
  1285. this._cancellable.cancel();
  1286. this._cancellable = new Gio.Cancellable();
  1287. Object.values(SNIconType).forEach(iconType =>
  1288. this._loadingIcons[iconType].clear());
  1289. this._updateIcon().catch(e => logError(e));
  1290. this._updateOverlayIcon().catch(e => logError(e));
  1291. }
  1292. _updateIconSize() {
  1293. const settings = SettingsManager.getDefaultGSettings();
  1294. const sizeValue = settings.get_int('icon-size');
  1295. if (sizeValue > 0) {
  1296. if (!this._defaultIconSize)
  1297. this._defaultIconSize = this._iconSize;
  1298. this._iconSize = sizeValue;
  1299. } else if (this._defaultIconSize) {
  1300. this._iconSize = this._defaultIconSize;
  1301. delete this._defaultIconSize;
  1302. }
  1303. const themeIconSize = Math.round(
  1304. this.get_theme_node().get_length('icon-size'));
  1305. let iconStyle = AppIndicatorsIconActor.DEFAULT_STYLE;
  1306. if (themeIconSize > 0) {
  1307. const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
  1308. if (themeIconSize / scaleFactor !== this._iconSize) {
  1309. iconStyle = `${AppIndicatorsIconActor.DEFAULT_STYLE};` +
  1310. 'icon-size: 0';
  1311. }
  1312. }
  1313. this.set_style(iconStyle);
  1314. this.set_icon_size(this._iconSize);
  1315. }
  1316. _updateCustomIcons() {
  1317. const settings = SettingsManager.getDefaultGSettings();
  1318. this._customIcons.clear();
  1319. settings.get_value('custom-icons').deep_unpack().forEach(customIcons => {
  1320. const [indicatorId, normalIcon, attentionIcon] = customIcons;
  1321. if (this._indicator.id === indicatorId) {
  1322. this._customIcons.set(SNIconType.NORMAL, normalIcon);
  1323. this._customIcons.set(SNIconType.ATTENTION, attentionIcon);
  1324. }
  1325. });
  1326. }
  1327. });