From 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 Mon Sep 17 00:00:00 2001 From: AlexanderCurl Date: Sat, 18 Apr 2026 17:46:06 +0100 Subject: Replaced file structures for themes in the correct format --- .../appIndicator.js | 1607 ++++++++++++++++++++ .../dbusMenu.js | 990 ++++++++++++ .../dbusProxy.js | 108 ++ .../dbusUtils.js | 121 ++ .../extension.js | 90 ++ .../iconCache.js | 179 +++ .../iconTheme.js | 12 + .../scalable/actions/custom-icons-symbolic.svg | 46 + .../actions/general-preferences-symbolic.svg | 1 + .../indicatorStatusIcon.js | 613 ++++++++ .../interfaces-xml/DBusMenu.xml | 66 + .../interfaces-xml/StatusNotifierItem.xml | 130 ++ .../interfaces-xml/StatusNotifierWatcher.xml | 38 + .../interfaces.js | 52 + .../locale/ar/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2722 bytes .../locale/bg/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2557 bytes .../locale/cs/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2221 bytes .../locale/de/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 949 bytes .../locale/fr/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2377 bytes .../locale/hu/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2646 bytes .../locale/it/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1205 bytes .../locale/ja/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 972 bytes .../locale/ka/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 3640 bytes .../locale/ko/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2373 bytes .../locale/nl/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1024 bytes .../locale/oc/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2334 bytes .../locale/pl/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2451 bytes .../pt_BR/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1067 bytes .../locale/ru/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2727 bytes .../locale/sk/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2239 bytes .../locale/sr/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1132 bytes .../sr@latin/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1020 bytes .../locale/ta/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 3523 bytes .../locale/tr/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2497 bytes .../locale/uk/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 3061 bytes .../zh_CN/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2338 bytes .../zh_TW/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2449 bytes .../logger.js | 105 ++ .../metadata.json | 18 + .../pixmapsUtils.js | 68 + .../preferences/customIconPage.js | 240 +++ .../preferences/generalPage.js | 159 ++ .../prefs.js | 45 + .../promiseUtils.js | 324 ++++ .../schemas/gschemas.compiled | Bin 0 -> 820 bytes ...gnome.shell.extensions.appindicator.gschema.xml | 49 + .../settingsManager.js | 55 + .../statusNotifierWatcher.js | 320 ++++ .../tools/busAnalyzer.js | 46 + .../trayIconsManager.js | 103 ++ .../appindicatorsupport@rgcjonas.gmail.com/util.js | 271 ++++ 51 files changed, 5856 insertions(+) create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusUtils.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/extension.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconCache.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconTheme.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/custom-icons-symbolic.svg create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/general-preferences-symbolic.svg create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ar/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/bg/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ka/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/uk/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_TW/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/logger.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/metadata.json create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/customIconPage.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/generalPage.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/prefs.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/tools/busAnalyzer.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/util.js (limited to 'raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com') diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js new file mode 100644 index 0000000..7efded2 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js new file mode 100644 index 0000000..52c812d --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js new file mode 100644 index 0000000..047f2d2 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusUtils.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusUtils.js new file mode 100644 index 0000000..d4d7a96 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/extension.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/extension.js new file mode 100644 index 0000000..6321077 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconCache.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconCache.js new file mode 100644 index 0000000..3610184 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconTheme.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/iconTheme.js new file mode 100644 index 0000000..cdd6540 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/custom-icons-symbolic.svg b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/custom-icons-symbolic.svg @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/general-preferences-symbolic.svg b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/icons/hicolor/scalable/actions/general-preferences-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js new file mode 100644 index 0000000..533789e --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml new file mode 100644 index 0000000..e98e04e --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml new file mode 100644 index 0000000..8c36294 --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml new file mode 100644 index 0000000..e450d04 --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/interfaces.js new file mode 100644 index 0000000..0a97907 --- /dev/null +++ b/raveos-gnome-theme/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 `${nodeContents}`; + } 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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ar/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ar/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..6c73616 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ar/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/bg/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/bg/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..2289e6d Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/bg/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..4cedb41 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..6edb419 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..ba0d667 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..ffbe4ed Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..8b4b367 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..d22309b Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ka/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ka/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..0773c28 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ka/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..6d33af8 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..9d81088 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..fe31bca Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..4536d6e Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..0bb1b44 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..b4dfbe5 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..060d9c3 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..a7770c5 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..af2179f Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..9b6e5a6 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..d31e5f2 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/uk/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/uk/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..88c7fbc Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/uk/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..df28844 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_TW/LC_MESSAGES/AppIndicatorExtension.mo b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_TW/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000..f66fd67 Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/locale/zh_TW/LC_MESSAGES/AppIndicatorExtension.mo differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/logger.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/logger.js new file mode 100644 index 0000000..3c60400 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/metadata.json b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/metadata.json new file mode 100644 index 0000000..96e3e2d --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js new file mode 100644 index 0000000..ce4c3f3 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/customIconPage.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/customIconPage.js new file mode 100644 index 0000000..b132562 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/generalPage.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/preferences/generalPage.js new file mode 100644 index 0000000..7a2575b --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/prefs.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/prefs.js new file mode 100644 index 0000000..344ba0a --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js new file mode 100644 index 0000000..2749b03 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled new file mode 100644 index 0000000..1a6beff Binary files /dev/null and b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled differ diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml @@ -0,0 +1,49 @@ + + + + true + Enable legacy tray icons support + + + 0.0 + Saturation + + + 0.0 + Brightness + + + 0.0 + Contrast + + + 240 + Opacity + + + 0 + Icon size + Icon size in pixel + + + true + Compact Mode + Reduces the Spacing Between Indicators + + + "right" + Position in tray + Set where the Icon tray should appear in Gnome tray + + + 1 + Order in tray + Set where the Icon tray should appear among other trays + + + [] + Custom icons + Replace any icons with custom icons from themes + + + diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js new file mode 100644 index 0000000..6d943b9 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js new file mode 100644 index 0000000..2c5e54f --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/tools/busAnalyzer.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/tools/busAnalyzer.js new file mode 100644 index 0000000..969f4e6 --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js new file mode 100644 index 0000000..6ec7ccd --- /dev/null +++ b/raveos-gnome-theme/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-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/util.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/util.js new file mode 100644 index 0000000..7ec25b5 --- /dev/null +++ b/raveos-gnome-theme/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(); + } +}); -- cgit v1.3