appIndicator.js 53 KB

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