From 5d94c0a7d44a2255b81815a52a7056a94a39842d Mon Sep 17 00:00:00 2001 From: nippy Date: Sat, 18 Apr 2026 13:49:56 +0200 Subject: update Raveos themes --- .../appIndicator.js | 1607 ++++++++++++++++++++ 1 file changed, 1607 insertions(+) create mode 100644 raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js (limited to 'raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js') 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); + } + }); + } +}); -- cgit v1.3