appIndicator.js 54 KB

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