1572 lines
53 KiB
JavaScript
1572 lines
53 KiB
JavaScript
|
// 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 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');
|
||
|
|
||
|
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.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_PROPERTY))
|
||
|
logError(e);
|
||
|
}
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
_onSignal(...args) {
|
||
|
this._onSignalAsync(...args).catch(e => {
|
||
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||
|
logError(e);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
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, this._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(`While refreshing property ${propertyName}: ${e}`);
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
this._commandLine = await Util.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() {
|
||
|
return 'padding: 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,
|
||
|
});
|
||
|
|
||
|
imageContent.set_bytes(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);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|