summaryrefslogtreecommitdiff
path: root/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com
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
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')
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js1607
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js990
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js108
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusUtils.js121
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/extension.js90
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconCache.js179
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconTheme.js12
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/custom-icons-symbolic.svg46
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/general-preferences-symbolic.svg1
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js613
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml66
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml130
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml38
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces.js52
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ar/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2722 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/bg/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2557 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2221 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 949 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2377 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2646 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 1205 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 972 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ka/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 3640 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2373 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 1024 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2334 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2451 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 1067 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2727 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2239 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 1132 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 1020 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 3523 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2497 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/uk/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 3061 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2338 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_TW/LC_MESSAGES/AppIndicatorExtension.mobin0 -> 2449 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/logger.js105
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/metadata.json18
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js68
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/customIconPage.js240
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/generalPage.js159
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/prefs.js45
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js324
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiledbin0 -> 820 bytes
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml49
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js55
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js320
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/tools/busAnalyzer.js46
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js103
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/util.js271
51 files changed, 5856 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);
+ }
+ });
+ }
+});
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js
new file mode 100644
index 0000000..52c812d
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js
@@ -0,0 +1,990 @@
+// 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 PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
+import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
+
+import * as DBusInterfaces from './interfaces.js';
+import * as PromiseUtils from './promiseUtils.js';
+import * as Util from './util.js';
+import {DBusProxy} from './dbusProxy.js';
+
+Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_async');
+
+// ////////////////////////////////////////////////////////////////////////
+// PART ONE: "ViewModel" backend implementation.
+// Both code and design are inspired by libdbusmenu
+// ////////////////////////////////////////////////////////////////////////
+
+/**
+ * Saves menu property values and handles type checking and defaults
+ */
+export class PropertyStore {
+ constructor(initialProperties) {
+ this._props = new Map();
+
+ if (initialProperties) {
+ for (const [prop, value] of Object.entries(initialProperties))
+ this.set(prop, value);
+ }
+ }
+
+ set(name, value) {
+ if (name in PropertyStore.MandatedTypes && value &&
+ !value.is_of_type(PropertyStore.MandatedTypes[name]))
+ Util.Logger.warn(`Cannot set property ${name}: type mismatch!`);
+ else if (value)
+ this._props.set(name, value);
+ else
+ this._props.delete(name);
+ }
+
+ get(name) {
+ const prop = this._props.get(name);
+ if (prop)
+ return prop;
+ else if (name in PropertyStore.DefaultValues)
+ return PropertyStore.DefaultValues[name];
+ else
+ return null;
+ }
+}
+
+// we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches
+PropertyStore.MandatedTypes = {
+ 'visible': GLib.VariantType.new('b'),
+ 'enabled': GLib.VariantType.new('b'),
+ 'label': GLib.VariantType.new('s'),
+ 'type': GLib.VariantType.new('s'),
+ 'children-display': GLib.VariantType.new('s'),
+ 'icon-name': GLib.VariantType.new('s'),
+ 'icon-data': GLib.VariantType.new('ay'),
+ 'toggle-type': GLib.VariantType.new('s'),
+ 'toggle-state': GLib.VariantType.new('i'),
+};
+
+PropertyStore.DefaultValues = {
+ 'visible': GLib.Variant.new_boolean(true),
+ 'enabled': GLib.Variant.new_boolean(true),
+ 'label': GLib.Variant.new_string(''),
+ 'type': GLib.Variant.new_string('standard'),
+ // elements not in here must return null
+};
+
+/**
+ * Represents a single menu item
+ */
+export class DbusMenuItem extends Signals.EventEmitter {
+ // will steal the properties object
+ constructor(client, id, properties, childrenIds) {
+ super();
+
+ this._client = client;
+ this._id = id;
+ this._propStore = new PropertyStore(properties);
+ this._children_ids = childrenIds;
+ }
+
+ propertyGet(propName) {
+ const prop = this.propertyGetVariant(propName);
+ return prop ? prop.get_string()[0] : null;
+ }
+
+ propertyGetVariant(propName) {
+ return this._propStore.get(propName);
+ }
+
+ propertyGetBool(propName) {
+ const prop = this.propertyGetVariant(propName);
+ return prop ? prop.get_boolean() : false;
+ }
+
+ propertyGetInt(propName) {
+ const prop = this.propertyGetVariant(propName);
+ return prop ? prop.get_int32() : 0;
+ }
+
+ propertySet(prop, value) {
+ this._propStore.set(prop, value);
+
+ this.emit('property-changed', prop, this.propertyGetVariant(prop));
+ }
+
+ resetProperties() {
+ Object.entries(PropertyStore.DefaultValues).forEach(([prop, value]) =>
+ this.propertySet(prop, value));
+ }
+
+ getChildrenIds() {
+ return this._children_ids.concat(); // clone it!
+ }
+
+ addChild(pos, childId) {
+ this._children_ids.splice(pos, 0, childId);
+ this.emit('child-added', this._client.getItem(childId), pos);
+ }
+
+ removeChild(childId) {
+ // find it
+ let pos = -1;
+ for (let i = 0; i < this._children_ids.length; ++i) {
+ if (this._children_ids[i] === childId) {
+ pos = i;
+ break;
+ }
+ }
+
+ if (pos < 0) {
+ Util.Logger.critical("Trying to remove child which doesn't exist");
+ } else {
+ this._children_ids.splice(pos, 1);
+ this.emit('child-removed', this._client.getItem(childId));
+ }
+ }
+
+ moveChild(childId, newPos) {
+ // find the old position
+ let oldPos = -1;
+ for (let i = 0; i < this._children_ids.length; ++i) {
+ if (this._children_ids[i] === childId) {
+ oldPos = i;
+ break;
+ }
+ }
+
+ if (oldPos < 0) {
+ Util.Logger.critical("tried to move child which wasn't in the list");
+ return;
+ }
+
+ if (oldPos !== newPos) {
+ this._children_ids.splice(oldPos, 1);
+ this._children_ids.splice(newPos, 0, childId);
+ this.emit('child-moved', oldPos, newPos, this._client.getItem(childId));
+ }
+ }
+
+ getChildren() {
+ return this._children_ids.map(el => this._client.getItem(el));
+ }
+
+ hasChildren() {
+ return this._children_ids.length > 0;
+ }
+
+ handleEvent(event, data, timestamp) {
+ if (!data)
+ data = GLib.Variant.new_int32(0);
+
+ return this._client.sendEvent(this._id, event, data, timestamp);
+ }
+
+ getId() {
+ return this._id;
+ }
+
+ sendAboutToShow() {
+ this._client.sendAboutToShow(this._id);
+ }
+}
+
+
+/**
+ * The client does the heavy lifting of actually reading layouts and distributing events
+ */
+
+export const DBusClient = GObject.registerClass({
+ Signals: {'ready-changed': {}},
+}, class AppIndicatorsDBusClient extends DBusProxy {
+ static get interfaceInfo() {
+ if (!this._interfaceInfo) {
+ this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(
+ DBusInterfaces.DBusMenu);
+ }
+ return this._interfaceInfo;
+ }
+
+ static get baseItems() {
+ if (!this._baseItems) {
+ this._baseItems = {
+ 'children-display': GLib.Variant.new_string('submenu'),
+ };
+ }
+ return this._baseItems;
+ }
+
+ static destroy() {
+ delete this._interfaceInfo;
+ }
+
+ _init(busName, objectPath) {
+ const {interfaceInfo} = AppIndicatorsDBusClient;
+
+ super._init(busName, objectPath, interfaceInfo,
+ Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES);
+
+ this._items = new Map();
+ this._items.set(0, new DbusMenuItem(this, 0, DBusClient.baseItems, []));
+ this._flagItemsUpdateRequired = false;
+
+ // will be set to true if a layout update is needed once active
+ this._flagLayoutUpdateRequired = false;
+
+ // property requests are queued
+ this._propertiesRequestedFor = new Set(/* ids */);
+
+ this._layoutUpdated = false;
+ this._active = false;
+ }
+
+ async initAsync(cancellable) {
+ await super.initAsync(cancellable);
+
+ this._requestLayoutUpdate();
+ }
+
+ _onNameOwnerChanged() {
+ if (this.isReady)
+ this._requestLayoutUpdate();
+ }
+
+ get isReady() {
+ return this._layoutUpdated && !!this.gNameOwner;
+ }
+
+ get cancellable() {
+ return this._cancellable;
+ }
+
+ getRoot() {
+ return this._items.get(0);
+ }
+
+ _requestLayoutUpdate() {
+ const cancellable = new Util.CancellableChild(this._cancellable);
+ this._beginLayoutUpdate(cancellable);
+ }
+
+ async _requestProperties(propertyId, cancellable) {
+ this._propertiesRequestedFor.add(propertyId);
+
+ if (this._propertiesRequest && this._propertiesRequest.pending())
+ return;
+
+ // if we don't have any requests queued, we'll need to add one
+ this._propertiesRequest = new PromiseUtils.IdlePromise(
+ GLib.PRIORITY_DEFAULT_IDLE, cancellable);
+ await this._propertiesRequest;
+
+ const requestedProperties = Array.from(this._propertiesRequestedFor);
+ this._propertiesRequestedFor.clear();
+ const [result] = await this.GetGroupPropertiesAsync(requestedProperties,
+ [], cancellable);
+
+ result.forEach(([id, properties]) => {
+ const item = this._items.get(id);
+ if (!item)
+ return;
+
+ item.resetProperties();
+
+ for (const [prop, value] of Object.entries(properties))
+ item.propertySet(prop, value);
+ });
+ }
+
+ // Traverses the list of cached menu items and removes everyone that is not in the list
+ // so we don't keep alive unused items
+ _gcItems() {
+ const tag = new Date().getTime();
+
+ const toTraverse = [0];
+ while (toTraverse.length > 0) {
+ const item = this.getItem(toTraverse.shift());
+ item._dbusClientGcTag = tag;
+ Array.prototype.push.apply(toTraverse, item.getChildrenIds());
+ }
+
+ this._items.forEach((i, id) => {
+ if (i._dbusClientGcTag !== tag)
+ this._items.delete(id);
+ });
+ }
+
+ // the original implementation will only request partial layouts if somehow possible
+ // we try to save us from multiple kinds of race conditions by always requesting a full layout
+ _beginLayoutUpdate(cancellable) {
+ this._layoutUpdateUpdateAsync(cancellable).catch(e => {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ });
+ }
+
+ // the original implementation will only request partial layouts if somehow possible
+ // we try to save us from multiple kinds of race conditions by always requesting a full layout
+ async _layoutUpdateUpdateAsync(cancellable) {
+ // we only read the type property, because if the type changes after reading all properties,
+ // the view would have to replace the item completely which we try to avoid
+ if (this._layoutUpdateCancellable)
+ this._layoutUpdateCancellable.cancel();
+
+ this._layoutUpdateCancellable = cancellable;
+
+ try {
+ const [revision_, root] = await this.GetLayoutAsync(0, -1,
+ ['type', 'children-display'], cancellable);
+
+ this._updateLayoutState(true);
+ this._doLayoutUpdate(root, cancellable);
+ this._gcItems();
+ this._flagLayoutUpdateRequired = false;
+ this._flagItemsUpdateRequired = false;
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ this._updateLayoutState(false);
+ throw e;
+ } finally {
+ if (this._layoutUpdateCancellable === cancellable)
+ this._layoutUpdateCancellable = null;
+ }
+ }
+
+ _updateLayoutState(state) {
+ const wasReady = this.isReady;
+ this._layoutUpdated = state;
+ if (this.isReady !== wasReady)
+ this.emit('ready-changed');
+ }
+
+ _doLayoutUpdate(item, cancellable) {
+ const [id, properties, children] = item;
+
+ const childrenUnpacked = children.map(c => c.deep_unpack());
+ const childrenIds = childrenUnpacked.map(([c]) => c);
+
+ // make sure all our children exist
+ childrenUnpacked.forEach(c => this._doLayoutUpdate(c, cancellable));
+
+ // make sure we exist
+ const menuItem = this._items.get(id);
+
+ if (menuItem) {
+ // we do, update our properties if necessary
+ for (const [prop, value] of Object.entries(properties))
+ menuItem.propertySet(prop, value);
+
+ // make sure our children are all at the right place, and exist
+ const oldChildrenIds = menuItem.getChildrenIds();
+ for (let i = 0; i < childrenIds.length; ++i) {
+ // try to recycle an old child
+ let oldChild = -1;
+ for (let j = 0; j < oldChildrenIds.length; ++j) {
+ if (oldChildrenIds[j] === childrenIds[i]) {
+ [oldChild] = oldChildrenIds.splice(j, 1);
+ break;
+ }
+ }
+
+ if (oldChild < 0) {
+ // no old child found, so create a new one!
+ menuItem.addChild(i, childrenIds[i]);
+ } else {
+ // old child found, reuse it!
+ menuItem.moveChild(childrenIds[i], i);
+ }
+ }
+
+ // remove any old children that weren't reused
+ oldChildrenIds.forEach(c => menuItem.removeChild(c));
+
+ if (!this._flagItemsUpdateRequired)
+ return id;
+ }
+
+ // we don't, so let's create us
+ let newMenuItem = menuItem;
+
+ if (!newMenuItem) {
+ newMenuItem = new DbusMenuItem(this, id, properties, childrenIds);
+ this._items.set(id, newMenuItem);
+ }
+
+ this._requestProperties(id, cancellable).catch(e => {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`);
+ });
+
+ return id;
+ }
+
+ async _doPropertiesUpdateAsync(cancellable) {
+ if (this._propertiesUpdateCancellable)
+ this._propertiesUpdateCancellable.cancel();
+
+ this._propertiesUpdateCancellable = cancellable;
+
+ try {
+ const requests = [];
+
+ this._items.forEach((_, id) =>
+ requests.push(this._requestProperties(id, cancellable)));
+
+ await Promise.all(requests);
+ } finally {
+ if (this._propertiesUpdateCancellable === cancellable)
+ this._propertiesUpdateCancellable = null;
+ }
+ }
+
+ _doPropertiesUpdate() {
+ const cancellable = new Util.CancellableChild(this._cancellable);
+ this._doPropertiesUpdateAsync(cancellable).catch(e => {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`);
+ });
+ }
+
+
+ set active(active) {
+ const wasActive = this._active;
+ this._active = active;
+
+ if (active && wasActive !== active) {
+ if (this._flagLayoutUpdateRequired) {
+ this._requestLayoutUpdate();
+ } else if (this._flagItemsUpdateRequired) {
+ this._doPropertiesUpdate();
+ this._flagItemsUpdateRequired = false;
+ }
+ }
+ }
+
+ _onSignal(_sender, signal, params) {
+ if (signal === 'LayoutUpdated') {
+ if (!this._active && this.getRoot()?.hasChildren()) {
+ this._flagLayoutUpdateRequired = true;
+ return;
+ }
+
+ this._requestLayoutUpdate();
+ } else if (signal === 'ItemsPropertiesUpdated') {
+ if (!this._active && this.getRoot()?.hasChildren()) {
+ this._flagItemsUpdateRequired = true;
+ return;
+ }
+
+ this._onPropertiesUpdated(params.deep_unpack());
+ }
+ }
+
+ getItem(id) {
+ const item = this._items.get(id);
+ if (!item)
+ Util.Logger.warn(`trying to retrieve item for non-existing id ${id} !?`);
+ return item || null;
+ }
+
+ // we don't need to cache and burst-send that since it will not happen that frequently
+ async sendAboutToShow(id) {
+ if (this._hasAboutToShow === false)
+ return;
+
+ /* Some indicators (you, dropbox!) don't use the right signature
+ * and don't return a boolean, so we need to support both cases */
+ try {
+ const ret = await this.gConnection.call(this.gName, this.gObjectPath,
+ this.gInterfaceName, 'AboutToShow', new GLib.Variant('(i)', [id]),
+ null, Gio.DBusCallFlags.NONE, -1, this._cancellable);
+
+ if ((ret.is_of_type(new GLib.VariantType('(b)')) &&
+ ret.get_child_value(0).get_boolean()) ||
+ ret.is_of_type(new GLib.VariantType('()')))
+ this._requestLayoutUpdate();
+ } catch (e) {
+ Util.Logger.debug('Error when calling \'AboutToShow()\' in ' +
+ `${this.gName}, ${this.gObjectPath}, ${this.gInterfaceName}`);
+ if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD) ||
+ e.matches(Gio.DBusError, Gio.DBusError.FAILED)) {
+ this._hasAboutToShow = false;
+ return;
+ }
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }
+ }
+
+ async sendEvent(id, event, params, timestamp) {
+ if (!this.gNameOwner)
+ return;
+
+ try {
+ await this.EventAsync(id, event, params, timestamp, this._cancellable);
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+
+ throw e;
+ }
+ }
+
+ _onPropertiesUpdated([changed, removed]) {
+ changed.forEach(([id, props]) => {
+ const item = this._items.get(id);
+ if (!item)
+ return;
+
+ for (const [prop, value] of Object.entries(props))
+ item.propertySet(prop, value);
+ });
+ removed.forEach(([id, propNames]) => {
+ const item = this._items.get(id);
+ if (!item)
+ return;
+
+ propNames.forEach(propName => item.propertySet(propName, null));
+ });
+ }
+});
+
+// ////////////////////////////////////////////////////////////////////////
+// PART TWO: "View" frontend implementation.
+// ////////////////////////////////////////////////////////////////////////
+
+// https://bugzilla.gnome.org/show_bug.cgi?id=731514
+// GNOME 3.10 and 3.12 can't open a nested submenu.
+// Patches have been written, but it's not clear when (if?) they will be applied.
+// We also don't know whether they will be backported to 3.10, so we will work around
+// it in the meantime. Offending versions can be clearly identified:
+const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype;
+
+/**
+ * Creates new wrapper menu items and injects methods for managing them at runtime.
+ *
+ * Many functions in this object will be bound to the created item and executed as event
+ * handlers, so any `this` will refer to a menu item create in createItem
+ */
+const MenuItemFactory = {
+ createItem(client, dbusItem) {
+ // first, decide whether it's a submenu or not
+ let shellItem;
+ if (dbusItem.propertyGet('children-display') === 'submenu')
+ shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME');
+ else if (dbusItem.propertyGet('type') === 'separator')
+ shellItem = new PopupMenu.PopupSeparatorMenuItem('');
+ else
+ shellItem = new PopupMenu.PopupMenuItem('FIXME');
+
+ shellItem._dbusItem = dbusItem;
+ shellItem._dbusClient = client;
+
+ if (shellItem instanceof PopupMenu.PopupMenuItem) {
+ shellItem._icon = new St.Icon({
+ style_class: 'popup-menu-icon',
+ xAlign: Clutter.ActorAlign.END,
+ });
+ shellItem.add_child(shellItem._icon);
+ shellItem.label.x_expand = true;
+ }
+
+ // initialize our state
+ MenuItemFactory._updateLabel.call(shellItem);
+ MenuItemFactory._updateOrnament.call(shellItem);
+ MenuItemFactory._updateImage.call(shellItem);
+ MenuItemFactory._updateVisible.call(shellItem);
+ MenuItemFactory._updateSensitive.call(shellItem);
+
+ // initially create children
+ if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) {
+ dbusItem.getChildren().forEach(c =>
+ shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, c)));
+ }
+
+ // now, connect various events
+ Util.connectSmart(dbusItem, 'property-changed',
+ shellItem, MenuItemFactory._onPropertyChanged);
+ Util.connectSmart(dbusItem, 'child-added',
+ shellItem, MenuItemFactory._onChildAdded);
+ Util.connectSmart(dbusItem, 'child-removed',
+ shellItem, MenuItemFactory._onChildRemoved);
+ Util.connectSmart(dbusItem, 'child-moved',
+ shellItem, MenuItemFactory._onChildMoved);
+ Util.connectSmart(shellItem, 'activate',
+ shellItem, MenuItemFactory._onActivate);
+
+ shellItem.connect('destroy', () => {
+ shellItem._dbusItemCancellable?.cancel();
+ shellItem._dbusItemCancellable = null;
+ shellItem._dbusItem = null;
+ shellItem._dbusClient = null;
+ shellItem._icon = null;
+ });
+
+ if (shellItem.menu) {
+ Util.connectSmart(shellItem.menu, 'open-state-changed',
+ shellItem, MenuItemFactory._onOpenStateChanged);
+ }
+
+ return shellItem;
+ },
+
+ _onOpenStateChanged(menu, open) {
+ if (open) {
+ if (NEED_NESTED_SUBMENU_FIX) {
+ // close our own submenus
+ if (menu._openedSubMenu)
+ menu._openedSubMenu.close(false);
+
+ // register ourselves and close sibling submenus
+ if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu)
+ menu._parent._openedSubMenu.close(true);
+
+ menu._parent._openedSubMenu = menu;
+ }
+
+ this._dbusItem.handleEvent('opened', null, 0).catch(logError);
+ this._dbusItem.sendAboutToShow();
+ } else {
+ if (NEED_NESTED_SUBMENU_FIX) {
+ // close our own submenus
+ if (menu._openedSubMenu)
+ menu._openedSubMenu.close(false);
+ }
+
+ this._dbusItem.handleEvent('closed', null, 0).catch(logError);
+ }
+ },
+
+ _onActivate(_item, event) {
+ const timestamp = event.get_time();
+ if (timestamp && this._dbusClient.indicator)
+ this._dbusClient.indicator.provideActivationToken(timestamp);
+
+ this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0),
+ timestamp).catch(logError);
+ },
+
+ _onPropertyChanged(dbusItem, prop, _value) {
+ if (prop === 'toggle-type' || prop === 'toggle-state')
+ MenuItemFactory._updateOrnament.call(this);
+ else if (prop === 'label')
+ MenuItemFactory._updateLabel.call(this);
+ else if (prop === 'enabled')
+ MenuItemFactory._updateSensitive.call(this);
+ else if (prop === 'visible')
+ MenuItemFactory._updateVisible.call(this);
+ else if (prop === 'icon-name' || prop === 'icon-data')
+ MenuItemFactory._updateImage.call(this);
+ else if (prop === 'type' || prop === 'children-display')
+ MenuItemFactory._replaceSelf.call(this);
+ else
+ Util.Logger.debug(`Unhandled property change: ${prop}`);
+ },
+
+ _onChildAdded(dbusItem, child, position) {
+ if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
+ Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole');
+ MenuItemFactory._replaceSelf.call(this);
+ } else {
+ this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position);
+ }
+ },
+
+ _onChildRemoved(dbusItem, child) {
+ if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
+ Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole');
+ MenuItemFactory._replaceSelf.call(this);
+ } else {
+ // find it!
+ this.menu._getMenuItems().forEach(item => {
+ if (item._dbusItem === child)
+ item.destroy();
+ });
+ }
+ },
+
+ _onChildMoved(dbusItem, child, oldpos, newpos) {
+ if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
+ Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole');
+ MenuItemFactory._replaceSelf.call(this);
+ } else {
+ MenuUtils.moveItemInMenu(this.menu, child, newpos);
+ }
+ },
+
+ _updateLabel() {
+ const label = this._dbusItem.propertyGet('label').replace(/_([^_])/, '$1');
+
+ if (this.label) // especially on GS3.8, the separator item might not even have a hidden label
+ this.label.set_text(label);
+ },
+
+ _updateOrnament() {
+ if (!this.setOrnament)
+ return; // separators and alike might not have gotten the polyfill
+
+ if (this._dbusItem.propertyGet('toggle-type') === 'checkmark' &&
+ this._dbusItem.propertyGetInt('toggle-state'))
+ this.setOrnament(PopupMenu.Ornament.CHECK);
+ else if (this._dbusItem.propertyGet('toggle-type') === 'radio' &&
+ this._dbusItem.propertyGetInt('toggle-state'))
+ this.setOrnament(PopupMenu.Ornament.DOT);
+ else
+ this.setOrnament(PopupMenu.Ornament.NONE);
+ },
+
+ async _updateImage() {
+ if (!this._icon)
+ return; // might be missing on submenus / separators
+
+ const iconName = this._dbusItem.propertyGet('icon-name');
+ const iconData = this._dbusItem.propertyGetVariant('icon-data');
+ if (iconName) {
+ this._icon.icon_name = iconName;
+ } else if (iconData) {
+ try {
+ if (!this._dbusItemCancellable) {
+ this._dbusItemCancellable = new Util.CancellableChild(
+ this._dbusClient.cancellable);
+ }
+
+ const inputStream = Gio.MemoryInputStream.new_from_bytes(
+ iconData.get_data_as_bytes());
+ this._icon.gicon = await GdkPixbuf.Pixbuf.new_from_stream_async(
+ inputStream, this._dbusItemCancellable);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }
+ }
+ },
+
+ _updateVisible() {
+ this.visible = this._dbusItem.propertyGetBool('visible');
+ },
+
+ _updateSensitive() {
+ this.setSensitive(this._dbusItem.propertyGetBool('enabled'));
+ },
+
+ _replaceSelf(newSelf) {
+ // create our new self if needed
+ if (!newSelf)
+ newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem);
+
+ // first, we need to find our old position
+ let pos = -1;
+ const family = this._parent._getMenuItems();
+ for (let i = 0; i < family.length; ++i) {
+ if (family[i] === this)
+ pos = i;
+ }
+
+ if (pos < 0)
+ throw new Error("DBusMenu: can't replace non existing menu item");
+
+
+ // add our new self while we're still alive
+ this._parent.addMenuItem(newSelf, pos);
+
+ // now destroy our old self
+ this.destroy();
+ },
+};
+
+/**
+ * Utility functions not necessarily belonging into the item factory
+ */
+const MenuUtils = {
+ moveItemInMenu(menu, dbusItem, newpos) {
+ // HACK: we're really getting into the internals of the PopupMenu implementation
+
+ // First, find our wrapper. Children tend to lie. We do not trust the old positioning.
+ const family = menu._getMenuItems();
+ for (let i = 0; i < family.length; ++i) {
+ if (family[i]._dbusItem === dbusItem) {
+ // now, remove it
+ menu.box.remove_child(family[i]);
+
+ // and add it again somewhere else
+ if (newpos < family.length && family[newpos] !== family[i])
+ menu.box.insert_child_below(family[i], family[newpos]);
+ else
+ menu.box.add(family[i]);
+
+ // skip the rest
+ return;
+ }
+ }
+ },
+};
+
+
+/**
+ * Processes DBus events, creates the menu items and handles the actions
+ *
+ * Something like a mini-god-object
+ */
+export class Client extends Signals.EventEmitter {
+ constructor(busName, path, indicator) {
+ super();
+
+ this._busName = busName;
+ this._busPath = path;
+ this._client = new DBusClient(busName, path);
+ this._rootMenu = null; // the shell menu
+ this._rootItem = null; // the DbusMenuItem for the root
+ this.indicator = indicator;
+ this.cancellable = new Util.CancellableChild(this.indicator.cancellable);
+
+ this._client.initAsync(this.cancellable).catch(e => {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ });
+
+ Util.connectSmart(this._client, 'ready-changed', this,
+ () => this.emit('ready-changed'));
+ }
+
+ get isReady() {
+ return this._client.isReady;
+ }
+
+ // this will attach the client to an already existing menu that will be used as the root menu.
+ // it will also connect the client to be automatically destroyed when the menu dies.
+ attachToMenu(menu) {
+ this._rootMenu = menu;
+ this._rootItem = this._client.getRoot();
+ this._itemsBeingAdded = new Set();
+
+ // cleanup: remove existing children (just in case)
+ this._rootMenu.removeAll();
+
+ if (NEED_NESTED_SUBMENU_FIX)
+ menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this);
+
+ // connect handlers
+ Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpenStateChanged);
+ Util.connectSmart(menu, 'destroy', this, this.destroy);
+
+ Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded);
+ Util.connectSmart(this._rootItem, 'child-removed', this, this._onRootChildRemoved);
+ Util.connectSmart(this._rootItem, 'child-moved', this, this._onRootChildMoved);
+
+ // Dropbox requires us to call AboutToShow(0) first
+ this._rootItem.sendAboutToShow();
+
+ // fill the menu for the first time
+ const children = this._rootItem.getChildren();
+ children.forEach(child =>
+ this._onRootChildAdded(this._rootItem, child));
+ }
+
+ _setOpenedSubmenu(submenu) {
+ if (!submenu)
+ return;
+
+ if (submenu._parent !== this._rootMenu)
+ return;
+
+ if (submenu === this._openedSubMenu)
+ return;
+
+ if (this._openedSubMenu && this._openedSubMenu.isOpen)
+ this._openedSubMenu.close(true);
+
+ this._openedSubMenu = submenu;
+ }
+
+ _onRootChildAdded(dbusItem, child, position) {
+ // Menu additions can be expensive, so let's do it in different chunks
+ const basePriority = this.isOpen ? GLib.PRIORITY_DEFAULT : GLib.PRIORITY_LOW;
+ const idlePromise = new PromiseUtils.IdlePromise(
+ basePriority + this._itemsBeingAdded.size, this.cancellable);
+ this._itemsBeingAdded.add(child);
+
+ idlePromise.then(() => {
+ if (!this._itemsBeingAdded.has(child))
+ return;
+
+ this._rootMenu.addMenuItem(
+ MenuItemFactory.createItem(this, child), position);
+ }).catch(e => {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }).finally(() => this._itemsBeingAdded.delete(child));
+ }
+
+ _onRootChildRemoved(dbusItem, child) {
+ // children like to play hide and seek
+ // but we know how to find it for sure!
+ const item = this._rootMenu._getMenuItems().find(it =>
+ it._dbusItem === child);
+
+ if (item)
+ item.destroy();
+ else
+ this._itemsBeingAdded.delete(child);
+ }
+
+ _onRootChildMoved(dbusItem, child, oldpos, newpos) {
+ MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos);
+ }
+
+ _onMenuOpenStateChanged(menu, state) {
+ if (!this._rootItem)
+ return;
+
+ this._client.active = state;
+
+ if (state) {
+ if (this._openedSubMenu && this._openedSubMenu.isOpen)
+ this._openedSubMenu.close();
+
+ this._rootItem.handleEvent('opened', null, 0).catch(logError);
+ this._rootItem.sendAboutToShow();
+ } else {
+ this._rootItem.handleEvent('closed', null, 0).catch(e => {
+ if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_OBJECT)) {
+ // The menu hay have been removed at this point, thus do not
+ // spam the users about this if it happens.
+ return;
+ }
+
+ logError(e);
+ });
+ }
+ }
+
+ destroy() {
+ this.emit('destroy');
+
+ if (this._client)
+ this._client.destroy();
+
+ this._client = null;
+ this._rootItem = null;
+ this._rootMenu = null;
+ this.indicator = null;
+ this._itemsBeingAdded = null;
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js
new file mode 100644
index 0000000..047f2d2
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js
@@ -0,0 +1,108 @@
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+
+import {CancellableChild, Logger} from './util.js';
+
+Gio._promisify(Gio.DBusProxy.prototype, 'init_async');
+
+export const DBusProxy = GObject.registerClass({
+ Signals: {'destroy': {}},
+}, class DBusProxy extends Gio.DBusProxy {
+ static get TUPLE_VARIANT_TYPE() {
+ if (!this._tupleVariantType)
+ this._tupleVariantType = new GLib.VariantType('(v)');
+
+ return this._tupleVariantType;
+ }
+
+ static destroy() {
+ delete this._tupleType;
+ }
+
+ _init(busName, objectPath, interfaceInfo, flags = Gio.DBusProxyFlags.NONE) {
+ if (interfaceInfo.signals.length)
+ Logger.warn('Avoid exposing signals to gjs!');
+
+ super._init({
+ gConnection: Gio.DBus.session,
+ gInterfaceName: interfaceInfo.name,
+ gInterfaceInfo: interfaceInfo,
+ gName: busName,
+ gObjectPath: objectPath,
+ gFlags: flags,
+ });
+
+ this._signalIds = [];
+
+ if (!(flags & Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS)) {
+ this._signalIds.push(this.connect('g-signal',
+ (_proxy, ...args) => this._onSignal(...args)));
+ }
+
+ this._signalIds.push(this.connect('notify::g-name-owner', () => {
+ if (!this.gNameOwner) {
+ this._cancellable.cancel();
+ this._cancellable = new Gio.Cancellable();
+ }
+ this._onNameOwnerChanged();
+ }));
+ }
+
+ async initAsync(cancellable) {
+ cancellable = new CancellableChild(cancellable);
+ await this.init_async(GLib.PRIORITY_DEFAULT, cancellable);
+ this._cancellable = cancellable;
+
+ this.gInterfaceInfo.methods.map(m => m.name).forEach(method =>
+ this._ensureAsyncMethod(method));
+ }
+
+ destroy() {
+ this.emit('destroy');
+
+ this._signalIds.forEach(id => this.disconnect(id));
+
+ if (this._cancellable)
+ this._cancellable.cancel();
+ }
+
+ // This can be removed when we will have GNOME 43 as minimum version
+ _ensureAsyncMethod(method) {
+ if (this[`${method}Async`])
+ return;
+
+ if (!this[`${method}Remote`])
+ throw new Error(`Missing remote method '${method}'`);
+
+ this[`${method}Async`] = function (...args) {
+ return new Promise((resolve, reject) => {
+ this[`${method}Remote`](...args, (ret, e) => {
+ if (e)
+ reject(e);
+ else
+ resolve(ret);
+ });
+ });
+ };
+ }
+
+ _onSignal() {
+ }
+
+ getProperty(propertyName, cancellable) {
+ return this.gConnection.call(this.gName,
+ this.gObjectPath, 'org.freedesktop.DBus.Properties', 'Get',
+ GLib.Variant.new('(ss)', [this.gInterfaceName, propertyName]),
+ DBusProxy.TUPLE_VARIANT_TYPE, Gio.DBusCallFlags.NONE, -1,
+ cancellable);
+ }
+
+ getProperties(cancellable) {
+ return this.gConnection.call(this.gName,
+ this.gObjectPath, 'org.freedesktop.DBus.Properties', 'GetAll',
+ GLib.Variant.new('(s)', [this.gInterfaceName]),
+ GLib.VariantType.new('(a{sv})'), Gio.DBusCallFlags.NONE, -1,
+ cancellable);
+ }
+});
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusUtils.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusUtils.js
new file mode 100644
index 0000000..d4d7a96
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusUtils.js
@@ -0,0 +1,121 @@
+// 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 Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+import {Logger} from './logger.js';
+
+export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
+
+Gio._promisify(Gio.DBusConnection.prototype, 'call');
+
+export async function getUniqueBusName(bus, name, cancellable) {
+ if (name[0] === ':')
+ return name;
+
+ if (!bus)
+ bus = Gio.DBus.session;
+
+ const variantName = new GLib.Variant('(s)', [name]);
+ const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
+ 'GetNameOwner', variantName, new GLib.VariantType('(s)'),
+ Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack();
+
+ return unique;
+}
+
+export async function getBusNames(bus, cancellable) {
+ if (!bus)
+ bus = Gio.DBus.session;
+
+ const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
+ 'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE,
+ -1, cancellable)).deep_unpack();
+
+ const uniqueNames = new Map();
+ const requests = names.map(name => getUniqueBusName(bus, name, cancellable));
+ const results = await Promise.allSettled(requests);
+
+ for (let i = 0; i < results.length; i++) {
+ const result = results[i];
+ if (result.status === 'fulfilled') {
+ let namesForBus = uniqueNames.get(result.value);
+ if (!namesForBus) {
+ namesForBus = new Set();
+ uniqueNames.set(result.value, namesForBus);
+ }
+ if (result.value !== names[i])
+ namesForBus.add(names[i]);
+ } else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`);
+ }
+ }
+
+ return uniqueNames;
+}
+
+async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) {
+ const res = await bus.call('org.freedesktop.DBus', '/',
+ 'org.freedesktop.DBus', 'GetConnectionUnixProcessID',
+ new GLib.Variant('(s)', [connectionName]),
+ new GLib.VariantType('(u)'),
+ Gio.DBusCallFlags.NONE,
+ -1,
+ cancellable);
+ const [pid] = res.deepUnpack();
+ return pid;
+}
+
+export async function getProcessName(connectionName, cancellable = null,
+ priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) {
+ const pid = await getProcessId(connectionName, cancellable, bus);
+ const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`);
+ const inputStream = await cmdFile.read_async(priority, cancellable);
+ const bytes = await inputStream.read_bytes_async(2048, priority, cancellable);
+ const textDecoder = new TextDecoder();
+ return textDecoder.decode(bytes.toArray().map(v => !v ? 0x20 : v));
+}
+
+export async function* introspectBusObject(bus, name, cancellable,
+ interfaces = undefined, path = undefined) {
+ if (!path)
+ path = '/';
+
+ const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable',
+ 'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE,
+ 5000, cancellable)).deep_unpack();
+
+ const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection);
+
+ if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces))
+ yield {nodeInfo, path};
+
+ if (path === '/')
+ path = '';
+
+ for (const subNodeInfo of nodeInfo.nodes) {
+ const subPath = `${path}/${subNodeInfo.path}`;
+ yield* introspectBusObject(bus, name, cancellable, interfaces, subPath);
+ }
+}
+
+function dbusNodeImplementsInterfaces(nodeInfo, interfaces) {
+ if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces))
+ return false;
+
+ return interfaces.some(iface => nodeInfo.lookup_interface(iface));
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/extension.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/extension.js
new file mode 100644
index 0000000..6321077
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/extension.js
@@ -0,0 +1,90 @@
+// 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 * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
+
+import * as StatusNotifierWatcher from './statusNotifierWatcher.js';
+import * as Interfaces from './interfaces.js';
+import * as TrayIconsManager from './trayIconsManager.js';
+import * as Util from './util.js';
+import {Logger} from './logger.js';
+import {SettingsManager} from './settingsManager.js';
+
+export default class AppIndicatorExtension extends Extension.Extension {
+ constructor(...args) {
+ super(...args);
+
+ Logger.init(this);
+ Interfaces.initialize(this);
+
+ this._isEnabled = false;
+ this._statusNotifierWatcher = null;
+ this._watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME);
+ this._watchDog.connect('vanished', () => this._maybeEnableAfterNameAvailable());
+
+ // HACK: we want to leave the watchdog alive when disabling the extension,
+ // but if we are being reloaded, we destroy it since it could be considered
+ // a leak and spams our log, too.
+ /* eslint-disable no-undef */
+ if (typeof global['--appindicator-extension-on-reload'] === 'function')
+ global['--appindicator-extension-on-reload']();
+
+ global['--appindicator-extension-on-reload'] = () => {
+ Logger.debug('Reload detected, destroying old watchdog');
+ this._watchDog.destroy();
+ this._watchDog = null;
+ };
+ /* eslint-enable no-undef */
+ }
+
+ enable() {
+ this._isEnabled = true;
+ SettingsManager.initialize(this);
+ Util.tryCleanupOldIndicators();
+ this._maybeEnableAfterNameAvailable();
+ TrayIconsManager.TrayIconsManager.initialize();
+ }
+
+ disable() {
+ this._isEnabled = false;
+ TrayIconsManager.TrayIconsManager.destroy();
+
+ if (this._statusNotifierWatcher !== null) {
+ this._statusNotifierWatcher.destroy();
+ this._statusNotifierWatcher = null;
+ }
+
+ SettingsManager.destroy();
+ }
+
+ // FIXME: when entering/leaving the lock screen, the extension might be
+ // enabled/disabled rapidly.
+ // This will create very bad side effects in case we were not done unowning
+ // the name while trying to own it again. Since g_bus_unown_name doesn't
+ // fire any callback when it's done, we need to monitor the bus manually
+ // to find out when the name vanished so we can reclaim it again.
+ _maybeEnableAfterNameAvailable() {
+ // by the time we get called whe might not be enabled
+ if (!this._isEnabled || this._statusNotifierWatcher)
+ return;
+
+ if (this._watchDog.nameAcquired && this._watchDog.nameOnBus)
+ return;
+
+ this._statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher(
+ this, this._watchDog);
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconCache.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconCache.js
new file mode 100644
index 0000000..3610184
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconCache.js
@@ -0,0 +1,179 @@
+// 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 GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+
+import * as PromiseUtils from './promiseUtils.js';
+import * as Util from './util.js';
+
+// The icon cache caches icon objects in case they're reused shortly aftwerwards.
+// This is necessary for some indicators like skype which rapidly switch between serveral icons.
+// Without caching, the garbage collection would never be able to handle the amount of new icon data.
+// If the lifetime of an icon is over, the cache will destroy the icon. (!)
+// The presence of active icons will extend the lifetime.
+
+const GC_INTERVAL = 100; // seconds
+const LIFETIME_TIMESPAN = 120; // seconds
+
+// how to use: see IconCache.add, IconCache.get
+export class IconCache {
+ constructor() {
+ this._cache = new Map();
+ this._activeIcons = Object.create(null);
+ this._lifetime = new Map(); // we don't want to attach lifetime to the object
+ }
+
+ add(id, icon) {
+ if (!(icon instanceof Gio.Icon)) {
+ Util.Logger.critical('IconCache: Only Gio.Icons are supported');
+ return null;
+ }
+
+ if (!id) {
+ Util.Logger.critical('IconCache: Invalid ID provided');
+ return null;
+ }
+
+ const oldIcon = this._cache.get(id);
+ if (!oldIcon || !oldIcon.equal(icon)) {
+ Util.Logger.debug(`IconCache: adding ${id}: ${icon}`);
+ this._cache.set(id, icon);
+ } else {
+ icon = oldIcon;
+ }
+
+ this._renewLifetime(id);
+ this._checkGC();
+
+ return icon;
+ }
+
+ updateActive(iconType, gicon, isActive) {
+ if (!gicon)
+ return;
+
+ const previousActive = this._activeIcons[iconType];
+
+ if (isActive && [...this._cache.values()].some(icon => icon === gicon))
+ this._activeIcons[iconType] = gicon;
+ else if (previousActive === gicon)
+ delete this._activeIcons[iconType];
+ else
+ return;
+
+ if (previousActive) {
+ this._cache.forEach((icon, id) => {
+ if (icon === previousActive)
+ this._renewLifetime(id);
+ });
+ }
+ }
+
+ _remove(id) {
+ Util.Logger.debug(`IconCache: removing ${id}`);
+
+ this._cache.delete(id);
+ this._lifetime.delete(id);
+ }
+
+ _renewLifetime(id) {
+ this._lifetime.set(id, new Date().getTime() + LIFETIME_TIMESPAN * 1000);
+ }
+
+ forceDestroy(id) {
+ const gicon = this._cache.has(id);
+ if (gicon) {
+ Object.keys(this._activeIcons).forEach(iconType =>
+ this.updateActive(iconType, gicon, false));
+ this._remove(id);
+ this._checkGC();
+ }
+ }
+
+ // marks all the icons as removable, if something doesn't claim them before
+ weakClear() {
+ this._activeIcons = Object.create(null);
+ this._checkGC();
+ }
+
+ // removes everything from the cache
+ clear() {
+ this._activeIcons = Object.create(null);
+ this._cache.forEach((_icon, id) => this._remove(id));
+ this._checkGC();
+ }
+
+ // returns an object from the cache, or null if it can't be found.
+ get(id) {
+ const icon = this._cache.get(id);
+ if (icon) {
+ Util.Logger.debug(`IconCache: retrieving ${id}: ${icon}`);
+ this._renewLifetime(id);
+ return icon;
+ }
+
+ return null;
+ }
+
+ async _checkGC() {
+ const cacheIsEmpty = this._cache.size === 0;
+
+ if (!cacheIsEmpty && !this._gcTimeout) {
+ Util.Logger.debug('IconCache: garbage collector started');
+ let anyUnusedInCache = false;
+ this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL,
+ GLib.PRIORITY_LOW);
+ try {
+ await this._gcTimeout;
+ anyUnusedInCache = this._gc();
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e, 'IconCache: garbage collector');
+ } finally {
+ delete this._gcTimeout;
+ }
+
+ if (anyUnusedInCache)
+ this._checkGC();
+ } else if (cacheIsEmpty && this._gcTimeout) {
+ Util.Logger.debug('IconCache: garbage collector stopped');
+ this._gcTimeout.cancel();
+ }
+ }
+
+ _gc() {
+ const time = new Date().getTime();
+ let anyUnused = false;
+
+ this._cache.forEach((icon, id) => {
+ if (Object.values(this._activeIcons).includes(icon)) {
+ Util.Logger.debug(`IconCache: ${id} is in use.`);
+ } else if (this._lifetime.get(id) < time) {
+ this._remove(id);
+ } else {
+ anyUnused = true;
+ Util.Logger.debug(`IconCache: ${id} survived this round.`);
+ }
+ });
+
+ return anyUnused;
+ }
+
+ destroy() {
+ this.clear();
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconTheme.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconTheme.js
new file mode 100644
index 0000000..cdd6540
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconTheme.js
@@ -0,0 +1,12 @@
+let _defaultTheme;
+export function getDefaultTheme() {
+ if (_defaultTheme)
+ return _defaultTheme;
+
+ _defaultTheme = new St.IconTheme();
+ return _defaultTheme;
+}
+
+export function destroyDefaultTheme() {
+ _defaultTheme = null;
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/custom-icons-symbolic.svg b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/custom-icons-symbolic.svg
new file mode 100644
index 0000000..bfc3698
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/custom-icons-symbolic.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ viewBox="0 0 16 16"
+ version="1.1"
+ id="svg2"
+ sodipodi:docname="custom-icons-symbolic.svg"
+ inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="namedview2"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:zoom="31.404053"
+ inkscape:cx="7.4671891"
+ inkscape:cy="8.9001252"
+ inkscape:window-width="1860"
+ inkscape:window-height="1011"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
+ <g
+ fill="#222222"
+ id="g3"
+ transform="translate(-0.16009403)">
+ <path
+ d="m 5.207031,1.292969 c -0.390625,-0.390625 -1.023437,-0.390625 -1.414062,0 l -2.5,2.5 c -0.390625,0.390625 -0.390625,1.023437 0,1.414062 l 2.5,2.5 c 0.390625,0.390625 1.023437,0.390625 1.414062,0 l 2.496094,-2.5 c 0.390625,-0.390625 0.390625,-1.023437 0,-1.414062 z m 0,0"
+ id="path1-3" />
+ <path
+ d="m 10,12 c 0,1.65625 -1.34375,3 -3,3 -1.660156,0 -3,-1.34375 -3,-3 0,-1.65625 1.339844,-3 3,-3 1.65625,0 3,1.34375 3,3 z m 0,0"
+ id="path2-6" />
+ <path
+ d="M 11.945312,2.003906 C 11.605469,2.019531 11.300781,2.207031 11.132812,2.5 L 8.8125,6.5 C 8.425781,7.164062 8.90625,8 9.675781,8 h 4.644531 c 0.769532,0 1.25,-0.835938 0.863282,-1.5 l -2.320313,-4 C 12.675781,2.171875 12.320312,1.980469 11.945312,2.003906 Z m 0,0"
+ id="path3" />
+ </g>
+</svg>
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/general-preferences-symbolic.svg b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/general-preferences-symbolic.svg
new file mode 100644
index 0000000..6334430
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/general-preferences-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M12.5 2A2.5 2.5 0 0 0 10 4.5 2.5 2.5 0 0 0 12.5 7 2.5 2.5 0 0 0 15 4.5 2.5 2.5 0 0 0 12.5 2zm0 1A1.5 1.5 0 0 1 14 4.5 1.5 1.5 0 0 1 12.5 6 1.5 1.5 0 0 1 11 4.5 1.5 1.5 0 0 1 12.5 3zm-9 5A2.5 2.5 0 0 0 1 10.5 2.5 2.5 0 0 0 3.5 13a2.5 2.5 0 0 0 2.443-2H15v-1H5.95A2.5 2.5 0 0 0 3.5 8zm0 1A1.5 1.5 0 0 1 5 10.5 1.5 1.5 0 0 1 3.5 12 1.5 1.5 0 0 1 2 10.5 1.5 1.5 0 0 1 3.5 9z" style="fill:gray;stroke-width:.70228"/><path d="M-9 4h8v1h-8z" style="fill:gray;fill-opacity:1;stroke-width:1.05153;opacity:.5" transform="scale(-1 1)"/></svg> \ No newline at end of file
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js
new file mode 100644
index 0000000..533789e
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js
@@ -0,0 +1,613 @@
+// 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 Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+
+import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Panel from 'resource:///org/gnome/shell/ui/panel.js';
+import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
+
+import * as AppIndicator from './appIndicator.js';
+import * as PromiseUtils from './promiseUtils.js';
+import * as SettingsManager from './settingsManager.js';
+import * as Util from './util.js';
+import * as DBusMenu from './dbusMenu.js';
+
+const DEFAULT_ICON_SIZE = Panel.PANEL_ICON_SIZE || 16;
+
+export function addIconToPanel(statusIcon) {
+ if (!(statusIcon instanceof BaseStatusIcon))
+ throw TypeError(`Unexpected icon type: ${statusIcon}`);
+
+ const settings = SettingsManager.getDefaultGSettings();
+ const indicatorId = `appindicator-${statusIcon.uniqueId}`;
+
+ const currentIcon = Main.panel.statusArea[indicatorId];
+ if (currentIcon) {
+ if (currentIcon !== statusIcon)
+ currentIcon.destroy();
+
+ Main.panel.statusArea[indicatorId] = null;
+ }
+
+ Main.panel.addToStatusArea(indicatorId, statusIcon, 1,
+ settings.get_string('tray-pos'));
+
+ Util.connectSmart(settings, 'changed::tray-pos', statusIcon, () =>
+ addIconToPanel(statusIcon));
+}
+
+export function getTrayIcons() {
+ return Object.values(Main.panel.statusArea).filter(
+ i => i instanceof IndicatorStatusTrayIcon);
+}
+
+export function getAppIndicatorIcons() {
+ return Object.values(Main.panel.statusArea).filter(
+ i => i instanceof IndicatorStatusIcon);
+}
+
+export const BaseStatusIcon = GObject.registerClass(
+class IndicatorBaseStatusIcon extends PanelMenu.Button {
+ _init(menuAlignment, nameText, iconActor, dontCreateMenu) {
+ super._init(menuAlignment, nameText, dontCreateMenu);
+
+ const settings = SettingsManager.getDefaultGSettings();
+ Util.connectSmart(settings, 'changed::icon-opacity', this, this._updateOpacity);
+ this.connect('notify::hover', () => this._onHoverChanged());
+
+ if (!super._onDestroy)
+ this.connect('destroy', () => this._onDestroy());
+
+ this._box = new St.BoxLayout({style_class: 'panel-status-indicators-box'});
+ this.add_child(this._box);
+
+ this._setIconActor(iconActor);
+ this._showIfReady();
+
+ this.set_style(IndicatorBaseStatusIcon.DEFAULT_STYLE);
+ }
+
+ _setIconActor(icon) {
+ if (!(icon instanceof Clutter.Actor))
+ throw new Error(`${icon} is not a valid actor`);
+
+ if (this._icon && this._icon !== icon)
+ this._icon.destroy();
+
+ this._icon = icon;
+ this._updateEffects();
+ this._monitorIconEffects();
+
+ if (this._icon) {
+ this._box.add_child(this._icon);
+ const id = this._icon.connect('destroy', () => {
+ this._icon.disconnect(id);
+ this._icon = null;
+ this._monitorIconEffects();
+ });
+ }
+ }
+
+ _onDestroy() {
+ if (this._icon)
+ this._icon.destroy();
+
+ if (super._onDestroy)
+ super._onDestroy();
+ }
+
+ isReady() {
+ throw new GObject.NotImplementedError('isReady() in %s'.format(this.constructor.name));
+ }
+
+ get icon() {
+ return this._icon;
+ }
+
+ get uniqueId() {
+ throw new GObject.NotImplementedError('uniqueId in %s'.format(this.constructor.name));
+ }
+
+ _showIfReady() {
+ this.visible = this.isReady();
+ }
+
+ _onHoverChanged() {
+ if (this.hover) {
+ this.opacity = 255;
+ if (this._icon)
+ this._icon.remove_effect_by_name('desaturate');
+ } else {
+ this._updateEffects();
+ }
+ }
+
+ _updateOpacity() {
+ const settings = SettingsManager.getDefaultGSettings();
+ const userValue = settings.get_user_value('icon-opacity');
+ if (userValue)
+ this.opacity = userValue.unpack();
+ else
+ this.opacity = 255;
+ }
+
+ _updateEffects() {
+ this._updateOpacity();
+
+ if (this._icon) {
+ this._updateSaturation();
+ this._updateBrightnessContrast();
+ }
+ }
+
+ _monitorIconEffects() {
+ const settings = SettingsManager.getDefaultGSettings();
+ const monitoring = !!this._iconSaturationIds;
+
+ if (!this._icon && monitoring) {
+ Util.disconnectSmart(settings, this, this._iconSaturationIds);
+ delete this._iconSaturationIds;
+
+ Util.disconnectSmart(settings, this, this._iconBrightnessIds);
+ delete this._iconBrightnessIds;
+
+ Util.disconnectSmart(settings, this, this._iconContrastIds);
+ delete this._iconContrastIds;
+
+ Util.disconnectSmart(settings, this, this._compactModeEnabledIds);
+ delete this._compactModeEnabledIds;
+ } else if (this._icon && !monitoring) {
+ this._iconSaturationIds =
+ Util.connectSmart(settings, 'changed::icon-saturation', this,
+ this._updateSaturation);
+ this._iconBrightnessIds =
+ Util.connectSmart(settings, 'changed::icon-brightness', this,
+ this._updateBrightnessContrast);
+ this._iconContrastIds =
+ Util.connectSmart(settings, 'changed::icon-contrast', this,
+ this._updateBrightnessContrast);
+ this._compactModeEnabledIds =
+ Util.connectSmart(settings, 'changed::compact-mode-enabled', this,
+ this._updateCompactMode);
+ }
+ }
+
+ static get DEFAULT_STYLE() {
+ const settings = SettingsManager.getDefaultGSettings();
+ if (!settings.get_boolean('compact-mode-enabled'))
+ return null; // drop to default -natural-hpadding.
+
+ return '-natural-hpadding: 10px';
+ }
+
+ _updateCompactMode() {
+ this._icon.set_style(AppIndicator.IconActor.DEFAULT_STYLE);
+ this.set_style(IndicatorBaseStatusIcon.DEFAULT_STYLE);
+ }
+
+ _updateSaturation() {
+ const settings = SettingsManager.getDefaultGSettings();
+ const desaturationValue = settings.get_double('icon-saturation');
+ let desaturateEffect = this._icon.get_effect('desaturate');
+
+ if (desaturationValue > 0) {
+ if (!desaturateEffect) {
+ desaturateEffect = new Clutter.DesaturateEffect();
+ this._icon.add_effect_with_name('desaturate', desaturateEffect);
+ }
+ desaturateEffect.set_factor(desaturationValue);
+ } else if (desaturateEffect) {
+ this._icon.remove_effect(desaturateEffect);
+ }
+ }
+
+ _updateBrightnessContrast() {
+ const settings = SettingsManager.getDefaultGSettings();
+ const brightnessValue = settings.get_double('icon-brightness');
+ const contrastValue = settings.get_double('icon-contrast');
+ let brightnessContrastEffect = this._icon.get_effect('brightness-contrast');
+
+ if (brightnessValue !== 0 | contrastValue !== 0) {
+ if (!brightnessContrastEffect) {
+ brightnessContrastEffect = new Clutter.BrightnessContrastEffect();
+ this._icon.add_effect_with_name('brightness-contrast', brightnessContrastEffect);
+ }
+ brightnessContrastEffect.set_brightness(brightnessValue);
+ brightnessContrastEffect.set_contrast(contrastValue);
+ } else if (brightnessContrastEffect) {
+ this._icon.remove_effect(brightnessContrastEffect);
+ }
+ }
+});
+
+/*
+ * IndicatorStatusIcon implements an icon in the system status area
+ */
+export const IndicatorStatusIcon = GObject.registerClass(
+class IndicatorStatusIcon extends BaseStatusIcon {
+ _init(indicator) {
+ super._init(0.5, indicator.accessibleName,
+ new AppIndicator.IconActor(indicator, DEFAULT_ICON_SIZE));
+
+ // Disable upstream's click gesture and fall back to vfunc_button_press_event etc.
+ this._clickGesture?.set_enabled(false);
+
+ this._indicator = indicator;
+
+ this._lastClickTime = -1;
+ this._lastClickX = -1;
+ this._lastClickY = -1;
+
+ this._box.add_style_class_name('appindicator-box');
+
+ Util.connectSmart(this._indicator, 'ready', this, this._showIfReady);
+ Util.connectSmart(this._indicator, 'menu', this, this._updateMenu);
+ Util.connectSmart(this._indicator, 'label', this, this._updateLabel);
+ Util.connectSmart(this._indicator, 'status', this, this._updateStatus);
+ Util.connectSmart(this._indicator, 'reset', this, () => {
+ this._updateStatus();
+ this._updateLabel();
+ });
+ Util.connectSmart(this._indicator, 'accessible-name', this, () =>
+ this.set_accessible_name(this._indicator.accessibleName));
+ Util.connectSmart(this._indicator, 'destroy', this, () => this.destroy());
+
+ this.connect('notify::visible', () => this._updateMenu());
+
+ this._showIfReady();
+ }
+
+ _onDestroy() {
+ if (this._menuClient) {
+ this._menuClient.disconnect(this._menuReadyId);
+ this._menuClient.destroy();
+ this._menuClient = null;
+ }
+
+ super._onDestroy();
+ }
+
+ get uniqueId() {
+ return this._indicator.uniqueId;
+ }
+
+ isReady() {
+ return this._indicator && this._indicator.isReady;
+ }
+
+ _updateLabel() {
+ const {label} = this._indicator;
+ if (label) {
+ if (!this._label || !this._labelBin) {
+ this._labelBin = new St.Bin({
+ yAlign: Clutter.ActorAlign.CENTER,
+ });
+ this._label = new St.Label();
+ Util.addActor(this._labelBin, this._label);
+ Util.addActor(this._box, this._labelBin);
+ }
+ this._label.set_text(label);
+ if (!this._box.contains(this._labelBin))
+ Util.addActor(this._box, this._labelBin); // FIXME: why is it suddenly necessary?
+ } else if (this._label) {
+ this._labelBin.destroy_all_children();
+ Util.removeActor(this._box, this._labelBin);
+ this._labelBin.destroy();
+ delete this._labelBin;
+ delete this._label;
+ }
+ }
+
+ _updateStatus() {
+ const wasVisible = this.visible;
+ this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE;
+
+ if (this.visible !== wasVisible)
+ this._indicator.checkAlive().catch(logError);
+ }
+
+ _updateMenu() {
+ if (this._menuClient) {
+ this._menuClient.disconnect(this._menuReadyId);
+ this._menuClient.destroy();
+ this._menuClient = null;
+ this.menu.removeAll();
+ }
+
+ if (this.visible && this._indicator.menuPath) {
+ this._menuClient = new DBusMenu.Client(this._indicator.busName,
+ this._indicator.menuPath, this._indicator);
+
+ if (this._menuClient.isReady)
+ this._menuClient.attachToMenu(this.menu);
+
+ this._menuReadyId = this._menuClient.connect('ready-changed', () => {
+ if (this._menuClient.isReady)
+ this._menuClient.attachToMenu(this.menu);
+ else
+ this._updateMenu();
+ });
+ }
+ }
+
+ _showIfReady() {
+ if (!this.isReady())
+ return;
+
+ this._updateLabel();
+ this._updateStatus();
+ this._updateMenu();
+ }
+
+ _updateClickCount(event) {
+ const [x, y] = event.get_coords();
+ const time = event.get_time();
+ const {doubleClickDistance, doubleClickTime} =
+ Clutter.Settings.get_default();
+
+ if (time > (this._lastClickTime + doubleClickTime) ||
+ (Math.abs(x - this._lastClickX) > doubleClickDistance) ||
+ (Math.abs(y - this._lastClickY) > doubleClickDistance))
+ this._clickCount = 0;
+
+ this._lastClickTime = time;
+ this._lastClickX = x;
+ this._lastClickY = y;
+
+ this._clickCount = (this._clickCount % 2) + 1;
+
+ return this._clickCount;
+ }
+
+ _maybeHandleDoubleClick(event) {
+ if (this._indicator.supportsActivation === false)
+ return Clutter.EVENT_PROPAGATE;
+
+ if (event.get_button() !== Clutter.BUTTON_PRIMARY)
+ return Clutter.EVENT_PROPAGATE;
+
+ if (this._updateClickCount(event) === 2) {
+ this._indicator.open(...event.get_coords(), event.get_time());
+ return Clutter.EVENT_STOP;
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ async _waitForDoubleClick() {
+ const {doubleClickTime} = Clutter.Settings.get_default();
+ this._waitDoubleClickPromise = new PromiseUtils.TimeoutPromise(
+ doubleClickTime);
+
+ try {
+ await this._waitDoubleClickPromise;
+ this.menu.toggle();
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ throw e;
+ } finally {
+ delete this._waitDoubleClickPromise;
+ }
+ }
+
+ vfunc_event(event) {
+ if (this.menu.numMenuItems && event.type() === Clutter.EventType.TOUCH_BEGIN)
+ this.menu.toggle();
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ vfunc_button_press_event(event) {
+ if (this._waitDoubleClickPromise)
+ this._waitDoubleClickPromise.cancel();
+
+ // if middle mouse button clicked send SecondaryActivate dbus event and do not show appindicator menu
+ if (event.get_button() === Clutter.BUTTON_MIDDLE) {
+ if (Main.panel.menuManager.activeMenu)
+ Main.panel.menuManager._closeMenu(true, Main.panel.menuManager.activeMenu);
+ this._indicator.secondaryActivate(event.get_time(), ...event.get_coords());
+ return Clutter.EVENT_STOP;
+ }
+
+ if (event.get_button() === Clutter.BUTTON_SECONDARY) {
+ this.menu.toggle();
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ const doubleClickHandled = this._maybeHandleDoubleClick(event);
+ if (doubleClickHandled === Clutter.EVENT_PROPAGATE &&
+ event.get_button() === Clutter.BUTTON_PRIMARY &&
+ this.menu.numMenuItems) {
+ if (this._indicator.supportsActivation !== false)
+ this._waitForDoubleClick().catch(logError);
+ else
+ this.menu.toggle();
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ vfunc_scroll_event(event) {
+ // Since Clutter 1.10, clutter will always send a smooth scrolling event
+ // with explicit deltas, no matter what input device is used
+ // In fact, for every scroll there will be a smooth and non-smooth scroll
+ // event, and we can choose which one we interpret.
+ if (event.get_scroll_direction() === Clutter.ScrollDirection.SMOOTH) {
+ const [dx, dy] = event.get_scroll_delta();
+
+ this._indicator.scroll(dx, dy);
+ return Clutter.EVENT_STOP;
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+});
+
+export const IndicatorStatusTrayIcon = GObject.registerClass(
+class IndicatorTrayIcon extends BaseStatusIcon {
+ _init(icon) {
+ super._init(0.5, icon.wm_class, icon, {dontCreateMenu: true});
+ Util.Logger.debug(`Adding legacy tray icon ${this.uniqueId}`);
+ this._box.add_style_class_name('appindicator-trayicons-box');
+ this.add_style_class_name('appindicator-icon');
+ this.add_style_class_name('tray-icon');
+
+ this.connect('button-press-event', (_actor, _event) => {
+ this.add_style_pseudo_class('active');
+ return Clutter.EVENT_PROPAGATE;
+ });
+ this.connect('button-release-event', (_actor, event) => {
+ this._icon.click(event);
+ this.remove_style_pseudo_class('active');
+ return Clutter.EVENT_PROPAGATE;
+ });
+ this.connect('key-press-event', (_actor, event) => {
+ this.add_style_pseudo_class('active');
+ this._icon.click(event);
+ return Clutter.EVENT_PROPAGATE;
+ });
+ this.connect('key-release-event', (_actor, event) => {
+ this._icon.click(event);
+ this.remove_style_pseudo_class('active');
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ Util.connectSmart(this._icon, 'destroy', this, () => {
+ icon.clear_effects();
+ this.destroy();
+ });
+
+ const settings = SettingsManager.getDefaultGSettings();
+ Util.connectSmart(settings, 'changed::icon-size', this, this._updateIconSize);
+
+ const themeContext = St.ThemeContext.get_for_stage(global.stage);
+ Util.connectSmart(themeContext, 'notify::scale-factor', this, () =>
+ this._updateIconSize());
+
+ this._updateIconSize();
+ }
+
+ _onDestroy() {
+ Util.Logger.debug(`Destroying legacy tray icon ${this.uniqueId}`);
+
+ if (this._waitDoubleClickPromise)
+ this._waitDoubleClickPromise.cancel();
+
+ super._onDestroy();
+ }
+
+ isReady() {
+ return !!this._icon;
+ }
+
+ get uniqueId() {
+ return `legacy:${this._icon.wm_class}:${this._icon.pid}`;
+ }
+
+ vfunc_navigate_focus(from, direction) {
+ this.grab_key_focus();
+ return super.vfunc_navigate_focus(from, direction);
+ }
+
+ _getSimulatedButtonEvent(touchEvent) {
+ const event = Clutter.Event.new(Clutter.EventType.BUTTON_RELEASE);
+ event.set_button(1);
+ event.set_time(touchEvent.get_time());
+ event.set_flags(touchEvent.get_flags());
+ event.set_stage(global.stage);
+ event.set_source(touchEvent.get_source());
+ event.set_coords(...touchEvent.get_coords());
+ event.set_state(touchEvent.get_state());
+ return event;
+ }
+
+ vfunc_touch_event(event) {
+ // Under X11 we rely on emulated pointer events
+ const isWayland = typeof imports.gi.Meta.is_wayland_compositor === 'function'
+ ? imports.gi.Meta.is_wayland_compositor()
+ : global.display?.is_wayland_compositor?.() ?? true;
+ if (!isWayland)
+ return Clutter.EVENT_PROPAGATE;
+
+ const slot = event.get_event_sequence().get_slot();
+
+ if (!this._touchPressSlot &&
+ event.get_type() === Clutter.EventType.TOUCH_BEGIN) {
+ this.add_style_pseudo_class('active');
+ this._touchButtonEvent = this._getSimulatedButtonEvent(event);
+ this._touchPressSlot = slot;
+ this._touchDelayPromise = new PromiseUtils.TimeoutPromise(
+ AppDisplay.MENU_POPUP_TIMEOUT);
+ this._touchDelayPromise.then(() => {
+ delete this._touchDelayPromise;
+ delete this._touchPressSlot;
+ this._touchButtonEvent.set_button(3);
+ this._icon.click(this._touchButtonEvent);
+ this.remove_style_pseudo_class('active');
+ });
+ } else if (event.get_type() === Clutter.EventType.TOUCH_END &&
+ this._touchPressSlot === slot) {
+ delete this._touchPressSlot;
+ delete this._touchButtonEvent;
+ if (this._touchDelayPromise) {
+ this._touchDelayPromise.cancel();
+ delete this._touchDelayPromise;
+ }
+
+ this._icon.click(this._getSimulatedButtonEvent(event));
+ this.remove_style_pseudo_class('active');
+ } else if (event.get_type() === Clutter.EventType.TOUCH_UPDATE &&
+ this._touchPressSlot === slot) {
+ this.add_style_pseudo_class('active');
+ this._touchButtonEvent = this._getSimulatedButtonEvent(event);
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ vfunc_leave_event(event) {
+ this.remove_style_pseudo_class('active');
+
+ if (this._touchDelayPromise) {
+ this._touchDelayPromise.cancel();
+ delete this._touchDelayPromise;
+ }
+
+ return super.vfunc_leave_event(event);
+ }
+
+ _updateIconSize() {
+ const settings = SettingsManager.getDefaultGSettings();
+ const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
+ let iconSize = settings.get_int('icon-size');
+
+ if (iconSize <= 0)
+ iconSize = DEFAULT_ICON_SIZE;
+
+ this.height = -1;
+ this._icon.set({
+ width: iconSize * scaleFactor,
+ height: iconSize * scaleFactor,
+ xAlign: Clutter.ActorAlign.CENTER,
+ yAlign: Clutter.ActorAlign.CENTER,
+ });
+ }
+});
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml
new file mode 100644
index 0000000..e98e04e
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml
@@ -0,0 +1,66 @@
+<interface name="com.canonical.dbusmenu">
+ <!-- Properties -->
+ <property name="Version" type="u" access="read" />
+ <property name="TextDirection" type="s" access="read" />
+ <property name="Status" type="s" access="read" />
+ <property name="IconThemePath" type="as" access="read" />
+
+ <!-- Functions -->
+ <method name="GetLayout">
+ <arg type="i" name="parentId" direction="in" />
+ <arg type="i" name="recursionDepth" direction="in" />
+ <arg type="as" name="propertyNames" direction="in" />
+ <arg type="u" name="revision" direction="out" />
+ <arg type="(ia{sv}av)" name="layout" direction="out" />
+ </method>
+
+ <method name="GetGroupProperties">
+ <arg type="ai" name="ids" direction="in" />
+ <arg type="as" name="propertyNames" direction="in" />
+ <arg type="a(ia{sv})" name="properties" direction="out" />
+ </method>
+
+ <method name="GetProperty">
+ <arg type="i" name="id" direction="in" />
+ <arg type="s" name="name" direction="in" />
+ <arg type="v" name="value" direction="out" />
+ </method>
+
+ <method name="Event">
+ <arg type="i" name="id" direction="in" />
+ <arg type="s" name="eventId" direction="in" />
+ <arg type="v" name="data" direction="in" />
+ <arg type="u" name="timestamp" direction="in" />
+ </method>
+
+ <method name="EventGroup">
+ <arg type="a(isvu)" name="events" direction="in" />
+ <arg type="ai" name="idErrors" direction="out" />
+ </method>
+
+ <method name="AboutToShow">
+ <arg type="i" name="id" direction="in" />
+ <arg type="b" name="needUpdate" direction="out" />
+ </method>
+
+ <method name="AboutToShowGroup">
+ <arg type="ai" name="ids" direction="in" />
+ <arg type="ai" name="updatesNeeded" direction="out" />
+ <arg type="ai" name="idErrors" direction="out" />
+ </method>
+
+ <!-- Signals
+ <signal name="ItemsPropertiesUpdated">
+ <arg type="a(ia{sv})" name="updatedProps" direction="out" />
+ <arg type="a(ias)" name="removedProps" direction="out" />
+ </signal>
+ <signal name="LayoutUpdated">
+ <arg type="u" name="revision" direction="out" />
+ <arg type="i" name="parent" direction="out" />
+ </signal>
+ <signal name="ItemActivationRequested">
+ <arg type="i" name="id" direction="out" />
+ <arg type="u" name="timestamp" direction="out" />
+ </signal>
+ -->
+</interface>
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml
new file mode 100644
index 0000000..8c36294
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml
@@ -0,0 +1,130 @@
+<!-- Based on:
+ https://invent.kde.org/frameworks/knotifications/-/blob/master/src/org.kde.StatusNotifierItem.xml
+-->
+
+<interface name="org.kde.StatusNotifierItem">
+
+ <property name="Category" type="s" access="read"/>
+ <property name="Id" type="s" access="read"/>
+ <property name="Title" type="s" access="read"/>
+ <property name="Status" type="s" access="read"/>
+ <property name="WindowId" type="i" access="read"/>
+
+ <!-- An additional path to add to the theme search path to find the icons specified above. -->
+ <property name="IconThemePath" type="s" access="read"/>
+ <property name="Menu" type="o" access="read"/>
+ <property name="ItemIsMenu" type="b" access="read"/>
+
+
+ <!-- main icon -->
+ <!-- names are preferred over pixmaps -->
+ <property name="IconName" type="s" access="read"/>
+
+ <!--struct containing width, height and image data-->
+ <property name="IconPixmap" type="a(iiay)" access="read">
+ <annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
+ </property>
+
+ <property name="OverlayIconName" type="s" access="read"/>
+
+ <property name="OverlayIconPixmap" type="a(iiay)" access="read">
+ <annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
+ </property>
+
+
+ <!-- Requesting attention icon -->
+ <property name="AttentionIconName" type="s" access="read"/>
+
+ <!--same definition as image-->
+ <property name="AttentionIconPixmap" type="a(iiay)" access="read">
+ <annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
+ </property>
+
+ <property name="AttentionMovieName" type="s" access="read"/>
+
+
+
+ <!-- tooltip data -->
+
+ <!--(iiay) is an image-->
+ <!-- We disable this as we don't support tooltip, so no need to go through it
+ <property name="ToolTip" type="(sa(iiay)ss)" access="read">
+ <annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusToolTipStruct"/>
+ </property>
+ -->
+
+ <!-- interaction: the systemtray wants the application to do something -->
+ <method name="ContextMenu">
+ <!-- we're passing the coordinates of the icon, so the app knows where to put the popup window -->
+ <arg name="x" type="i" direction="in"/>
+ <arg name="y" type="i" direction="in"/>
+ </method>
+
+ <method name="Activate">
+ <arg name="x" type="i" direction="in"/>
+ <arg name="y" type="i" direction="in"/>
+ </method>
+
+ <method name="ProvideXdgActivationToken">
+ <arg name="token" type="s" direction="in"/>
+ </method>
+
+ <method name="SecondaryActivate">
+ <arg name="x" type="i" direction="in"/>
+ <arg name="y" type="i" direction="in"/>
+ </method>
+
+ <method name="XAyatanaSecondaryActivate">
+ <arg name="timestamp" type="u" direction="in"/>
+ </method>
+
+ <method name="Scroll">
+ <arg name="delta" type="i" direction="in"/>
+ <arg name="orientation" type="s" direction="in"/>
+ </method>
+
+ <!-- Signals: the client wants to change something in the status
+ <signal name="NewTitle">
+ </signal>
+
+ <signal name="NewIcon">
+ </signal>
+
+ <signal name="NewAttentionIcon">
+ </signal>
+
+ <signal name="NewOverlayIcon">
+ </signal>
+ -->
+ <!-- We disable this as we don't support tooltip, so no need to go through it
+ <signal name="NewToolTip">
+ </signal>
+ -->
+
+ <!--
+ <signal name="NewStatus">
+ <arg name="status" type="s"/>
+ </signal>
+ -->
+
+
+ <!-- The following items are not supported by specs, but widely used
+ <signal name="NewIconThemePath">
+ <arg type="s" name="icon_theme_path" direction="out" />
+ </signal>
+
+ <signal name="NewMenu"></signal>
+ -->
+
+ <!-- ayatana labels -->
+ <!-- These are commented out because GDBusProxy would otherwise require them,
+ but they are not available for KDE indicators
+ -->
+ <!--<signal name="XAyatanaNewLabel">
+ <arg type="s" name="label" direction="out" />
+ <arg type="s" name="guide" direction="out" />
+ </signal>
+ <property name="XAyatanaLabel" type="s" access="read" />
+ <property name="XAyatanaLabelGuide" type="s" access="read" />-->
+
+</interface>
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml
new file mode 100644
index 0000000..e450d04
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml
@@ -0,0 +1,38 @@
+<interface name="org.kde.StatusNotifierWatcher">
+
+ <!-- methods -->
+ <method name="RegisterStatusNotifierItem">
+ <arg name="service" type="s" direction="in"/>
+ </method>
+
+ <method name="RegisterStatusNotifierHost">
+ <arg name="service" type="s" direction="in"/>
+ </method>
+
+
+ <!-- properties -->
+
+ <property name="RegisteredStatusNotifierItems" type="as" access="read">
+ <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/>
+ </property>
+
+ <property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
+
+ <property name="ProtocolVersion" type="i" access="read"/>
+
+
+ <!-- signals -->
+ <signal name="StatusNotifierItemRegistered">
+ <arg type="s"/>
+ </signal>
+
+ <signal name="StatusNotifierItemUnregistered">
+ <arg type="s"/>
+ </signal>
+
+ <signal name="StatusNotifierHostRegistered">
+ </signal>
+
+ <signal name="StatusNotifierHostUnregistered">
+ </signal>
+</interface>
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces.js
new file mode 100644
index 0000000..0a97907
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces.js
@@ -0,0 +1,52 @@
+// 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.
+
+export let StatusNotifierItem = null;
+export let StatusNotifierWatcher = null;
+export let DBusMenu = null;
+
+// loads a xml file into an in-memory string
+function loadInterfaceXml(extension, filename) {
+ const interfacesDir = extension.dir.get_child('interfaces-xml');
+ const file = interfacesDir.get_child(filename);
+ const [result, contents] = imports.gi.GLib.file_get_contents(file.get_path());
+
+ if (result) {
+ // HACK: The "" + trick is important as hell because file_get_contents returns
+ // an object (WTF?) but Gio.makeProxyWrapper requires `typeof() === "string"`
+ // Otherwise, it will try to check `instanceof XML` and fail miserably because there
+ // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough,
+ // will spit out a TypeError soon).
+ let nodeContents = contents;
+ if (contents instanceof Uint8Array)
+ nodeContents = imports.byteArray.toString(contents);
+ return `<node>${nodeContents}</node>`;
+ } else {
+ throw new Error(`AppIndicatorSupport: Could not load file: ${filename}`);
+ }
+}
+
+export function initialize(extension) {
+ StatusNotifierItem = loadInterfaceXml(extension, 'StatusNotifierItem.xml');
+ StatusNotifierWatcher = loadInterfaceXml(extension, 'StatusNotifierWatcher.xml');
+ DBusMenu = loadInterfaceXml(extension, 'DBusMenu.xml');
+}
+
+export function destroy() {
+ StatusNotifierItem = null;
+ StatusNotifierWatcher = null;
+ DBusMenu = null;
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ar/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ar/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..6c73616
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ar/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/bg/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/bg/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..2289e6d
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/bg/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..4cedb41
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..6edb419
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..ba0d667
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..ffbe4ed
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..8b4b367
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..d22309b
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ka/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ka/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..0773c28
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ka/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..6d33af8
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..9d81088
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..fe31bca
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..4536d6e
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..0bb1b44
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..b4dfbe5
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..060d9c3
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..a7770c5
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..af2179f
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..9b6e5a6
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..d31e5f2
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/uk/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/uk/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..88c7fbc
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/uk/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..df28844
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_TW/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_TW/LC_MESSAGES/AppIndicatorExtension.mo
new file mode 100644
index 0000000..f66fd67
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_TW/LC_MESSAGES/AppIndicatorExtension.mo
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/logger.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/logger.js
new file mode 100644
index 0000000..3c60400
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/logger.js
@@ -0,0 +1,105 @@
+// 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 GLib from 'gi://GLib';
+
+/**
+ * Helper class for logging stuff
+ */
+export class Logger {
+ static _logStructured(logLevel, message, extraFields = {}) {
+ if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) {
+ Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING,
+ 'logLevel is not a valid GLib.LogLevelFlags');
+ return;
+ }
+
+ if (!Logger._levels.includes(logLevel))
+ return;
+
+ let fields = {
+ 'SYSLOG_IDENTIFIER': Logger._uuid,
+ 'MESSAGE': `${message}`,
+ };
+
+ let thisFile = null;
+ const {stack} = new Error();
+ for (let stackLine of stack.split('\n')) {
+ stackLine = stackLine.replace('resource:///org/gnome/Shell/', '');
+ const [code, line] = stackLine.split(':');
+ const [func, file] = code.split(/@(.+)/);
+
+ if (!thisFile || thisFile === file) {
+ thisFile = file;
+ continue;
+ }
+
+ fields = Object.assign(fields, {
+ 'CODE_FILE': file || '',
+ 'CODE_LINE': line || '',
+ 'CODE_FUNC': func || '',
+ });
+
+ break;
+ }
+
+ GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
+ }
+
+ static init(extension) {
+ if (Logger._domain)
+ return;
+
+ const allLevels = Object.values(GLib.LogLevelFlags);
+ const domains = GLib.getenv('G_MESSAGES_DEBUG');
+ const {name: domain} = extension.metadata;
+ Logger._uuid = extension.metadata.uuid;
+ Logger._domain = domain.replaceAll(' ', '-');
+
+ if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) {
+ Logger._levels = allLevels;
+ } else {
+ Logger._levels = allLevels.filter(
+ l => l <= GLib.LogLevelFlags.LEVEL_WARNING);
+ }
+ }
+
+ static destroy() {
+ delete Logger._domain;
+ delete Logger._uuid;
+ delete Logger._levels;
+ }
+
+ static debug(message) {
+ Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message);
+ }
+
+ static message(message) {
+ Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message);
+ }
+
+ static warn(message) {
+ Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message);
+ }
+
+ static error(message) {
+ Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message);
+ }
+
+ static critical(message) {
+ Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message);
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/metadata.json b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/metadata.json
new file mode 100644
index 0000000..96e3e2d
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/metadata.json
@@ -0,0 +1,18 @@
+{
+ "_generated": "Generated by SweetTooth, do not edit",
+ "description": "Adds AppIndicator, KStatusNotifierItem and legacy Tray icons support to the Shell",
+ "gettext-domain": "AppIndicatorExtension",
+ "name": "AppIndicator and KStatusNotifierItem Support",
+ "settings-schema": "org.gnome.shell.extensions.appindicator",
+ "shell-version": [
+ "45",
+ "46",
+ "47",
+ "48",
+ "49",
+ "50"
+ ],
+ "url": "https://github.com/ubuntu/gnome-shell-extension-appindicator",
+ "uuid": "appindicatorsupport@rgcjonas.gmail.com",
+ "version": 64
+} \ No newline at end of file
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js
new file mode 100644
index 0000000..ce4c3f3
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js
@@ -0,0 +1,68 @@
+// 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.
+
+export function argbToRgba(src) {
+ const dest = new Uint8Array(src.length);
+
+ for (let j = 0; j < src.length; j += 4) {
+ const srcAlpha = src[j];
+
+ dest[j] = src[j + 1]; /* red */
+ dest[j + 1] = src[j + 2]; /* green */
+ dest[j + 2] = src[j + 3]; /* blue */
+ dest[j + 3] = srcAlpha; /* alpha */
+ }
+
+ return dest;
+}
+
+export function getBestPixmap(pixmapsVariant, preferredSize) {
+ if (!pixmapsVariant)
+ throw new TypeError('null pixmapsVariant');
+
+ const pixmapsVariantsArray = new Array(pixmapsVariant.n_children());
+ if (!pixmapsVariantsArray.length)
+ throw TypeError('Empty Icon found');
+
+ for (let i = 0; i < pixmapsVariantsArray.length; ++i)
+ pixmapsVariantsArray[i] = pixmapsVariant.get_child_value(i);
+
+ const pixmapsSizedArray = pixmapsVariantsArray.map((pixmapVariant, index) => ({
+ width: pixmapVariant.get_child_value(0).unpack(),
+ height: pixmapVariant.get_child_value(1).unpack(),
+ index,
+ }));
+
+ const sortedIconPixmapArray = pixmapsSizedArray.sort(
+ ({width: widthA, height: heightA}, {width: widthB, height: heightB}) => {
+ const areaA = widthA * heightA;
+ const areaB = widthB * heightB;
+
+ return areaA - areaB;
+ });
+
+ // we prefer any pixmap that is equal or bigger than our requested size
+ const qualifiedIconPixmapArray = sortedIconPixmapArray.filter(({width, height}) =>
+ width >= preferredSize && height >= preferredSize);
+
+ const {width, height, index} = qualifiedIconPixmapArray.length > 0
+ ? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop();
+
+ const pixmapVariant = pixmapsVariantsArray[index].get_child_value(2);
+ const rowStride = width * 4; // hopefully this is correct
+
+ return {pixmapVariant, width, height, rowStride};
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/customIconPage.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/customIconPage.js
new file mode 100644
index 0000000..b132562
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/customIconPage.js
@@ -0,0 +1,240 @@
+/* exported GeneralPage*/
+
+import Adw from 'gi://Adw';
+import Gtk from 'gi://Gtk';
+import GObject from 'gi://GObject';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+const IconData = GObject.registerClass({
+ GTypeName: 'IconData',
+ Properties: {
+ 'id': GObject.ParamSpec.string(
+ 'id',
+ 'Id',
+ 'A read and write string property',
+ GObject.ParamFlags.READWRITE,
+ ''
+ ),
+ 'name': GObject.ParamSpec.string(
+ 'name',
+ 'Name',
+ 'A read and write string property',
+ GObject.ParamFlags.READWRITE,
+ ''
+ ),
+ 'at-name': GObject.ParamSpec.string(
+ 'at-name',
+ 'At Name',
+ 'A read and write string property',
+ GObject.ParamFlags.READWRITE,
+ ''
+ ),
+
+ },
+}, class IconData extends GObject.Object {
+ constructor(props = {}) {
+ super(props);
+ }
+
+ get id() {
+ if (this._id === 'undefined')
+ this._id = null;
+ return this._id;
+ }
+
+ set id(value) {
+ if (this.id === value)
+ return;
+
+ this._id = value;
+ this.notify('id');
+ }
+
+ get name() {
+ if (this._name === 'undefined')
+ this._name = null;
+ return this._name;
+ }
+
+ set name(value) {
+ if (this.name === value)
+ return;
+
+ this._name = value;
+ this.notify('name');
+ }
+
+ get atName() {
+ if (this._atName === 'undefined')
+ this._atName = null;
+ return this._atName;
+ }
+
+ set atName(value) {
+ if (this.atName === value)
+ return;
+
+ this._atName = value;
+ this.notify('at-name');
+ }
+});
+
+export var CustomIconPage = GObject.registerClass(
+class AppIndicatorCustomIconPage extends Adw.PreferencesPage {
+ _init(settings, settingsKey) {
+ super._init({
+ title: _('Custom Icons'),
+ icon_name: 'custom-icons-symbolic',
+ name: 'Custom Icons Page',
+ });
+ this._settings = settings;
+ this._settingsKey = settingsKey;
+
+ this.group = new Adw.PreferencesGroup();
+
+ this.container = new Gtk.Box({
+ halign: Gtk.Align.FILL,
+ hexpand: true,
+ orientation: Gtk.Orientation.VERTICAL,
+ });
+
+ this.listStore = new Gio.ListStore({
+ item_type: IconData,
+ });
+
+ const initList = this._settings.get_value(this._settingsKey.CUSTOM_ICONS).deep_unpack();
+ initList.forEach(pair => {
+ let data = new IconData({
+ id: pair[0],
+ name: pair[1],
+ atName: pair[2],
+ });
+ this.listStore.append(data);
+ });
+
+ this.listStore.append(new IconData({id: '', name: '', atName: ''}));
+
+ this.selectionModel = new Gtk.SingleSelection({
+ model: this.listStore,
+ });
+
+ this.columnView = new Gtk.ColumnView({
+ hexpand: true,
+ model: this.selectionModel,
+ reorderable: false,
+ });
+
+ const columnTitles = [
+ _('Indicator ID'),
+ _('Icon Name'),
+ _('Attention Icon Name'),
+ ];
+ const indicatorIdColumn = new Gtk.ColumnViewColumn({
+ title: columnTitles[0],
+ expand: true,
+ });
+ const iconNameColumn = new Gtk.ColumnViewColumn({
+ title: columnTitles[1],
+ expand: true,
+ });
+ const attentionIconNameColumn = new Gtk.ColumnViewColumn({
+ title: columnTitles[2],
+ expand: true,
+ });
+
+
+ const factoryId = new Gtk.SignalListItemFactory();
+ factoryId.connect('setup', (_widget, item) => {
+ const label = new Gtk.EditableLabel({text: ''});
+ item.set_child(label);
+ });
+ factoryId.connect('bind', (_widget, item) => {
+ const label = item.get_child();
+ const data = item.get_item();
+ label.set_text(data.id);
+ label.bind_property('text', data, 'id', GObject.BindingFlags.SYNC_CREATE);
+ label.connect('changed', () => this._autoSave(item));
+ });
+
+ const factoryName = new Gtk.SignalListItemFactory();
+ factoryName.connect('setup', (_widget, item) => {
+ const label = new Gtk.EditableLabel({text: ''});
+ item.set_child(label);
+ });
+ factoryName.connect('bind', (_widget, item) => {
+ const label = item.get_child();
+ const data = item.get_item();
+ label.set_text(data.name);
+ label.bind_property('text', data, 'name', GObject.BindingFlags.SYNC_CREATE);
+ label.connect('changed', () => this._autoSave(item));
+ });
+
+ const factoryItName = new Gtk.SignalListItemFactory();
+ factoryItName.connect('setup', (_widget, item) => {
+ const label = new Gtk.EditableLabel({text: ''});
+ item.set_child(label);
+ });
+ factoryItName.connect('bind', (_widget, item) => {
+ const label = item.get_child();
+ const data = item.get_item();
+ label.set_text(data.atName);
+ label.bind_property('text', data, 'at-name', GObject.BindingFlags.SYNC_CREATE);
+ label.connect('changed', () => this._autoSave(item));
+ });
+
+ indicatorIdColumn.set_factory(factoryId);
+ iconNameColumn.set_factory(factoryName);
+ attentionIconNameColumn.set_factory(factoryItName);
+
+ this.columnView.append_column(indicatorIdColumn);
+ this.columnView.append_column(iconNameColumn);
+ this.columnView.append_column(attentionIconNameColumn);
+
+ this.container.append(this.columnView);
+
+ this.group.add(this.container);
+ this.add(this.group);
+ }
+
+ _autoSave(item) {
+ const storeLength = this.listStore.get_n_items();
+ const newStore = [];
+
+ for (let i = 0; i < storeLength; i++) {
+ const currentItem = this.listStore.get_item(i);
+ const id = currentItem.id;
+ const name = currentItem.name;
+ const atName = currentItem.atName;
+ if (id && name)
+ newStore.push([id, name, atName || '']);
+ }
+
+ this._settings.set_value(this._settingsKey.CUSTOM_ICONS,
+ new GLib.Variant('a(sss)', newStore));
+
+ const id = item.get_item().id;
+ const name = item.get_item().name;
+
+ /* dynamic new entry*/
+ if (storeLength === 1 && (id || name))
+ this.listStore.append(new IconData({id: '', name: '', atName: ''}));
+
+ if (storeLength > 1) {
+ if ((id || name) && item.get_position() >= storeLength - 1)
+ this.listStore.append(new IconData({id: '', name: '', atName: ''}));
+ }
+ }
+
+ _autoDelete(item) {
+ const storeLength = this.listStore.get_n_items();
+ const id = item.get_item().id;
+ const name = item.get_item().name;
+ if (storeLength > 1 && !id && !name &&
+ item.get_position() < storeLength - 1)
+ this.listStore.remove(item.get_position());
+ }
+});
+
+
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/generalPage.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/generalPage.js
new file mode 100644
index 0000000..7a2575b
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/generalPage.js
@@ -0,0 +1,159 @@
+/* exported GeneralPage*/
+
+import Adw from 'gi://Adw';
+import Gtk from 'gi://Gtk';
+import GObject from 'gi://GObject';
+import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+
+export var GeneralPage = GObject.registerClass(
+class AppIndicatorGeneralPage extends Adw.PreferencesPage {
+ _init(settings, settingsKey) {
+ super._init({
+ title: _('General'),
+ icon_name: 'general-preferences-symbolic',
+ name: 'General Page',
+ });
+
+ this._settings = settings;
+ this._settingsKey = settingsKey;
+
+ this.group = new Adw.PreferencesGroup(); // no title since only single group
+
+ const legacyTraySwitch = new Adw.SwitchRow({
+ title: _('Enable Legacy Tray Icons support'),
+ subtitle: _('Add X11 legacy tray icons to the panel area'),
+ active: this._settings.get_boolean(this._settingsKey.LEGACY_TRAY_ENABLED),
+ });
+
+ legacyTraySwitch.connect('notify::active', widget =>
+ this._settings.set_boolean(
+ this._settingsKey.LEGACY_TRAY_ENABLED,
+ widget.get_active()
+ )
+ );
+
+ this.group.add(legacyTraySwitch);
+
+ const compactModeSwitch = new Adw.SwitchRow({
+ title: _('Compact Mode'),
+ subtitle: _('Puts tray indicators closer together'),
+ active: this._settings.get_boolean(this._settingsKey.COMPACT_MODE_ENABLED),
+ });
+
+ compactModeSwitch.connect('notify::active', widget =>
+ this._settings.set_boolean(
+ this._settingsKey.COMPACT_MODE_ENABLED,
+ widget.get_active()
+ )
+ );
+
+ this.group.add(compactModeSwitch);
+
+ this._createSpinRow({
+ title: _('Opacity'),
+ settingsKey: this._settingsKey.ICON_OPACITY,
+ from: 0,
+ to: 255,
+ step: 1,
+ round: true,
+ });
+
+ this._createSpinRow({
+ title: _('Desaturation'),
+ settingsKey: this._settingsKey.ICON_SATURATION,
+ from: 0.0,
+ to: 1.0,
+ step: 0.1,
+ });
+
+ this._createSpinRow({
+ title: _('Brightness'),
+ settingsKey: this._settingsKey.ICON_BRIGHTNESS,
+ from: -1.0,
+ to: 1.0,
+ step: 0.1,
+ });
+
+ this._createSpinRow({
+ title: _('Contrast'),
+ settingsKey: this._settingsKey.ICON_CONTRAST,
+ from: -1.0,
+ to: 1.0,
+ step: 0.1,
+ });
+
+ this._createSpinRow({
+ title: _('Icon Size'),
+ settingsKey: this._settingsKey.ICON_SIZE,
+ from: 0,
+ to: 96,
+ step: 2,
+ round: true,
+ });
+
+ const alignmentList = new Gtk.StringList();
+ const comboItems = [
+ {pos: 'center', label: _('Center')},
+ {pos: 'left', label: _('Left')},
+ {pos: 'right', label: _('Right')},
+ ];
+
+ comboItems.forEach(item => alignmentList.append(item.label));
+
+ const combo = new Adw.ComboRow({
+ title: _('Tray Horizontal Alignment'),
+ model: alignmentList,
+ });
+ const trayPos = this._settings.get_string(this._settingsKey.TRAY_POS);
+ combo.set_selected(comboItems.findIndex(item => trayPos === item.pos));
+
+ combo.connect('notify::selected', widget => this._settings.set_string(
+ this._settingsKey.TRAY_POS, comboItems[widget.get_selected()].pos));
+
+ this.group.add(combo);
+ this.add(this.group);
+ }
+
+ _createSpinRow(args) {
+ let title = args.title || 'Default Title';
+ let subtitle = args.subtitle || null;
+ let settingsKey = args.settingsKey || '';
+ let from = args.from || 0;
+ let to = args.to || 100;
+ let step = args.step || 1;
+ let round = args.round || false;
+
+ let spin = Adw.SpinRow.new_with_range(from, to, step);
+ spin.title = title;
+ if (subtitle !== null)
+ spin.subtitle = subtitle;
+
+ if (round)
+ spin.set_value(this._settings.get_int(settingsKey));
+ else
+ spin.set_value(this._settings.get_int(settingsKey));
+
+
+ spin.connect('input', widget => {
+ if (round)
+ this._settings.set_int(settingsKey, parseInt(widget.get_value(), 10));
+ else
+ this._settings.set_double(settingsKey, widget.get_value());
+
+ return false;
+ });
+ spin.connect('output', widget => {
+ if (round)
+ this._settings.set_int(settingsKey, parseInt(widget.get_value(), 10));
+ else
+ this._settings.set_double(settingsKey, widget.get_value());
+
+ return false;
+ });
+
+ this.group.add(spin);
+ }
+});
+
+
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/prefs.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/prefs.js
new file mode 100644
index 0000000..344ba0a
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/prefs.js
@@ -0,0 +1,45 @@
+// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
+
+/* exported init, buildPrefsWidget */
+
+import Gtk from 'gi://Gtk'; // will be removed
+import Gdk from 'gi://Gdk';
+import * as GeneralPreferences from './preferences/generalPage.js';
+import * as CustomIconPreferences from './preferences/customIconPage.js';
+
+import {
+ ExtensionPreferences,
+ gettext as _
+} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+const SettingsKey = {
+ LEGACY_TRAY_ENABLED: 'legacy-tray-enabled',
+ COMPACT_MODE_ENABLED: 'compact-mode-enabled',
+ ICON_SIZE: 'icon-size',
+ ICON_OPACITY: 'icon-opacity',
+ ICON_SATURATION: 'icon-saturation',
+ ICON_BRIGHTNESS: 'icon-brightness',
+ ICON_CONTRAST: 'icon-contrast',
+ TRAY_POS: 'tray-pos',
+ CUSTOM_ICONS: 'custom-icons',
+};
+
+export default class DockPreferences extends ExtensionPreferences {
+ fillPreferencesWindow(window) {
+ const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default());
+ if (!iconTheme.get_search_path().includes(`${this.path}/icons`))
+ iconTheme.add_search_path(`${this.path}/icons`);
+
+
+ const settings = this.getSettings();
+ const generalPage = new GeneralPreferences.GeneralPage(settings, SettingsKey);
+ const customIconPage = new CustomIconPreferences.CustomIconPage(settings, SettingsKey);
+
+ window.add(generalPage);
+ window.add(customIconPage);
+
+ window.connect('close-request', () => {
+ window.destroy();
+ });
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js
new file mode 100644
index 0000000..2749b03
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js
@@ -0,0 +1,324 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://GdkPixbuf';
+
+import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
+
+export class CancellablePromise extends Promise {
+ constructor(executor, cancellable) {
+ if (!(executor instanceof Function))
+ throw TypeError('executor is not a function');
+
+ if (cancellable && !(cancellable instanceof Gio.Cancellable))
+ throw TypeError('cancellable parameter is not a Gio.Cancellable');
+
+ let rejector;
+ let resolver;
+ super((resolve, reject) => {
+ resolver = resolve;
+ rejector = reject;
+ });
+
+ const {stack: promiseStack} = new Error();
+ this._promiseStack = promiseStack;
+
+ this._resolver = (...args) => {
+ resolver(...args);
+ this._resolved = true;
+ this._cleanup();
+ };
+ this._rejector = (...args) => {
+ rejector(...args);
+ this._rejected = true;
+ this._cleanup();
+ };
+
+ if (!cancellable) {
+ executor(this._resolver, this._rejector);
+ return;
+ }
+
+ this._cancellable = cancellable;
+ this._cancelled = cancellable.is_cancelled();
+ if (this._cancelled) {
+ this._rejector(new GLib.Error(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.CANCELLED, 'Promise cancelled'));
+ return;
+ }
+
+ this._cancellationId = cancellable.connect(() => {
+ const id = this._cancellationId;
+ this._cancellationId = 0;
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => cancellable.disconnect(id));
+ this.cancel();
+ });
+
+ executor(this._resolver, this._rejector);
+ }
+
+ _cleanup() {
+ if (this._cancellationId)
+ this._cancellable.disconnect(this._cancellationId);
+ }
+
+ get cancellable() {
+ return this._chainRoot._cancellable || null;
+ }
+
+ get _chainRoot() {
+ return this._root ? this._root : this;
+ }
+
+ then(...args) {
+ const ret = super.then(...args);
+
+ /* Every time we call then() on this promise we'd get a new
+ * CancellablePromise however that won't have the properties that the
+ * root one has set, and then it won't be possible to cancel a promise
+ * chain from the last one.
+ * To allow this we keep track of the root promise, make sure that
+ * the same method on the root object is called during cancellation
+ * or any destruction method if you want this to work. */
+ if (ret instanceof CancellablePromise)
+ ret._root = this._chainRoot;
+
+ return ret;
+ }
+
+ resolved() {
+ return !!this._chainRoot._resolved;
+ }
+
+ rejected() {
+ return !!this._chainRoot._rejected;
+ }
+
+ cancelled() {
+ return !!this._chainRoot._cancelled;
+ }
+
+ pending() {
+ return !this.resolved() && !this.rejected();
+ }
+
+ cancel() {
+ if (this._root) {
+ this._root.cancel();
+ return this;
+ }
+
+ if (!this.pending())
+ return this;
+
+ this._cancelled = true;
+ const error = new GLib.Error(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.CANCELLED, 'Promise cancelled');
+ error.stack += `## Promise created at:\n${this._promiseStack}`;
+ this._rejector(error);
+
+ return this;
+ }
+}
+
+export class SignalConnectionPromise extends CancellablePromise {
+ constructor(object, signal, cancellable) {
+ if (arguments.length === 1 && object instanceof Function) {
+ super(object);
+ return;
+ }
+
+ if (!(object.connect instanceof Function))
+ throw new TypeError('Not a valid object');
+
+ if (object instanceof GObject.Object &&
+ !GObject.signal_lookup(signal.split(':')[0], object.constructor.$gtype))
+ throw new TypeError(`Signal ${signal} not found on object ${object}`);
+
+ let id;
+ let destroyId;
+ super(resolve => {
+ let connectSignal;
+ if (object instanceof GObject.Object)
+ connectSignal = (sig, cb) => GObject.signal_connect(object, sig, cb);
+ else
+ connectSignal = (sig, cb) => object.connect(sig, cb);
+
+ id = connectSignal(signal, (_obj, ...args) => {
+ if (!args.length)
+ resolve();
+ else
+ resolve(args.length === 1 ? args[0] : args);
+ });
+
+ if (signal !== 'destroy' &&
+ (!(object instanceof GObject.Object) ||
+ GObject.signal_lookup('destroy', object.constructor.$gtype)))
+ destroyId = connectSignal('destroy', () => this.cancel());
+ }, cancellable);
+
+ this._object = object;
+ this._id = id;
+ this._destroyId = destroyId;
+ }
+
+ _cleanup() {
+ if (this._id) {
+ let disconnectSignal;
+
+ if (this._object instanceof GObject.Object)
+ disconnectSignal = id => GObject.signal_handler_disconnect(this._object, id);
+ else
+ disconnectSignal = id => this._object.disconnect(id);
+
+ disconnectSignal(this._id);
+ if (this._destroyId) {
+ disconnectSignal(this._destroyId);
+ this._destroyId = 0;
+ }
+ this._object = null;
+ this._id = 0;
+ }
+
+ super._cleanup();
+ }
+
+ get object() {
+ return this._chainRoot._object;
+ }
+}
+
+export class GSourcePromise extends CancellablePromise {
+ constructor(gsource, priority, cancellable) {
+ if (arguments.length === 1 && gsource instanceof Function) {
+ super(gsource);
+ return;
+ }
+
+ if (gsource.constructor.$gtype !== GLib.Source.$gtype)
+ throw new TypeError(`gsource ${gsource} is not of type GLib.Source`);
+
+ if (priority === undefined)
+ priority = GLib.PRIORITY_DEFAULT;
+ else if (!Number.isInteger(priority))
+ throw TypeError('Invalid priority');
+
+ super(resolve => {
+ gsource.set_priority(priority);
+ gsource.set_callback(() => {
+ resolve();
+ return GLib.SOURCE_REMOVE;
+ });
+ gsource.attach(null);
+ }, cancellable);
+
+ this._gsource = gsource;
+ this._gsource.set_name(`[gnome-shell] ${this.constructor.name} ${
+ new Error().stack.split('\n').filter(line =>
+ !line.match(/misc\/promiseUtils\.js/))[0]}`);
+
+ if (this.rejected())
+ this._gsource.destroy();
+ }
+
+ get gsource() {
+ return this._chainRoot._gsource;
+ }
+
+ _cleanup() {
+ if (this._gsource) {
+ this._gsource.destroy();
+ this._gsource = null;
+ }
+ super._cleanup();
+ }
+}
+
+export class IdlePromise extends GSourcePromise {
+ constructor(priority, cancellable) {
+ if (arguments.length === 1 && priority instanceof Function) {
+ super(priority);
+ return;
+ }
+
+ if (priority === undefined)
+ priority = GLib.PRIORITY_DEFAULT_IDLE;
+
+ super(GLib.idle_source_new(), priority, cancellable);
+ }
+}
+
+export class TimeoutPromise extends GSourcePromise {
+ constructor(interval, priority, cancellable) {
+ if (arguments.length === 1 && interval instanceof Function) {
+ super(interval);
+ return;
+ }
+
+ if (!Number.isInteger(interval) || interval < 0)
+ throw TypeError('Invalid interval');
+
+ super(GLib.timeout_source_new(interval), priority, cancellable);
+ }
+}
+
+export class TimeoutSecondsPromise extends GSourcePromise {
+ constructor(interval, priority, cancellable) {
+ if (arguments.length === 1 && interval instanceof Function) {
+ super(interval);
+ return;
+ }
+
+ if (!Number.isInteger(interval) || interval < 0)
+ throw TypeError('Invalid interval');
+
+ super(GLib.timeout_source_new_seconds(interval), priority, cancellable);
+ }
+}
+
+export class MetaLaterPromise extends CancellablePromise {
+ constructor(laterType, cancellable) {
+ if (arguments.length === 1 && laterType instanceof Function) {
+ super(laterType);
+ return;
+ }
+
+ if (laterType && laterType.constructor.$gtype !== Meta.LaterType.$gtype)
+ throw new TypeError(`laterType ${laterType} is not of type Meta.LaterType`);
+ else if (!laterType)
+ laterType = Meta.LaterType.BEFORE_REDRAW;
+
+ let id;
+ super(resolve => {
+ id = Meta.later_add(laterType, () => {
+ this.remove();
+ resolve();
+ return GLib.SOURCE_REMOVE;
+ });
+ }, cancellable);
+
+ this._id = id;
+ }
+
+ _cleanup() {
+ if (this._id) {
+ Meta.later_remove(this._id);
+ this._id = 0;
+ }
+ super._cleanup();
+ }
+}
+
+export function _promisifySignals(proto) {
+ if (proto.connect_once)
+ return;
+
+ proto.connect_once = function (signal, cancellable) {
+ return new SignalConnectionPromise(this, signal, cancellable);
+ };
+}
+
+_promisifySignals(GObject.Object.prototype);
+_promisifySignals(Signals.EventEmitter.prototype);
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled
new file mode 100644
index 0000000..1a6beff
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled
Binary files differ
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml
new file mode 100644
index 0000000..c303489
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml
@@ -0,0 +1,49 @@
+<schemalist gettext-domain="AppIndicatorExtension">
+ <schema id="org.gnome.shell.extensions.appindicator" path="/org/gnome/shell/extensions/appindicator/">
+ <key name="legacy-tray-enabled" type="b">
+ <default>true</default>
+ <summary>Enable legacy tray icons support</summary>
+ </key>
+ <key name="icon-saturation" type="d">
+ <default>0.0</default>
+ <summary>Saturation</summary>
+ </key>
+ <key name="icon-brightness" type="d">
+ <default>0.0</default>
+ <summary>Brightness</summary>
+ </key>
+ <key name="icon-contrast" type="d">
+ <default>0.0</default>
+ <summary>Contrast</summary>
+ </key>
+ <key name="icon-opacity" type="i">
+ <default>240</default>
+ <summary>Opacity</summary>
+ </key>
+ <key name="icon-size" type="i">
+ <default>0</default>
+ <summary>Icon size</summary>
+ <description>Icon size in pixel</description>
+ </key>
+ <key name="compact-mode-enabled" type="b">
+ <default>true</default>
+ <summary>Compact Mode</summary>
+ <description>Reduces the Spacing Between Indicators</description>
+ </key>
+ <key name="tray-pos" type="s">
+ <default>"right"</default>
+ <summary>Position in tray</summary>
+ <description>Set where the Icon tray should appear in Gnome tray</description>
+ </key>
+ <key name="tray-order" type="i">
+ <default>1</default>
+ <summary>Order in tray</summary>
+ <description>Set where the Icon tray should appear among other trays</description>
+ </key>
+ <key name="custom-icons" type="a(sss)">
+ <default>[]</default>
+ <summary>Custom icons</summary>
+ <description>Replace any icons with custom icons from themes</description>
+ </key>
+ </schema>
+</schemalist>
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js
new file mode 100644
index 0000000..6d943b9
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js
@@ -0,0 +1,55 @@
+// 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.
+
+let settingsManager;
+
+export class SettingsManager {
+ static initialize(extension) {
+ SettingsManager._settingsManager = new SettingsManager(extension);
+ }
+
+ static destroy() {
+ SettingsManager._settingsManager.destroy();
+ SettingsManager._settingsManager = null;
+ }
+
+ static getDefault() {
+ return this._settingsManager;
+ }
+
+ get gsettings() {
+ return this._gsettings;
+ }
+
+ constructor(extension) {
+ if (settingsManager)
+ throw new Error('SettingsManager is already constructed');
+
+ this._gsettings = extension.getSettings();
+ }
+
+ destroy() {
+ this._gsettings = null;
+ }
+}
+
+export function getDefault() {
+ return SettingsManager.getDefault();
+}
+
+export function getDefaultGSettings() {
+ return SettingsManager.getDefault().gsettings;
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js
new file mode 100644
index 0000000..2c5e54f
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js
@@ -0,0 +1,320 @@
+// 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 Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+import * as AppIndicator from './appIndicator.js';
+import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
+import * as Interfaces from './interfaces.js';
+import * as PromiseUtils from './promiseUtils.js';
+import * as Util from './util.js';
+import * as DBusUtils from './dbusUtils.js';
+import * as DBusMenu from './dbusMenu.js';
+
+import {DBusProxy} from './dbusProxy.js';
+
+Gio._promisify(Gio.Subprocess.prototype, 'wait_async');
+Gio._promisify(Gio.Subprocess.prototype, 'communicate_async');
+Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async', 'read_line_finish_utf8');
+
+
+// TODO: replace with org.freedesktop and /org/freedesktop when approved
+const KDE_PREFIX = 'org.kde';
+
+export const WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`;
+const WATCHER_OBJECT = '/StatusNotifierWatcher';
+
+const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem';
+
+/*
+ * The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object
+ */
+export class StatusNotifierWatcher {
+ constructor(extension, watchDog) {
+ this._watchDog = watchDog;
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this);
+ try {
+ this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT);
+ } catch (e) {
+ Util.Logger.warn(`Failed to export ${WATCHER_OBJECT}`);
+ logError(e);
+ }
+ this._cancellable = new Gio.Cancellable();
+ this._everAcquiredName = false;
+ this._ownName = Gio.DBus.session.own_name(WATCHER_BUS_NAME,
+ Gio.BusNameOwnerFlags.NONE,
+ this._acquiredName.bind(this),
+ this._lostName.bind(this));
+ this._items = new Map();
+
+ try {
+ this._dbusImpl.emit_signal('StatusNotifierHostRegistered', null);
+ } catch (e) {
+ Util.Logger.warn(`Failed to notify registered host ${WATCHER_OBJECT}`);
+ }
+
+ this._seekStatusNotifierItems(extension).catch(e => {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e, 'Looking for StatusNotifierItem\'s');
+ });
+ }
+
+ _acquiredName() {
+ this._everAcquiredName = true;
+ this._watchDog.nameAcquired = true;
+ }
+
+ _lostName() {
+ if (this._everAcquiredName)
+ Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`);
+ else
+ Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`);
+ this._watchDog.nameAcquired = false;
+ }
+
+ async _registerItem(service, busName, objPath) {
+ const id = Util.indicatorId(service, busName, objPath);
+
+ if (this._items.has(id)) {
+ Util.Logger.warn(`Item ${id} is already registered`);
+ return;
+ }
+
+ Util.Logger.debug(`Registering StatusNotifierItem ${id}`);
+
+ try {
+ const indicator = new AppIndicator.AppIndicator(service, busName, objPath);
+ this._items.set(id, indicator);
+ indicator.connect('destroy', () => this._onIndicatorDestroyed(indicator));
+
+ indicator.connect('name-owner-changed', async () => {
+ if (!indicator.hasNameOwner) {
+ try {
+ await new PromiseUtils.TimeoutPromise(500,
+ GLib.PRIORITY_DEFAULT, this._cancellable);
+ if (this._items.has(id) && !indicator.hasNameOwner)
+ indicator.destroy();
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }
+ }
+ });
+
+ // if the desktop is not ready delay the icon creation and signal emissions
+ await Util.waitForStartupCompletion(indicator.cancellable);
+ const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator);
+ IndicatorStatusIcon.addIconToPanel(statusIcon);
+
+ this._dbusImpl.emit_signal('StatusNotifierItemRegistered',
+ GLib.Variant.new('(s)', [indicator.uniqueId]));
+ this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
+ GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ throw e;
+ }
+ }
+
+ async _ensureItemRegistered(service, busName, objPath) {
+ const id = Util.indicatorId(service, busName, objPath);
+ const item = this._items.get(id);
+
+ if (item) {
+ // delete the old one and add the new indicator
+ Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`);
+ item.reset();
+ return;
+ }
+
+ await this._registerItem(service, busName, objPath);
+ }
+
+ async _seekStatusNotifierItems(extension) {
+ // Some indicators (*coff*, dropbox, *coff*) do not re-register again
+ // when the plugin is enabled/disabled, thus we need to manually look
+ // for the objects in the session bus that implements the
+ // StatusNotifierItem interface... However let's do it after a low
+ // priority timeout, and using an external process so that it won't
+ // affect startup or memory (as it seems that gjs is not great at
+ // handling the memory of the bus analyzer async code).
+ const cancellable = this._cancellable;
+ await new PromiseUtils.TimeoutSecondsPromise(2, GLib.PRIORITY_LOW, cancellable);
+ const busAnalyzer = GLib.build_filenamev([
+ extension.path, 'tools', 'busAnalyzer.js',
+ ]);
+
+ const subProcess = Gio.Subprocess.new(['gjs', '-m', busAnalyzer],
+ Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
+
+ const stdOut = subProcess.get_stdout_pipe();
+ const dataInputStream = new Gio.DataInputStream({base_stream: stdOut});
+ const textDecoder = new TextDecoder();
+
+ while (true) {
+ // eslint-disable-next-line no-await-in-loop
+ const [line] = await dataInputStream.read_line_async(GLib.PRIORITY_DEFAULT,
+ cancellable);
+ if (!line)
+ break;
+
+ try {
+ const {services, name, path} = JSON.parse(textDecoder.decode(line));
+ const ids = [null, ...services].map(s => Util.indicatorId(s, name, path));
+
+ if (ids.every(id => !this._items.has(id))) {
+ const service = services.find(s =>
+ s && s.startsWith('org.kde.StatusNotifierItem')) || services[0];
+ const id = Util.indicatorId(
+ path === DEFAULT_ITEM_OBJECT_PATH ? service : null,
+ name, path);
+ Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`);
+ // eslint-disable-next-line no-await-in-loop
+ await this._registerItem(service, name, path);
+ }
+ } catch (e) {
+ logError(e);
+ }
+ }
+
+ const [, stdErr] = await subProcess.communicate_async(null, cancellable);
+ await subProcess.wait_async(cancellable);
+
+ if (subProcess.get_exit_status() !== 0) {
+ const errorLines = textDecoder.decode(stdErr.toArray()).split('\n');
+ const error = new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED,
+ errorLines[0]);
+ error.stack = `${errorLines.slice(3).join('\n')}${error.stack}`;
+ throw error;
+ }
+ }
+
+ async RegisterStatusNotifierItemAsync(params, invocation) {
+ // it would be too easy if all application behaved the same
+ // instead, ayatana patched gnome apps to send a path
+ // while kde apps send a bus name
+ const [service] = params;
+ let busName, objPath;
+
+ if (service.charAt(0) === '/') { // looks like a path
+ busName = invocation.get_sender();
+ objPath = service;
+ } else if (service.match(DBusUtils.BUS_ADDRESS_REGEX)) {
+ try {
+ busName = await DBusUtils.getUniqueBusName(invocation.get_connection(),
+ service, this._cancellable);
+ } catch (e) {
+ logError(e);
+ }
+ objPath = DEFAULT_ITEM_OBJECT_PATH;
+ }
+
+ if (!busName || !objPath) {
+ const error = `Impossible to register an indicator for parameters '${
+ service.toString()}'`;
+ Util.Logger.warn(error);
+
+ invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
+ error);
+ return;
+ }
+
+ try {
+ await this._ensureItemRegistered(service, busName, objPath);
+ invocation.return_value(null);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
+ e.message);
+ }
+ }
+
+ _onIndicatorDestroyed(indicator) {
+ const {uniqueId} = indicator;
+ this._items.delete(uniqueId);
+
+ try {
+ this._dbusImpl.emit_signal('StatusNotifierItemUnregistered',
+ GLib.Variant.new('(s)', [uniqueId]));
+ this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
+ GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
+ } catch (e) {
+ Util.Logger.warn(`Failed to emit signals: ${e}`);
+ }
+ }
+
+ RegisterStatusNotifierHostAsync(_service, invocation) {
+ invocation.return_error_literal(
+ Gio.DBusError,
+ Gio.DBusError.NOT_SUPPORTED,
+ 'Registering additional notification hosts is not supported');
+ }
+
+ IsNotificationHostRegistered() {
+ return true;
+ }
+
+ get RegisteredStatusNotifierItems() {
+ return Array.from(this._items.values()).map(i => i.uniqueId);
+ }
+
+ get IsStatusNotifierHostRegistered() {
+ return true;
+ }
+
+ get ProtocolVersion() {
+ return 0;
+ }
+
+ destroy() {
+ if (this._isDestroyed)
+ return;
+
+ // this doesn't do any sync operation and doesn't allow us to hook up
+ // the event of being finished which results in our unholy debounce hack
+ // (see extension.js)
+ this._items.forEach(indicator => indicator.destroy());
+ this._cancellable.cancel();
+
+ try {
+ this._dbusImpl.emit_signal('StatusNotifierHostUnregistered', null);
+ } catch (e) {
+ Util.Logger.warn(`Failed to emit uinregistered signal: ${e}`);
+ }
+
+ Gio.DBus.session.unown_name(this._ownName);
+
+ try {
+ this._dbusImpl.unexport();
+ } catch (e) {
+ Util.Logger.warn(`Failed to unexport watcher object: ${e}`);
+ }
+
+ DBusMenu.DBusClient.destroy();
+ AppIndicator.AppIndicatorProxy.destroy();
+ DBusProxy.destroy();
+ Util.destroyDefaultTheme();
+
+ this._dbusImpl.run_dispose();
+ delete this._dbusImpl;
+
+ delete this._items;
+ this._isDestroyed = true;
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/tools/busAnalyzer.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/tools/busAnalyzer.js
new file mode 100644
index 0000000..969f4e6
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/tools/busAnalyzer.js
@@ -0,0 +1,46 @@
+#!/usr/bin/env gjs -m
+
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import GioUnix from 'gi://GioUnix';
+
+import * as DBusUtils from '../dbusUtils.js';
+
+async function seekStatusNotifierItems() {
+ // Some indicators (*coff*, dropbox, *coff*) do not re-register again
+ // when the plugin is enabled/disabled, thus we need to manually look
+ // for the objects in the session bus that implements the StatusNotifierItem
+ // interface...
+ const cancellable = null;
+ const bus = Gio.DBus.session;
+ const uniqueNames = await DBusUtils.getBusNames(bus, cancellable);
+
+ const stdErrOutputStream = new GioUnix.OutputStream({fd: 1, closeFd: true});
+ const introspectName = async name => {
+ const nodes = DBusUtils.introspectBusObject(bus, name, cancellable,
+ ['org.kde.StatusNotifierItem']);
+ const services = [...uniqueNames.get(name)];
+
+ for await (const node of nodes) {
+ const {path} = node;
+ stdErrOutputStream.write(`${JSON.stringify({services, name, path})}\n`,
+ cancellable);
+ }
+ };
+ await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
+}
+
+function main(_argv) {
+ const loop = new GLib.MainLoop(null, false);
+
+ let exitCode = 0;
+ seekStatusNotifierItems().catch(e => {
+ logError(e);
+ exitCode = 1;
+ }).finally(() => loop.quit());
+ loop.run();
+
+ return exitCode;
+}
+
+imports.system.exit(main(ARGV));
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js
new file mode 100644
index 0000000..6ec7ccd
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js
@@ -0,0 +1,103 @@
+// 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 Shell from 'gi://Shell';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
+
+import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
+import * as Util from './util.js';
+import * as SettingsManager from './settingsManager.js';
+
+let trayIconsManager;
+
+export class TrayIconsManager extends Signals.EventEmitter {
+ static initialize() {
+ if (!trayIconsManager)
+ trayIconsManager = new TrayIconsManager();
+ return trayIconsManager;
+ }
+
+ static destroy() {
+ trayIconsManager.destroy();
+ }
+
+ constructor() {
+ super();
+
+ if (trayIconsManager)
+ throw new Error('TrayIconsManager is already constructed');
+
+ this._changedId = SettingsManager.getDefaultGSettings().connect(
+ 'changed::legacy-tray-enabled', () => this._toggle());
+
+ this._toggle();
+ }
+
+ _toggle() {
+ if (SettingsManager.getDefaultGSettings().get_boolean('legacy-tray-enabled'))
+ this._enable();
+ else
+ this._disable();
+ }
+
+ _getPanelBgColor() {
+ return Main.panel?.get_parent()
+ ? Main.panel.get_theme_node()?.get_background_color() : null;
+ }
+
+ _enable() {
+ if (this._tray)
+ return;
+
+ this._tray = new Shell.TrayManager({bgColor: this._getPanelBgColor()});
+ Util.connectSmart(this._tray, 'tray-icon-added', this, this.onTrayIconAdded);
+ Util.connectSmart(this._tray, 'tray-icon-removed', this, this.onTrayIconRemoved);
+
+ this._tray.manage_screen(Main.panel);
+ }
+
+ _disable() {
+ if (!this._tray)
+ return;
+
+ IndicatorStatusIcon.getTrayIcons().forEach(i => i.destroy());
+ this._tray.unmanage_screen();
+ this._tray = null;
+ }
+
+ onTrayIconAdded(_tray, icon) {
+ const trayIcon = new IndicatorStatusIcon.IndicatorStatusTrayIcon(icon);
+ IndicatorStatusIcon.addIconToPanel(trayIcon);
+ }
+
+ onTrayIconRemoved(_tray, icon) {
+ try {
+ const [trayIcon] = IndicatorStatusIcon.getTrayIcons().filter(i => i.icon === icon);
+ trayIcon.destroy();
+ } catch (e) {
+ Util.Logger.warning(`No icon container found for ${icon.title} (${icon})`);
+ }
+ }
+
+ destroy() {
+ this.emit('destroy');
+ SettingsManager.getDefaultGSettings().disconnect(this._changedId);
+ this._disable();
+ trayIconsManager = null;
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/util.js b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/util.js
new file mode 100644
index 0000000..7ec25b5
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/util.js
@@ -0,0 +1,271 @@
+// 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 Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as Config from 'resource:///org/gnome/shell/misc/config.js';
+import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
+
+import {Logger} from './logger.js';
+import {BaseStatusIcon} from './indicatorStatusIcon.js';
+import {BUS_ADDRESS_REGEX} from './dbusUtils.js';
+
+Gio._promisify(Gio._LocalFilePrototype, 'read');
+Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async');
+
+export function indicatorId(service, busName, objectPath) {
+ if (service !== busName && service?.match(BUS_ADDRESS_REGEX))
+ return service;
+
+ return `${busName}@${objectPath}`;
+}
+
+
+export class NameWatcher extends Signals.EventEmitter {
+ constructor(name) {
+ super();
+
+ this._watcherId = Gio.DBus.session.watch_name(name,
+ Gio.BusNameWatcherFlags.NONE, () => {
+ this._nameOnBus = true;
+ Logger.debug(`Name ${name} appeared`);
+ this.emit('changed');
+ this.emit('appeared');
+ }, () => {
+ this._nameOnBus = false;
+ Logger.debug(`Name ${name} vanished`);
+ this.emit('changed');
+ this.emit('vanished');
+ });
+ }
+
+ destroy() {
+ this.emit('destroy');
+
+ Gio.DBus.session.unwatch_name(this._watcherId);
+ delete this._watcherId;
+ }
+
+ get nameOnBus() {
+ return !!this._nameOnBus;
+ }
+}
+
+function connectSmart3A(src, signal, handler) {
+ const id = src.connect(signal, handler);
+ let destroyId = 0;
+
+ if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) {
+ destroyId = src.connect('destroy', () => {
+ src.disconnect(id);
+ src.disconnect(destroyId);
+ });
+ }
+
+ return [id, destroyId];
+}
+
+function connectSmart4A(src, signal, target, method) {
+ if (typeof method !== 'function')
+ throw new TypeError('Unsupported function');
+
+ method = method.bind(target);
+ const signalId = src.connect(signal, method);
+ const onDestroy = () => {
+ src.disconnect(signalId);
+ if (srcDestroyId)
+ src.disconnect(srcDestroyId);
+ if (tgtDestroyId)
+ target.disconnect(tgtDestroyId);
+ };
+
+ // GObject classes might or might not have a destroy signal
+ // JS Classes will not complain when connecting to non-existent signals
+ const srcDestroyId = src.connect && (!(src instanceof GObject.Object) ||
+ GObject.signal_lookup('destroy', src)) ? src.connect('destroy', onDestroy) : 0;
+ const tgtDestroyId = target.connect && (!(target instanceof GObject.Object) ||
+ GObject.signal_lookup('destroy', target)) ? target.connect('destroy', onDestroy) : 0;
+
+ return [signalId, srcDestroyId, tgtDestroyId];
+}
+
+// eslint-disable-next-line valid-jsdoc
+/**
+ * Connect signals to slots, and remove the connection when either source or
+ * target are destroyed
+ *
+ * Usage:
+ * Util.connectSmart(srcOb, 'signal', tgtObj, 'handler')
+ * or
+ * Util.connectSmart(srcOb, 'signal', () => { ... })
+ */
+export function connectSmart(...args) {
+ if (arguments.length === 4)
+ return connectSmart4A(...args);
+ else
+ return connectSmart3A(...args);
+}
+
+function disconnectSmart3A(src, signalIds) {
+ const [id, destroyId] = signalIds;
+ src.disconnect(id);
+
+ if (destroyId)
+ src.disconnect(destroyId);
+}
+
+function disconnectSmart4A(src, tgt, signalIds) {
+ const [signalId, srcDestroyId, tgtDestroyId] = signalIds;
+
+ disconnectSmart3A(src, [signalId, srcDestroyId]);
+
+ if (tgtDestroyId)
+ tgt.disconnect(tgtDestroyId);
+}
+
+export function disconnectSmart(...args) {
+ if (arguments.length === 2)
+ return disconnectSmart3A(...args);
+ else if (arguments.length === 3)
+ return disconnectSmart4A(...args);
+
+ throw new TypeError('Unexpected number of arguments');
+}
+
+let _defaultTheme;
+export function getDefaultTheme() {
+ if (_defaultTheme)
+ return _defaultTheme;
+
+ _defaultTheme = new St.IconTheme();
+ return _defaultTheme;
+}
+
+export function destroyDefaultTheme() {
+ _defaultTheme = null;
+}
+
+// eslint-disable-next-line valid-jsdoc
+/**
+ * Helper function to wait for the system startup to be completed.
+ * Adding widgets before the desktop is ready to accept them can result in errors.
+ */
+export async function waitForStartupCompletion(cancellable) {
+ if (Main.layoutManager._startingUp)
+ await Main.layoutManager.connect_once('startup-complete', cancellable);
+}
+
+export {Logger};
+
+export function versionCheck(required) {
+ const current = Config.PACKAGE_VERSION;
+ const currentArray = current.split('.');
+ const [major] = currentArray;
+ return major >= required;
+}
+
+export function tryCleanupOldIndicators() {
+ const indicatorType = BaseStatusIcon;
+ const indicators = Object.values(Main.panel.statusArea).filter(i => i instanceof indicatorType);
+
+ try {
+ const panelBoxes = [
+ Main.panel._leftBox, Main.panel._centerBox, Main.panel._rightBox,
+ ];
+
+ panelBoxes.forEach(box =>
+ indicators.push(...box.get_children().filter(i => i instanceof indicatorType)));
+ } catch (e) {
+ logError(e);
+ }
+
+ new Set(indicators).forEach(i => i.destroy());
+}
+
+export function addActor(obj, actor) {
+ if (obj.add_actor)
+ obj.add_actor(actor);
+ else
+ obj.add_child(actor);
+}
+
+export function removeActor(obj, actor) {
+ if (obj.remove_actor)
+ obj.remove_actor(actor);
+ else
+ obj.remove_child(actor);
+}
+
+export const CancellableChild = GObject.registerClass({
+ Properties: {
+ 'parent': GObject.ParamSpec.object(
+ 'parent', 'parent', 'parent',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ Gio.Cancellable.$gtype),
+ },
+},
+class CancellableChild extends Gio.Cancellable {
+ _init(parent) {
+ if (parent && !(parent instanceof Gio.Cancellable))
+ throw TypeError('Not a valid cancellable');
+
+ super._init({parent});
+
+ if (parent) {
+ if (parent.is_cancelled()) {
+ this.cancel();
+ return;
+ }
+
+ this._connectToParent();
+ }
+ }
+
+ _connectToParent() {
+ this._connectId = this.parent.connect(() => {
+ this._realCancel();
+
+ if (this._disconnectIdle)
+ return;
+
+ this._disconnectIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ delete this._disconnectIdle;
+ this._disconnectFromParent();
+ return GLib.SOURCE_REMOVE;
+ });
+ });
+ }
+
+ _disconnectFromParent() {
+ if (this._connectId && !this._disconnectIdle) {
+ this.parent.disconnect(this._connectId);
+ delete this._connectId;
+ }
+ }
+
+ _realCancel() {
+ Gio.Cancellable.prototype.cancel.call(this);
+ }
+
+ cancel() {
+ this._disconnectFromParent();
+ this._realCancel();
+ }
+});