summaryrefslogtreecommitdiff
path: root/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js
diff options
context:
space:
mode:
authornippy <you@example.com>2026-04-18 13:49:56 +0200
committernippy <you@example.com>2026-04-18 13:49:56 +0200
commit5d94c0a7d44a2255b81815a52a7056a94a39842d (patch)
tree759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js
parente79cdf210b267f21a186255ce1a4d50029439d54 (diff)
downloadRaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz
RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip
update Raveos themes
Diffstat (limited to 'raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js')
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js1607
1 files changed, 1607 insertions, 0 deletions
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js
new file mode 100644
index 0000000..7efded2
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js
@@ -0,0 +1,1607 @@
+// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import GdkPixbuf from 'gi://GdkPixbuf';
+import Gio from 'gi://Gio';
+import St from 'gi://St';
+
+import * as Params from 'resource:///org/gnome/shell/misc/params.js';
+import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
+
+import * as DBusUtils from './dbusUtils.js';
+import * as IconCache from './iconCache.js';
+import * as Util from './util.js';
+import * as Interfaces from './interfaces.js';
+import * as PixmapsUtils from './pixmapsUtils.js';
+import * as PromiseUtils from './promiseUtils.js';
+import * as SettingsManager from './settingsManager.js';
+import {DBusProxy} from './dbusProxy.js';
+
+Gio._promisify(Gio.File.prototype, 'read_async');
+Gio._promisify(GdkPixbuf.Pixbuf, 'get_file_info_async');
+Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_at_scale_async',
+ 'new_from_stream_finish');
+Gio._promisify(St.IconInfo.prototype, 'load_symbolic_async');
+Gio._promisify(Gio.DBusConnection.prototype, 'call');
+
+const MAX_UPDATE_FREQUENCY = 30; // In ms
+const FALLBACK_ICON_NAME = 'image-loading-symbolic';
+const PIXMAPS_FORMAT = imports.gi.Cogl.PixelFormat.ARGB_8888;
+
+export const SNICategory = Object.freeze({
+ APPLICATION: 'ApplicationStatus',
+ COMMUNICATIONS: 'Communications',
+ SYSTEM: 'SystemServices',
+ HARDWARE: 'Hardware',
+});
+
+export const SNIStatus = Object.freeze({
+ PASSIVE: 'Passive',
+ ACTIVE: 'Active',
+ NEEDS_ATTENTION: 'NeedsAttention',
+});
+
+const SNIconType = Object.freeze({
+ NORMAL: 0,
+ ATTENTION: 1,
+ OVERLAY: 2,
+
+ toPropertyName: (iconType, params = {isPixbuf: false}) => {
+ let propertyName = 'Icon';
+
+ if (iconType === SNIconType.OVERLAY)
+ propertyName = 'OverlayIcon';
+ else if (iconType === SNIconType.ATTENTION)
+ propertyName = 'AttentionIcon';
+
+ return `${propertyName}${params.isPixbuf ? 'Pixmap' : 'Name'}`;
+ },
+});
+
+export const AppIndicatorProxy = GObject.registerClass(
+class AppIndicatorProxy extends DBusProxy {
+ static get interfaceInfo() {
+ if (!this._interfaceInfo) {
+ this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(
+ Interfaces.StatusNotifierItem);
+ }
+ return this._interfaceInfo;
+ }
+
+ static get OPTIONAL_PROPERTIES() {
+ return [
+ 'XAyatanaLabel',
+ 'XAyatanaLabelGuide',
+ 'XAyatanaOrderingIndex',
+ 'IconAccessibleDesc',
+ 'AttentionAccessibleDesc',
+ ];
+ }
+
+ static get TUPLE_TYPE() {
+ if (!this._tupleType)
+ this._tupleType = new GLib.VariantType('()');
+
+ return this._tupleType;
+ }
+
+ static destroy() {
+ delete this._interfaceInfo;
+ delete this._tupleType;
+ }
+
+ _init(busName, objectPath) {
+ const {interfaceInfo} = AppIndicatorProxy;
+
+ super._init(busName, objectPath, interfaceInfo,
+ Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES);
+
+ this.set_cached_property('Status',
+ new GLib.Variant('s', SNIStatus.PASSIVE));
+
+
+ this._accumulatedProperties = new Set();
+ this._cancellables = new Map();
+ this._changedProperties = Object.create(null);
+ }
+
+ async initAsync(cancellable) {
+ await super.initAsync(cancellable);
+
+ this._setupProxyPropertyList();
+ }
+
+ destroy() {
+ const cachedProperties = this.get_cached_property_names();
+ if (cachedProperties) {
+ cachedProperties.forEach(propertyName =>
+ this.set_cached_property(propertyName, null));
+ }
+
+ super.destroy();
+ }
+
+ _onNameOwnerChanged() {
+ this._resetNeededProperties();
+
+ if (!this.gNameOwner)
+ this._cancelRefreshProperties();
+ else
+ this._setupProxyPropertyList();
+ }
+
+ _setupProxyPropertyList() {
+ this._propertiesList =
+ (this.get_cached_property_names() || []).filter(p =>
+ this.gInterfaceInfo.properties.some(pInfo => pInfo.name === p));
+
+ if (this._propertiesList.length) {
+ AppIndicatorProxy.OPTIONAL_PROPERTIES.forEach(
+ p => this._addExtraProperty(p));
+ }
+ }
+
+ _addExtraProperty(name) {
+ if (this._propertiesList.includes(name))
+ return;
+
+ if (!(name in this)) {
+ Object.defineProperty(this, name, {
+ configurable: false,
+ enumerable: true,
+ get: () => {
+ const v = this.get_cached_property(name);
+ return v ? v.deep_unpack() : null;
+ },
+ });
+ }
+
+ this._propertiesList.push(name);
+ }
+
+ _signalToPropertyName(signal) {
+ if (signal.startsWith('New'))
+ return signal.substr(3);
+ else if (signal.startsWith('XAyatanaNew'))
+ return `XAyatana${signal.substr(11)}`;
+
+ return null;
+ }
+
+ // The Author of the spec didn't like the PropertiesChanged signal, so he invented his own
+ async _refreshOwnProperties(prop) {
+ await Promise.all(
+ [prop, `${prop}Name`, `${prop}Pixmap`, `${prop}AccessibleDesc`].filter(p =>
+ this._propertiesList.includes(p)).map(async p => {
+ try {
+ await this.refreshProperty(p, {
+ skipEqualityCheck: p.endsWith('Pixmap'),
+ });
+ } catch (e) {
+ if (!AppIndicatorProxy.OPTIONAL_PROPERTIES.includes(p) ||
+ !(e instanceof Gio.DBusError))
+ logError(e);
+ }
+ }));
+ }
+
+ _onSignal(sender, signal, ...args) {
+ this._onSignalAsync(sender, signal, ...args).catch(e => {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e, `Error while processing signal '${signal}'`);
+ });
+ }
+
+ async _onSignalAsync(_sender, signal, params) {
+ const property = this._signalToPropertyName(signal);
+ if (!property)
+ return;
+
+ if (this.status === SNIStatus.PASSIVE &&
+ ![...AppIndicator.NEEDED_PROPERTIES, 'Status'].includes(property)) {
+ this._accumulatedProperties.add(property);
+ return;
+ }
+
+ const cancellable = this._cancellable;
+
+ if (!params.get_type().equal(AppIndicatorProxy.TUPLE_TYPE)) {
+ // If the property includes arguments, we can just queue the signal emission
+ const [value] = params.unpack();
+ try {
+ await this._queuePropertyUpdate(property, value);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ throw e;
+ }
+
+ if (!this._accumulatedProperties.size)
+ return;
+ } else {
+ this._accumulatedProperties.add(property);
+ }
+
+ if (this._signalsAccumulator)
+ return;
+
+ this._signalsAccumulator = new PromiseUtils.TimeoutPromise(
+ MAX_UPDATE_FREQUENCY, GLib.PRIORITY_DEFAULT_IDLE, cancellable);
+ try {
+ await this._signalsAccumulator;
+ const refreshPropertiesPromises =
+ [...this._accumulatedProperties].map(p =>
+ this._refreshOwnProperties(p));
+ this._accumulatedProperties.clear();
+ await Promise.all(refreshPropertiesPromises);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ throw e;
+ } finally {
+ delete this._signalsAccumulator;
+ }
+ }
+
+ _resetNeededProperties() {
+ AppIndicator.NEEDED_PROPERTIES.forEach(p =>
+ this.set_cached_property(p, null));
+ }
+
+ async refreshAllProperties() {
+ const cancellableName = 'org.freedesktop.DBus.Properties.GetAll';
+ const cancellable = this._cancelRefreshProperties({
+ propertyName: cancellableName,
+ addNew: true,
+ });
+
+ try {
+ const [valuesVariant] = (await this.getProperties(
+ cancellable)).deep_unpack();
+
+ this._cancellables.delete(cancellableName);
+
+ await Promise.all(
+ Object.entries(valuesVariant).map(([propertyName, valueVariant]) =>
+ this._queuePropertyUpdate(propertyName, valueVariant, {
+ skipEqualityCheck: true,
+ cancellable,
+ })));
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ // the property may not even exist, silently ignore it
+ Util.Logger.debug(`While refreshing all properties: ${e}`);
+
+ this.get_cached_property_names().forEach(propertyName =>
+ this.set_cached_property(propertyName, null));
+
+ this._cancellables.delete(cancellableName);
+ throw e;
+ }
+ }
+ }
+
+ async refreshProperty(propertyName, params) {
+ params = Params.parse(params, {
+ skipEqualityCheck: false,
+ });
+
+ const cancellable = this._cancelRefreshProperties({
+ propertyName,
+ addNew: true,
+ });
+
+ try {
+ const [valueVariant] = (await this.getProperty(
+ propertyName, cancellable)).deep_unpack();
+
+ this._cancellables.delete(propertyName);
+ await this._queuePropertyUpdate(propertyName, valueVariant,
+ Object.assign(params, {cancellable}));
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ // the property may not even exist, silently ignore it
+ Util.Logger.debug(`Error when calling 'Get(${propertyName})' ` +
+ `in ${this.gName}, ${this.gObjectPath}, ` +
+ `org.freedesktop.DBus.Properties, ${this.gInterfaceName} ` +
+ `while refreshing property ${propertyName}: ${e}\n` +
+ `${e.stack}`);
+ this.set_cached_property(propertyName, null);
+ this._cancellables.delete(propertyName);
+ delete this._changedProperties[propertyName];
+ throw e;
+ }
+ }
+ }
+
+ async _queuePropertyUpdate(propertyName, value, params) {
+ params = Params.parse(params, {
+ skipEqualityCheck: false,
+ cancellable: null,
+ });
+
+ if (!params.skipEqualityCheck) {
+ const cachedProperty = this.get_cached_property(propertyName);
+
+ if (value && cachedProperty &&
+ value.equal(this.get_cached_property(propertyName)))
+ return;
+ }
+
+ this.set_cached_property(propertyName, value);
+
+ // synthesize a batched property changed event
+ this._changedProperties[propertyName] = value;
+
+ if (!this._propertiesEmitTimeout || !this._propertiesEmitTimeout.pending()) {
+ if (!params.cancellable) {
+ params.cancellable = this._cancelRefreshProperties({
+ propertyName,
+ addNew: true,
+ });
+ }
+ this._propertiesEmitTimeout = new PromiseUtils.TimeoutPromise(
+ MAX_UPDATE_FREQUENCY * 2, GLib.PRIORITY_DEFAULT_IDLE, params.cancellable);
+ await this._propertiesEmitTimeout;
+
+ if (Object.keys(this._changedProperties).length) {
+ this.emit('g-properties-changed', GLib.Variant.new('a{sv}',
+ this._changedProperties), []);
+ this._changedProperties = Object.create(null);
+ }
+ }
+ }
+
+ _cancelRefreshProperties(params) {
+ params = Params.parse(params, {
+ propertyName: undefined,
+ addNew: false,
+ });
+
+ if (!this._cancellables.size && !params.addNew)
+ return null;
+
+ if (params.propertyName !== undefined) {
+ let cancellable = this._cancellables.get(params.propertyName);
+ if (cancellable) {
+ cancellable.cancel();
+
+ if (!params.addNew)
+ this._cancellables.delete(params.propertyName);
+ }
+
+ if (params.addNew) {
+ cancellable = new Util.CancellableChild(this._cancellable);
+ this._cancellables.set(params.propertyName, cancellable);
+ return cancellable;
+ }
+ } else {
+ this._cancellables.forEach(c => c.cancel());
+ this._cancellables.clear();
+ this._changedProperties = Object.create(null);
+ }
+
+ return null;
+ }
+});
+
+/**
+ * the AppIndicator class serves as a generic container for indicator information and functions common
+ * for every displaying implementation (IndicatorMessageSource and IndicatorStatusIcon)
+ */
+export class AppIndicator extends Signals.EventEmitter {
+ static get NEEDED_PROPERTIES() {
+ return ['Id', 'Menu'];
+ }
+
+ constructor(service, busName, object) {
+ super();
+
+ this.isReady = false;
+ this.busName = busName;
+ this._uniqueId = Util.indicatorId(service, busName, object);
+
+ this._cancellable = new Gio.Cancellable();
+ this._proxy = new AppIndicatorProxy(busName, object);
+ this._invalidatedPixmapsIcons = new Set();
+
+ this._setupProxy().catch(logError);
+ Util.connectSmart(this._proxy, 'g-properties-changed', this, this._onPropertiesChanged);
+ Util.connectSmart(this._proxy, 'notify::g-name-owner', this, this._nameOwnerChanged);
+
+ if (this.uniqueId === service) {
+ this._nameWatcher = new Util.NameWatcher(service);
+ Util.connectSmart(this._nameWatcher, 'changed', this, this._nameOwnerChanged);
+ }
+ }
+
+ async _setupProxy() {
+ const cancellable = this._cancellable;
+
+ try {
+ await this._proxy.initAsync(cancellable);
+ this._checkIfReady();
+ await this._checkNeededProperties();
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ logError(e, `While initalizing proxy for ${this._uniqueId}`);
+ this.destroy();
+ }
+ }
+
+ // We try to lookup the activate method to see if the app supports it
+ try {
+ const introspectionVariant = await this._proxy.gConnection.call(
+ this._proxy.gNameOwner, this._proxy.gObjectPath,
+ 'org.freedesktop.DBus.Introspectable', 'Introspect', null, null,
+ Gio.DBusCallFlags.NONE, -1, cancellable);
+ const [introspectionXml] = introspectionVariant.deep_unpack();
+ const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspectionXml);
+ const interfaceInfo = nodeInfo.lookup_interface(this._proxy.gInterfaceName);
+ this.supportsActivation = !!interfaceInfo.lookup_method('Activate');
+ this._hasAyatanaSecondaryActivate =
+ !!interfaceInfo.lookup_method('XAyatanaSecondaryActivate');
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ Util.Logger.debug(
+ `${this.uniqueId}, check for Activation support: ${e.message}`);
+ }
+ }
+
+ try {
+ this._commandLine = await DBusUtils.getProcessName(this.busName,
+ cancellable, GLib.PRIORITY_LOW);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ Util.Logger.debug(
+ `${this.uniqueId}, failed getting command line: ${e.message}`);
+ }
+ }
+ }
+
+ _checkIfReady() {
+ const wasReady = this.isReady;
+ let isReady = false;
+
+ if (this.hasNameOwner && this.id && this.menuPath)
+ isReady = true;
+
+ this.isReady = isReady;
+
+ if (this.isReady && !wasReady) {
+ if (this._delayCheck) {
+ this._delayCheck.cancel();
+ delete this._delayCheck;
+ }
+
+ this.emit('ready');
+ return true;
+ }
+
+ return false;
+ }
+
+ async _checkNeededProperties() {
+ if (this.id && this.menuPath)
+ return true;
+
+ const MAX_RETRIES = 3;
+ const cancellable = this._cancellable;
+ for (let checks = 0; checks < MAX_RETRIES; ++checks) {
+ this._delayCheck = new PromiseUtils.TimeoutSecondsPromise(1,
+ GLib.PRIORITY_DEFAULT_IDLE, cancellable);
+ // eslint-disable-next-line no-await-in-loop
+ await this._delayCheck;
+
+ try {
+ // eslint-disable-next-line no-await-in-loop
+ await Promise.all(AppIndicator.NEEDED_PROPERTIES.map(p =>
+ this._proxy.refreshProperty(p)));
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ throw e;
+
+ if (checks < MAX_RETRIES - 1)
+ continue;
+
+ throw e;
+ }
+
+ if (this.id && this.menuPath)
+ break;
+ }
+
+ return this.id && this.menuPath;
+ }
+
+ async _nameOwnerChanged() {
+ if (!this.hasNameOwner) {
+ this._checkIfReady();
+ } else {
+ try {
+ await this._checkNeededProperties();
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ Util.Logger.warn(`${this.uniqueId}, Impossible to get basic properties: ${e}`);
+ this.checkAlive();
+ }
+ }
+ }
+
+ this.emit('name-owner-changed');
+ }
+
+ // public property getters
+ get title() {
+ return this._proxy.Title;
+ }
+
+ get id() {
+ return this._proxy.Id;
+ }
+
+ get uniqueId() {
+ return this._uniqueId;
+ }
+
+ get status() {
+ return this._proxy.Status;
+ }
+
+ get label() {
+ return this._proxy.XAyatanaLabel || null;
+ }
+
+ get accessibleName() {
+ const accessibleDesc = this.status === SNIStatus.NEEDS_ATTENTION
+ ? this._proxy.AccessibleDesc : this._proxy.IconAccessibleDesc;
+
+ return accessibleDesc || this.title;
+ }
+
+ get menuPath() {
+ if (this._proxy.Menu === '/NO_DBUSMENU')
+ return null;
+
+ return this._proxy.Menu;
+ }
+
+ get attentionIcon() {
+ return {
+ theme: this._proxy.IconThemePath,
+ name: this._proxy.AttentionIconName,
+ pixmap: this._getPixmapProperty(SNIconType.ATTENTION),
+ };
+ }
+
+ get icon() {
+ return {
+ theme: this._proxy.IconThemePath,
+ name: this._proxy.IconName,
+ pixmap: this._getPixmapProperty(SNIconType.NORMAL),
+ };
+ }
+
+ get overlayIcon() {
+ return {
+ theme: this._proxy.IconThemePath,
+ name: this._proxy.OverlayIconName,
+ pixmap: this._getPixmapProperty(SNIconType.OVERLAY),
+ };
+ }
+
+ get hasOverlayIcon() {
+ const {name, pixmap} = this.overlayIcon;
+
+ return name || (pixmap && pixmap.n_children());
+ }
+
+ get hasNameOwner() {
+ if (this._nameWatcher && !this._nameWatcher.nameOnBus)
+ return false;
+ return !!this._proxy.g_name_owner;
+ }
+
+ get cancellable() {
+ return this._cancellable;
+ }
+
+ async checkAlive() {
+ // Some applications (hey electron!) just remove the indicator object
+ // from bus after hiding it, without closing its bus name, so we are
+ // not able to understand whe they're gone.
+ // Thus we just kill it when an expected well-known method is failing.
+ if (this.status !== SNIStatus.PASSIVE && this._checkIfReady()) {
+ if (this._checkAliveTimeout) {
+ this._checkAliveTimeout.cancel();
+ delete this._checkAliveTimeout;
+ }
+ return;
+ }
+
+ if (this._checkAliveTimeout)
+ return;
+
+ try {
+ const cancellable = this._cancellable;
+ this._checkAliveTimeout = new PromiseUtils.TimeoutSecondsPromise(10,
+ GLib.PRIORITY_DEFAULT_IDLE, cancellable);
+ Util.Logger.debug(`${this.uniqueId}: may not respond, checking...`);
+ await this._checkAliveTimeout;
+
+ // We should call the Ping method instead but in some containers
+ // such as snaps that's not accessible, so let's just use our own
+ await this._proxy.getProperty('Status', cancellable);
+ } catch (e) {
+ if (e.matches(Gio.DBusError, Gio.DBusError.NAME_HAS_NO_OWNER) ||
+ e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN) ||
+ e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_OBJECT) ||
+ e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_INTERFACE) ||
+ e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD) ||
+ e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_PROPERTY)) {
+ Util.Logger.warn(`${this.uniqueId}: not on bus anymore, removing it`);
+ this.destroy();
+ return;
+ }
+
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ } finally {
+ delete this._checkAliveTimeout;
+ }
+ }
+
+ _onPropertiesChanged(_proxy, changed, _invalidated) {
+ const props = Object.keys(changed.unpack());
+ const signalsToEmit = new Set();
+ const checkIfReadyChanged = () => {
+ if (checkIfReadyChanged.value === undefined)
+ checkIfReadyChanged.value = this._checkIfReady();
+ return checkIfReadyChanged.value;
+ };
+
+ props.forEach(property => {
+ // some property changes require updates on our part,
+ // a few need to be passed down to the displaying code
+ if (property === 'Id')
+ checkIfReadyChanged();
+
+ // all these can mean that the icon has to be changed
+ if (property.startsWith('Icon') ||
+ property.startsWith('AttentionIcon'))
+ signalsToEmit.add('icon');
+
+ // same for overlays
+ if (property.startsWith('OverlayIcon'))
+ signalsToEmit.add('overlay-icon');
+
+ // this may make all of our icons invalid
+ if (property === 'IconThemePath') {
+ signalsToEmit.add('icon');
+ signalsToEmit.add('overlay-icon');
+ }
+
+ // the label will be handled elsewhere
+ if (property === 'XAyatanaLabel')
+ signalsToEmit.add('label');
+
+ if (property === 'Menu') {
+ if (!checkIfReadyChanged() && this.isReady)
+ signalsToEmit.add('menu');
+ }
+
+ if (property === 'IconAccessibleDesc' ||
+ property === 'AttentionAccessibleDesc' ||
+ property === 'Title')
+ signalsToEmit.add('accessible-name');
+
+ // status updates may cause the indicator to be hidden
+ if (property === 'Status') {
+ signalsToEmit.add('icon');
+ signalsToEmit.add('overlay-icon');
+ signalsToEmit.add('status');
+ signalsToEmit.add('accessible-name');
+ }
+ });
+
+ signalsToEmit.forEach(s => this.emit(s));
+ }
+
+ reset() {
+ this.emit('reset');
+ }
+
+ destroy() {
+ this.emit('destroy');
+
+ this.disconnectAll();
+ this._proxy.destroy();
+ this._cancellable.cancel();
+ this._invalidatedPixmapsIcons.clear();
+
+ if (this._nameWatcher)
+ this._nameWatcher.destroy();
+ delete this._cancellable;
+ delete this._proxy;
+ delete this._nameWatcher;
+ }
+
+ _getPixmapProperty(iconType) {
+ const propertyName = SNIconType.toPropertyName(iconType,
+ {isPixbuf: true});
+ const pixmap = this._proxy.get_cached_property(propertyName);
+ const wasInvalidated = this._invalidatedPixmapsIcons.delete(iconType);
+
+ if (!pixmap && wasInvalidated) {
+ this._proxy.refreshProperty(propertyName, {
+ skipEqualityCheck: true,
+ }).catch(e => {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ });
+ }
+
+ return pixmap;
+ }
+
+ invalidatePixmapProperty(iconType) {
+ this._invalidatedPixmapsIcons.add(iconType);
+ this._proxy.set_cached_property(
+ SNIconType.toPropertyName(iconType, {isPixbuf: true}), null);
+ }
+
+ _getActivationToken(timestamp) {
+ const launchContext = global.create_app_launch_context(timestamp, -1);
+ const fakeAppInfo = Gio.AppInfo.create_from_commandline(
+ this._commandLine || 'true', this.id,
+ Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION);
+ return [launchContext, launchContext.get_startup_notify_id(fakeAppInfo, [])];
+ }
+
+ async provideActivationToken(timestamp) {
+ if (this._hasProvideXdgActivationToken === false)
+ return;
+
+ const [launchContext, activationToken] = this._getActivationToken(timestamp);
+ try {
+ await this._proxy.ProvideXdgActivationTokenAsync(activationToken,
+ this._cancellable);
+ this._hasProvideXdgActivationToken = true;
+ } catch (e) {
+ launchContext.launch_failed(activationToken);
+
+ if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD))
+ this._hasProvideXdgActivationToken = false;
+ else
+ Util.Logger.warn(`${this.id}, failed to provide activation token: ${e.message}`);
+ }
+ }
+
+ async open(x, y, timestamp) {
+ const cancellable = this._cancellable;
+ // we can't use WindowID because we're not able to get the x11 window id from a MetaWindow
+ // nor can we call any X11 functions. Luckily, the Activate method usually works fine.
+ // parameters are "an hint to the item where to show eventual windows" [sic]
+ // ... and don't seem to have any effect.
+
+ try {
+ await this.provideActivationToken(timestamp);
+ await this._proxy.ActivateAsync(x, y, cancellable);
+ this.supportsActivation = true;
+ } catch (e) {
+ if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD)) {
+ this.supportsActivation = false;
+ Util.Logger.warn(`${this.id}, does not support activation: ${e.message}`);
+ return;
+ }
+
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ Util.Logger.critical(`${this.id}, failed to activate: ${e.message}`);
+ }
+ }
+
+ async secondaryActivate(timestamp, x, y) {
+ const cancellable = this._cancellable;
+
+ try {
+ await this.provideActivationToken(timestamp);
+
+ if (this._hasAyatanaSecondaryActivate !== false) {
+ try {
+ await this._proxy.XAyatanaSecondaryActivateAsync(timestamp, cancellable);
+ this._hasAyatanaSecondaryActivate = true;
+ } catch (e) {
+ if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD))
+ this._hasAyatanaSecondaryActivate = false;
+ else
+ throw e;
+ }
+ }
+
+ if (!this._hasAyatanaSecondaryActivate)
+ await this._proxy.SecondaryActivateAsync(x, y, cancellable);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ Util.Logger.critical(`${this.id}, failed to secondary activate: ${e.message}`);
+ }
+ }
+
+ async scroll(dx, dy) {
+ const cancellable = this._cancellable;
+
+ try {
+ const actions = [];
+
+ if (dx !== 0) {
+ actions.push(this._proxy.ScrollAsync(Math.floor(dx),
+ 'horizontal', cancellable));
+ }
+
+ if (dy !== 0) {
+ actions.push(this._proxy.ScrollAsync(Math.floor(dy),
+ 'vertical', cancellable));
+ }
+
+ await Promise.all(actions);
+ } catch (e) {
+ Util.Logger.critical(`${this.id}, failed to scroll: ${e.message}`);
+ }
+ }
+}
+
+const StTextureCacheSkippingFileIcon = GObject.registerClass({
+ Implements: [Gio.Icon],
+}, class StTextureCacheSkippingFileIconImpl extends Gio.EmblemedIcon {
+ _init(params) {
+ // FIXME: We can't just inherit from Gio.FileIcon for some reason
+ super._init({gicon: new Gio.FileIcon(params)});
+ }
+
+ vfunc_to_tokens() {
+ // Disables the to_tokens() vfunc so that the icon to_string()
+ // method won't work and thus can't be kept forever around by
+ // StTextureCache, see the awesome debugging session in this thread:
+ // https://twitter.com/mild_sunrise/status/1458739604098621443
+ // upstream bug is at:
+ // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/4944
+ return [false, [], 0];
+ }
+});
+
+export const IconActor = GObject.registerClass(
+class AppIndicatorsIconActor extends St.Icon {
+ static get DEFAULT_STYLE() {
+ const settings = SettingsManager.getDefaultGSettings();
+ if (!settings.get_boolean('compact-mode-enabled'))
+ return 'padding: 0';
+ else
+ return 'padding: 0; margin: 0';
+ }
+
+ static get USER_WRITABLE_PATHS() {
+ if (!this._userWritablePaths) {
+ this._userWritablePaths = [
+ GLib.get_user_cache_dir(),
+ GLib.get_user_data_dir(),
+ GLib.get_user_config_dir(),
+ GLib.get_user_runtime_dir(),
+ GLib.get_home_dir(),
+ GLib.get_tmp_dir(),
+ ];
+
+ this._userWritablePaths.push(Object.values(GLib.UserDirectory).slice(
+ 0, -1).map(dirId => GLib.get_user_special_dir(dirId)));
+ }
+
+ return this._userWritablePaths;
+ }
+
+ _init(indicator, iconSize) {
+ super._init({
+ reactive: true,
+ style_class: 'system-status-icon',
+ fallbackIconName: FALLBACK_ICON_NAME,
+ });
+
+ this.name = this.constructor.name;
+ this.add_style_class_name('appindicator-icon');
+ this.add_style_class_name('status-notifier-icon');
+ this.set_style(AppIndicatorsIconActor.DEFAULT_STYLE);
+
+ const themeContext = St.ThemeContext.get_for_stage(global.stage);
+ this.height = iconSize * themeContext.scale_factor;
+
+ this._indicator = indicator;
+ this._customIcons = new Map();
+ this._iconSize = iconSize;
+ this._iconCache = new IconCache.IconCache();
+ this._cancellable = new Gio.Cancellable();
+ this._loadingIcons = Object.create(null);
+
+ Object.values(SNIconType).forEach(t => (this._loadingIcons[t] = new Map()));
+
+ Util.connectSmart(this._indicator, 'icon', this, () => {
+ if (this.is_mapped())
+ this._updateIcon();
+ });
+ Util.connectSmart(this._indicator, 'overlay-icon', this, () => {
+ if (this.is_mapped())
+ this._updateIcon();
+ });
+ Util.connectSmart(this._indicator, 'reset', this,
+ () => this._invalidateIconWhenFullyReady());
+
+ const settings = SettingsManager.getDefaultGSettings();
+ Util.connectSmart(settings, 'changed::icon-size', this, () =>
+ this._updateWhenFullyReady());
+ Util.connectSmart(settings, 'changed::custom-icons', this, () => {
+ this._updateCustomIcons();
+ this._invalidateIconWhenFullyReady();
+ });
+
+ if (GObject.signal_lookup('resource-scale-changed', this))
+ this.connect('resource-scale-changed', () => this._invalidateIcon());
+ else
+ this.connect('notify::resource-scale', () => this._invalidateIcon());
+
+ Util.connectSmart(themeContext, 'notify::scale-factor', this, tc => {
+ this._updateIconSize();
+ this.height = this._iconSize * tc.scale_factor;
+ this.width = -1;
+ this._invalidateIcon();
+ });
+
+ Util.connectSmart(Util.getDefaultTheme(), 'changed', this,
+ () => this._invalidateIconWhenFullyReady());
+
+ this.connect('notify::mapped', () => {
+ if (!this.is_mapped())
+ this._updateWhenFullyReady();
+ });
+
+ this._updateWhenFullyReady();
+
+ this.connect('destroy', () => {
+ this._iconCache.destroy();
+ this._cancellable.cancel();
+ this._cancellable = null;
+ this._indicator = null;
+ this._loadingIcons = null;
+ this._iconTheme = null;
+ });
+ }
+
+ get debugId() {
+ return this._indicator ? this._indicator.id : this.toString();
+ }
+
+ async _waitForFullyReady() {
+ const waitConditions = [];
+
+ if (!this.is_mapped()) {
+ waitConditions.push(new PromiseUtils.SignalConnectionPromise(
+ this, 'notify::mapped', this._cancellable));
+ }
+
+ if (!this._indicator.isReady) {
+ waitConditions.push(new PromiseUtils.SignalConnectionPromise(
+ this._indicator, 'ready', this._cancellable));
+ }
+
+ if (!waitConditions.length)
+ return true;
+
+ await Promise.all(waitConditions);
+ return this._waitForFullyReady();
+ }
+
+ async _updateWhenFullyReady() {
+ if (this._waitingReady)
+ return;
+
+ try {
+ this._waitingReady = true;
+ await this._waitForFullyReady();
+
+ this._updateIconSize();
+ this._updateIconClass();
+ this._updateCustomIcons();
+ this._invalidateIcon();
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ } finally {
+ delete this._waitingReady;
+ }
+ }
+
+ _updateIconClass() {
+ if (!this._indicator)
+ return;
+
+ this.add_style_class_name(
+ `appindicator-icon-${this._indicator.id.toLowerCase().replace(/_|\s/g, '-')}`);
+ }
+
+ _cancelLoadingByType(iconType) {
+ this._loadingIcons[iconType].forEach(c => c.cancel());
+ this._loadingIcons[iconType].clear();
+ }
+
+ _ensureNoIconIsLoading(iconType, id) {
+ if (this._loadingIcons[iconType].has(id)) {
+ Util.Logger.debug(`${this.debugId}, Icon ${id} Is still loading, ignoring the request`);
+ throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING,
+ 'Already in progress');
+ } else if (this._loadingIcons[iconType].size > 0) {
+ throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS,
+ 'Another icon is already loading');
+ }
+ }
+
+ _getIconLoadingCancellable(iconType, loadingId) {
+ try {
+ this._ensureNoIconIsLoading(iconType, loadingId);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
+ throw e;
+ this._cancelLoadingByType(iconType);
+ }
+
+ const cancellable = new Util.CancellableChild(this._cancellable);
+ this._loadingIcons[iconType].set(loadingId, cancellable);
+
+ return cancellable;
+ }
+
+ _cleanupIconLoadingCancellable(iconType, loadingId) {
+ if (this._loadingIcons)
+ this._loadingIcons[iconType].delete(loadingId);
+ }
+
+ _getResourceScale() {
+ // Remove this when we remove support for versions earlier than 3.38
+ const resourceScale = this.get_resource_scale();
+ if (Array.isArray(resourceScale))
+ return resourceScale[0] ? resourceScale[1] : 1.0;
+
+ return resourceScale;
+ }
+
+ // Will look the icon up in the cache, if it's found
+ // it will return it. Otherwise, it will create it and cache it.
+ async _cacheOrCreateIconByName(iconType, iconSize, iconScaling, iconName, themePath) {
+ const id = `${iconType}:${iconName}@${iconSize * iconScaling}:${themePath || ''}`;
+ let gicon = this._iconCache.get(id);
+
+ if (gicon)
+ return gicon;
+
+ const iconData = this._getIconData(iconName, themePath, iconSize, iconScaling);
+ const loadingId = iconData.file ? iconData.file.get_path() : id;
+
+ const cancellable = await this._getIconLoadingCancellable(iconType, id);
+ try {
+ gicon = await this._createIconByIconData(iconData, iconSize,
+ iconScaling, cancellable);
+ } finally {
+ this._cleanupIconLoadingCancellable(iconType, loadingId);
+ }
+ if (gicon)
+ gicon = this._iconCache.add(id, gicon);
+ return gicon;
+ }
+
+ _getIconLookupFlags(themeNode) {
+ let lookupFlags = 0;
+
+ if (!themeNode)
+ return lookupFlags;
+
+ const lookupFlagsEnum = St.IconLookupFlags;
+ const iconStyle = themeNode.get_icon_style();
+ if (iconStyle === St.IconStyle.REGULAR)
+ lookupFlags |= lookupFlagsEnum.FORCE_REGULAR;
+ else if (iconStyle === St.IconStyle.SYMBOLIC)
+ lookupFlags |= lookupFlagsEnum.FORCE_SYMBOLIC;
+
+ if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
+ lookupFlags |= lookupFlagsEnum.DIR_RTL;
+ else
+ lookupFlags |= lookupFlagsEnum.DIR_LTR;
+
+ return lookupFlags;
+ }
+
+ async _createIconByIconData(iconData, iconSize, iconScaling, cancellable) {
+ const {file, name} = iconData;
+
+ if (!file && !name) {
+ if (this._createIconIdle) {
+ throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING,
+ 'Already in progress');
+ }
+
+ try {
+ this._createIconIdle = new PromiseUtils.IdlePromise(GLib.PRIORITY_DEFAULT_IDLE,
+ cancellable);
+ await this._createIconIdle;
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ throw e;
+ } finally {
+ delete this._createIconIdle;
+ }
+ return this.gicon;
+ } else if (this._createIconIdle) {
+ this._createIconIdle.cancel();
+ delete this._createIconIdle;
+ }
+
+ if (name)
+ return new Gio.ThemedIcon({name});
+
+ if (!file)
+ throw new Error('Neither file or name are set');
+
+ if (!this._isFileInWritableArea(file))
+ return new Gio.FileIcon({file});
+
+ try {
+ const [format, width, height] = await GdkPixbuf.Pixbuf.get_file_info_async(
+ file.get_path(), cancellable);
+
+ if (!format) {
+ Util.Logger.critical(`${this.debugId}, Invalid image format: ${file.get_path()}`);
+ return null;
+ }
+
+ if (width >= height * 1.5) {
+ /* Hello indicator-multiload! */
+ await this._loadCustomImage(file,
+ width, height, iconSize, iconScaling, cancellable);
+ return null;
+ } else {
+ /* We'll wrap the icon so that it won't be cached forever by the shell */
+ return new StTextureCacheSkippingFileIcon({file});
+ }
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ Util.Logger.warn(
+ `${this.debugId}, Impossible to read image info from ` +
+ `path '${file ? file.get_path() : null}' or name '${name}': ${e}`);
+ }
+ throw e;
+ }
+ }
+
+ async _loadCustomImage(file, width, height, iconSize, iconScaling, cancellable) {
+ const textureCache = St.TextureCache.get_default();
+ const customImage = textureCache.load_file_async(file, -1,
+ height, 1, iconScaling);
+
+ const setCustomImageActor = imageActor => {
+ const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
+ const {content} = imageActor;
+ imageActor.content = null;
+ imageActor.destroy();
+
+ this._setImageContent(content,
+ width * scaleFactor, height * scaleFactor);
+ };
+
+ if (customImage.content) {
+ setCustomImageActor(customImage);
+ return;
+ }
+
+ const imageContentPromise = new PromiseUtils.SignalConnectionPromise(
+ customImage, 'notify::content', cancellable);
+ const waitPromise = new PromiseUtils.TimeoutSecondsPromise(
+ 1, GLib.PRIORITY_DEFAULT, cancellable);
+
+ const racingPromises = [imageContentPromise, waitPromise];
+
+ try {
+ await Promise.race(racingPromises);
+ if (!waitPromise.resolved())
+ setCustomImageActor(customImage);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ throw e;
+ } finally {
+ racingPromises.forEach(p => p.cancel());
+ }
+ }
+
+ _isFileInWritableArea(file) {
+ // No need to use IO here, we can just do some assumptions
+ // print('Writable paths', IconActor.USER_WRITABLE_PATHS)
+ const path = file.get_path();
+ return IconActor.USER_WRITABLE_PATHS.some(writablePath =>
+ path.startsWith(writablePath));
+ }
+
+ _createIconTheme(searchPath = []) {
+ const iconTheme = new St.IconTheme();
+ iconTheme.set_search_path(searchPath);
+
+ return iconTheme;
+ }
+
+ _getIconData(name, themePath, size, scale) {
+ const emptyIconData = {iconInfo: null, file: null, name: null};
+
+ if (!name) {
+ delete this._iconTheme;
+ return emptyIconData;
+ }
+
+ // HACK: icon is a path name. This is not specified by the API,
+ // but at least indicator-sensors uses it.
+ if (name[0] === '/') {
+ delete this._iconTheme;
+
+ const file = Gio.File.new_for_path(name);
+ return {file, iconInfo: null, name: null};
+ }
+
+ if (name.includes('.')) {
+ const splits = name.split('.');
+
+ if (['svg', 'png'].includes(splits[splits.length - 1]))
+ name = splits.slice(0, -1).join('');
+ }
+
+ if (themePath && Util.getDefaultTheme().get_search_path().includes(themePath))
+ themePath = null;
+
+ if (themePath) {
+ // If a theme path is provided, we need to lookup the icon ourself
+ // as St won't be able to do it unless we mess with default theme
+ // that is something we prefer not to do, as it would imply lots of
+ // St.TextureCache cleanups.
+
+ const newSearchPath = [themePath];
+ if (!this._iconTheme) {
+ this._iconTheme = this._createIconTheme(newSearchPath);
+ } else {
+ const currentSearchPath = this._iconTheme.get_search_path();
+
+ if (!currentSearchPath.includes(newSearchPath))
+ this._iconTheme.set_search_path(newSearchPath);
+ }
+
+ // try to look up the icon in the icon theme
+ const iconInfo = this._iconTheme.lookup_icon_for_scale(`${name}`,
+ size, scale, this._getIconLookupFlags(this.get_theme_node()) |
+ St.IconLookupFlags.GENERIC_FALLBACK);
+
+ if (iconInfo) {
+ return {
+ iconInfo,
+ file: Gio.File.new_for_path(iconInfo.get_filename()),
+ name: null,
+ };
+ }
+
+ const logger = this.gicon ? Util.Logger.debug : Util.Logger.warn;
+ logger(`${this.debugId}, Impossible to lookup icon ` +
+ `for '${name}' in ${themePath}`);
+
+ return emptyIconData;
+ }
+
+ delete this._iconTheme;
+ return {name, iconInfo: null, file: null};
+ }
+
+ _setImageContent(content, width, height) {
+ this.set({
+ content,
+ width,
+ height,
+ contentGravity: Clutter.ContentGravity.RESIZE_ASPECT,
+ fallbackIconName: null,
+ });
+ }
+
+ async _createIconFromPixmap(iconType, iconSize, iconScaling, scaleFactor, pixmapsVariant) {
+ const {pixmapVariant, width, height, rowStride} =
+ PixmapsUtils.getBestPixmap(pixmapsVariant, iconSize * iconScaling);
+
+ const id = `__PIXMAP_ICON_${width}x${height}`;
+
+ const imageContent = new St.ImageContent({
+ preferredWidth: width,
+ preferredHeight: height,
+ });
+
+ // Remove this dynamic check when we depend on GNOME 48.
+ const coglContext = [];
+ const mutterBackend = global.stage?.context?.get_backend?.();
+ if (imageContent.set_bytes.length === 6 && mutterBackend?.get_cogl_context)
+ coglContext.push(mutterBackend.get_cogl_context());
+ imageContent.set_bytes(...coglContext, pixmapVariant.get_data_as_bytes(),
+ PIXMAPS_FORMAT, width, height, rowStride);
+
+ if (iconType !== SNIconType.OVERLAY && !this._indicator.hasOverlayIcon) {
+ const scaledSize = iconSize * scaleFactor;
+ this._setImageContent(imageContent, scaledSize, scaledSize);
+ return null;
+ }
+
+ const cancellable = this._getIconLoadingCancellable(iconType, id);
+ try {
+ // FIXME: async API results in a gray icon for some reason
+ const [inputStream] = imageContent.load(iconSize, cancellable);
+ return await GdkPixbuf.Pixbuf.new_from_stream_at_scale_async(
+ inputStream, -1, iconSize * iconScaling, true, cancellable);
+ } catch (e) {
+ // the image data was probably bogus. We don't really know why, but it _does_ happen.
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ Util.Logger.warn(`${this.debugId}, Impossible to create image from data: ${e}`);
+ throw e;
+ } finally {
+ this._cleanupIconLoadingCancellable(iconType, id);
+ }
+ }
+
+ // The icon cache Active flag will be set to true if the used gicon matches
+ // the cached one (as in some cases it may be equal, but not the same object).
+ // So when it's not need anymore we make sure to check the active state
+ // and set it to false so that it can be picked up by the garbage collector.
+ _setGicon(iconType, gicon) {
+ if (iconType !== SNIconType.OVERLAY) {
+ if (gicon) {
+ if (this.gicon === gicon ||
+ (this.gicon && this.gicon.get_icon() === gicon))
+ return;
+
+ if (gicon instanceof Gio.EmblemedIcon)
+ this.gicon = gicon;
+ else
+ this.gicon = new Gio.EmblemedIcon({gicon});
+
+ this._iconCache.updateActive(SNIconType.NORMAL, gicon,
+ this.gicon.get_icon() === gicon);
+ } else {
+ this.gicon = null;
+ }
+ } else if (gicon) {
+ this._emblem = new Gio.Emblem({icon: gicon});
+ this._iconCache.updateActive(iconType, gicon, true);
+ } else {
+ this._emblem = null;
+ }
+
+ if (this.gicon) {
+ if (!this.gicon.get_emblems().some(e => e.equal(this._emblem))) {
+ this.gicon.clear_emblems();
+ if (this._emblem)
+ this.gicon.add_emblem(this._emblem);
+ }
+ }
+ }
+
+ async _updateIconByType(iconType, iconSize) {
+ let icon;
+ switch (iconType) {
+ case SNIconType.ATTENTION:
+ icon = this._indicator.attentionIcon;
+ break;
+ case SNIconType.NORMAL:
+ ({icon} = this._indicator);
+ break;
+ case SNIconType.OVERLAY:
+ icon = this._indicator.overlayIcon;
+ break;
+ }
+
+ const {theme, name, pixmap} = icon;
+ const commonArgs = [theme, iconType, iconSize];
+
+ if (this._customIcons.size) {
+ let customIcon = this._customIcons.get(iconType);
+ if (!await this._createAndSetIcon(customIcon, null, ...commonArgs)) {
+ if (iconType !== SNIconType.OVERLAY) {
+ customIcon = this._customIcons.get(SNIconType.NORMAL);
+ await this._createAndSetIcon(customIcon, null, ...commonArgs);
+ }
+ }
+ } else {
+ await this._createAndSetIcon(name, pixmap, ...commonArgs);
+ }
+ }
+
+ async _createAndSetIcon(name, pixmap, theme, iconType, iconSize) {
+ let gicon = null;
+
+ try {
+ gicon = await this._createIcon(name, pixmap, theme, iconType, iconSize);
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) ||
+ e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING))
+ return null;
+
+ if (iconType === SNIconType.OVERLAY) {
+ logError(e, `${this.debugId} unable to update icon emblem`);
+ } else {
+ this.fallbackIconName = FALLBACK_ICON_NAME;
+ logError(e, `${this.debugId} unable to update icon`);
+ }
+ }
+
+ try {
+ this._setGicon(iconType, gicon);
+
+ if (pixmap && this.gicon) {
+ // The pixmap has been saved, we can free the variants memory
+ this._indicator.invalidatePixmapProperty(iconType);
+ }
+
+ return gicon;
+ } catch (e) {
+ logError(e, 'Setting GIcon failed');
+ return null;
+ }
+ }
+
+ // updates the base icon
+ async _createIcon(name, pixmap, theme, iconType, iconSize) {
+ const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
+ const resourceScale = this._getResourceScale();
+ const iconScaling = Math.ceil(resourceScale * scaleFactor);
+
+ // From now on we consider them the same thing, as one replaces the other
+ if (iconType === SNIconType.ATTENTION)
+ iconType = SNIconType.NORMAL;
+
+ if (name) {
+ const gicon = await this._cacheOrCreateIconByName(
+ iconType, iconSize, iconScaling, name, theme);
+ if (gicon)
+ return gicon;
+ }
+
+ if (pixmap && pixmap.n_children()) {
+ return this._createIconFromPixmap(iconType,
+ iconSize, iconScaling, scaleFactor, pixmap);
+ }
+
+ return null;
+ }
+
+ // updates the base icon
+ async _updateIcon() {
+ if (this._indicator.status === SNIStatus.PASSIVE)
+ return;
+
+ if (this.gicon instanceof Gio.EmblemedIcon) {
+ const {gicon} = this.gicon;
+ this._iconCache.updateActive(SNIconType.NORMAL, gicon, false);
+ }
+
+ // we might need to use the AttentionIcon*, which have precedence over the normal icons
+ const iconType = this._indicator.status === SNIStatus.NEEDS_ATTENTION
+ ? SNIconType.ATTENTION : SNIconType.NORMAL;
+
+ try {
+ await this._updateIconByType(iconType, this._iconSize);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) &&
+ !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING))
+ logError(e, `${this.debugId}: Updating icon type ${iconType} failed`);
+ }
+ }
+
+ async _updateOverlayIcon() {
+ if (this._indicator.status === SNIStatus.PASSIVE)
+ return;
+
+ if (this._emblem) {
+ const {icon} = this._emblem;
+ this._iconCache.updateActive(SNIconType.OVERLAY, icon, false);
+ }
+
+ // KDE hardcodes the overlay icon size to 10px (normal icon size 16px)
+ // we approximate that ratio for other sizes, too.
+ // our algorithms will always pick a smaller one instead of stretching it.
+ const iconSize = Math.floor(this._iconSize / 1.6);
+
+ try {
+ await this._updateIconByType(SNIconType.OVERLAY, iconSize);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) &&
+ !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING))
+ logError(e, `${this.debugId}: Updating overlay icon failed`);
+ }
+ }
+
+ async _invalidateIconWhenFullyReady() {
+ if (this._waitingInvalidation)
+ return;
+
+ try {
+ this._waitingInvalidation = true;
+ await this._waitForFullyReady();
+ this._invalidateIcon();
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ } finally {
+ delete this._waitingInvalidation;
+ }
+ }
+
+ // called when the icon theme changes
+ _invalidateIcon() {
+ this._iconCache.clear();
+ this._cancellable.cancel();
+ this._cancellable = new Gio.Cancellable();
+ Object.values(SNIconType).forEach(iconType =>
+ this._loadingIcons[iconType].clear());
+
+ this._updateIcon().catch(e => logError(e));
+ this._updateOverlayIcon().catch(e => logError(e));
+ }
+
+ _updateIconSize() {
+ const settings = SettingsManager.getDefaultGSettings();
+ const sizeValue = settings.get_int('icon-size');
+
+ if (sizeValue > 0) {
+ if (!this._defaultIconSize)
+ this._defaultIconSize = this._iconSize;
+
+ this._iconSize = sizeValue;
+ } else if (this._defaultIconSize) {
+ this._iconSize = this._defaultIconSize;
+ delete this._defaultIconSize;
+ }
+
+ const themeIconSize = Math.round(
+ this.get_theme_node().get_length('icon-size'));
+ let iconStyle = AppIndicatorsIconActor.DEFAULT_STYLE;
+
+ if (themeIconSize > 0) {
+ const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
+
+ if (themeIconSize / scaleFactor !== this._iconSize) {
+ iconStyle = `${AppIndicatorsIconActor.DEFAULT_STYLE};` +
+ 'icon-size: 0';
+ }
+ }
+
+ this.set_style(iconStyle);
+ this.set_icon_size(this._iconSize);
+ }
+
+ _updateCustomIcons() {
+ const settings = SettingsManager.getDefaultGSettings();
+ this._customIcons.clear();
+
+ settings.get_value('custom-icons').deep_unpack().forEach(customIcons => {
+ const [indicatorId, normalIcon, attentionIcon] = customIcons;
+ if (this._indicator.id === indicatorId) {
+ this._customIcons.set(SNIconType.NORMAL, normalIcon);
+ this._customIcons.set(SNIconType.ATTENTION, attentionIcon);
+ }
+ });
+ }
+});